Compare commits

...

7 Commits

Author SHA1 Message Date
fallenbagel
166796804e refactor: attempt to fix ip forwarding issue with more logging 2024-06-13 00:50:01 +05:00
Fallenbagel
9aeb3604e6 fix(auth): validation of ipv6/ipv4 (#812)
validation for ipv6 was sort of broken where for example `::1` was being sent as `1`, therefore,
logins were broken. This PR fixes it by using nodejs `net.isIPv4()` & `net.isIPv6` for ipv4 and ipv6
validation.

possibly related to and fixes #795
2024-06-12 18:50:00 +05:00
Fallenbagel
6eb88f8674 ci: temporarily disable snap release builds (#811) 2024-06-12 10:49:15 +05:00
Gauthier
46ee8a4ca1 fix(api): add DNS caching (#810)
fix #387 #657 #728
2024-06-12 02:56:10 +05:00
Gauthier
f52939e4cd fix: remove the settings button of media when useless (#809)
After the Media Availability Sync job rund on deleted media, the setting button is still visible
even if neither the media file nor the media request no longer exists. This PR hides this button
when it's no longer the case
2024-06-11 19:47:02 +05:00
Gauthier
d31a2c37e6 fix(jellyfinscanner): assign only 4k available badge for a 4k request instead of both badges (#805)
When you have a 4k server setup, and request a 4k item, when it becomes available it also sets the
normal item as available thus not allowing the user to request for the normal item
2024-06-11 17:58:48 +05:00
Gauthier
20863d4a8d fix: empty email in user settings (#807)
Email is mandatory for every user and required during the setup of Jellyseerr, but it is possible to
set it empty afterwards in the user settings. When the email is empty, users are not able to connect
to Jellyseer. This PR makes the email field mandatory in the user settings.

fix #803
2024-06-11 16:23:35 +05:00
10 changed files with 140 additions and 85 deletions

View File

@@ -35,60 +35,60 @@ jobs:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: npx semantic-release run: npx semantic-release
build-snap: # build-snap:
name: Build Snap Package (${{ matrix.architecture }}) # name: Build Snap Package (${{ matrix.architecture }})
needs: semantic-release # needs: semantic-release
runs-on: ubuntu-22.04 # runs-on: ubuntu-22.04
strategy: # strategy:
fail-fast: false # fail-fast: false
matrix: # matrix:
architecture: # architecture:
- amd64 # - amd64
- arm64 # - arm64
- armhf # - armhf
steps: # steps:
- name: Checkout Code # - name: Checkout Code
uses: actions/checkout@v4 # uses: actions/checkout@v4
with: # with:
fetch-depth: 0 # fetch-depth: 0
- name: Switch to main branch # - name: Switch to main branch
run: git checkout main # run: git checkout main
- name: Pull latest changes # - name: Pull latest changes
run: git pull # run: git pull
- name: Prepare # - name: Prepare
id: prepare # id: prepare
run: | # run: |
git fetch --prune --tags # git fetch --prune --tags
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then # if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
echo "RELEASE=stable" >> $GITHUB_OUTPUT # echo "RELEASE=stable" >> $GITHUB_OUTPUT
else # else
echo "RELEASE=edge" >> $GITHUB_OUTPUT # echo "RELEASE=edge" >> $GITHUB_OUTPUT
fi # fi
- name: Set Up QEMU # - name: Set Up QEMU
uses: docker/setup-qemu-action@v3 # uses: docker/setup-qemu-action@v3
with: # with:
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde # image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
- name: Build Snap Package # - name: Build Snap Package
uses: diddlesnaps/snapcraft-multiarch-action@v1 # uses: diddlesnaps/snapcraft-multiarch-action@v1
id: build # id: build
with: # with:
architecture: ${{ matrix.architecture }} # architecture: ${{ matrix.architecture }}
- name: Upload Snap Package # - name: Upload Snap Package
uses: actions/upload-artifact@v4 # uses: actions/upload-artifact@v4
with: # with:
name: jellyseerr-snap-package-${{ matrix.architecture }} # name: jellyseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }} # path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package # - name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1 # uses: diddlesnaps/snapcraft-review-tools-action@v1
with: # with:
snap: ${{ steps.build.outputs.snap }} # snap: ${{ steps.build.outputs.snap }}
- name: Publish Snap Package # - name: Publish Snap Package
uses: snapcore/action-publish@v1 # uses: snapcore/action-publish@v1
env: # env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }} # SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }}
with: # with:
snap: ${{ steps.build.outputs.snap }} # snap: ${{ steps.build.outputs.snap }}
release: ${{ steps.prepare.outputs.RELEASE }} # release: ${{ steps.prepare.outputs.RELEASE }}
discord: discord:
name: Send Discord Notification name: Send Discord Notification

View File

@@ -44,6 +44,7 @@
"axios-rate-limit": "1.3.0", "axios-rate-limit": "1.3.0",
"bcrypt": "5.1.0", "bcrypt": "5.1.0",
"bowser": "2.11.0", "bowser": "2.11.0",
"cacheable-lookup": "^7.0.0",
"connect-typeorm": "1.1.4", "connect-typeorm": "1.1.4",
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
"copy-to-clipboard": "3.3.3", "copy-to-clipboard": "3.3.3",

View File

@@ -133,6 +133,11 @@ class JellyfinAPI extends ExternalAPI {
} }
: {}; : {};
logger.debug(`Logging in to Jellyfin server: ${this.jellyfinHost}`, {
label: 'Jellyfin API',
clientIp: ClientIP,
});
const authResponse = await this.post<JellyfinLoginResponse>( const authResponse = await this.post<JellyfinLoginResponse>(
'/Users/AuthenticateByName', '/Users/AuthenticateByName',
{ {
@@ -146,6 +151,12 @@ class JellyfinAPI extends ExternalAPI {
return authResponse; return authResponse;
} catch (e) { } catch (e) {
logger.error('Failed to login to Jellyfin server', {
label: 'Jellyfin API',
clientIp: ClientIP,
error: e,
});
const status = e.response?.status; const status = e.response?.status;
const networkErrorCodes = new Set([ const networkErrorCodes = new Set([

View File

@@ -23,6 +23,7 @@ import imageproxy from '@server/routes/imageproxy';
import { getAppVersion } from '@server/utils/appVersion'; import { getAppVersion } from '@server/utils/appVersion';
import restartFlag from '@server/utils/restartFlag'; import restartFlag from '@server/utils/restartFlag';
import { getClientIp } from '@supercharge/request-ip'; import { getClientIp } from '@supercharge/request-ip';
import type CacheableLookupType from 'cacheable-lookup';
import { TypeormStore } from 'connect-typeorm/out'; import { TypeormStore } from 'connect-typeorm/out';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import csurf from 'csurf'; import csurf from 'csurf';
@@ -32,10 +33,14 @@ import * as OpenApiValidator from 'express-openapi-validator';
import type { Store } from 'express-session'; import type { Store } from 'express-session';
import session from 'express-session'; import session from 'express-session';
import next from 'next'; import next from 'next';
import http from 'node:http';
import https from 'node:https';
import path from 'path'; import path from 'path';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs'; import YAML from 'yamljs';
const _importDynamic = new Function('modulePath', 'return import(modulePath)');
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
logger.info(`Starting Overseerr version ${getAppVersion()}`); logger.info(`Starting Overseerr version ${getAppVersion()}`);
@@ -46,6 +51,12 @@ const handle = app.getRequestHandler();
app app
.prepare() .prepare()
.then(async () => { .then(async () => {
const CacheableLookup = (await _importDynamic('cacheable-lookup'))
.default as typeof CacheableLookupType;
const cacheable = new CacheableLookup();
cacheable.install(http.globalAgent);
cacheable.install(https.globalAgent);
const dbConnection = await dataSource.initialize(); const dbConnection = await dataSource.initialize();
// Run migrations in production // Run migrations in production

View File

@@ -83,13 +83,17 @@ class JellyfinScanner {
} }
const has4k = metadata.MediaSources?.some((MediaSource) => { const has4k = metadata.MediaSources?.some((MediaSource) => {
return MediaSource.MediaStreams.some((MediaStream) => { return MediaSource.MediaStreams.filter(
(MediaStream) => MediaStream.Type === 'Video'
).some((MediaStream) => {
return (MediaStream.Width ?? 0) > 2000; return (MediaStream.Width ?? 0) > 2000;
}); });
}); });
const hasOtherResolution = metadata.MediaSources?.some((MediaSource) => { const hasOtherResolution = metadata.MediaSources?.some((MediaSource) => {
return MediaSource.MediaStreams.some((MediaStream) => { return MediaSource.MediaStreams.filter(
(MediaStream) => MediaStream.Type === 'Video'
).some((MediaStream) => {
return (MediaStream.Width ?? 0) <= 2000; return (MediaStream.Width ?? 0) <= 2000;
}); });
}); });

View File

@@ -14,6 +14,7 @@ import { ApiError } from '@server/types/error';
import * as EmailValidator from 'email-validator'; import * as EmailValidator from 'email-validator';
import { Router } from 'express'; import { Router } from 'express';
import gravatarUrl from 'gravatar-url'; import gravatarUrl from 'gravatar-url';
import net from 'net';
const authRoutes = Router(); const authRoutes = Router();
@@ -271,11 +272,21 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
? jellyfinHost.slice(0, -1) ? jellyfinHost.slice(0, -1)
: jellyfinHost; : jellyfinHost;
const ip = req.ip ? req.ip.split(':').reverse()[0] : undefined; const ip = req.ip;
let clientIp;
if (ip) {
if (net.isIPv4(ip)) {
clientIp = ip;
} else if (net.isIPv6(ip)) {
clientIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
}
}
const account = await jellyfinserver.login( const account = await jellyfinserver.login(
body.username, body.username,
body.password, body.password,
ip clientIp
); );
// Next let's see if the user already exists // Next let's see if the user already exists

View File

@@ -434,33 +434,38 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
{hasPermission(Permission.MANAGE_REQUESTS) && data.mediaInfo && ( {hasPermission(Permission.MANAGE_REQUESTS) &&
<Tooltip content={intl.formatMessage(messages.managemovie)}> data.mediaInfo &&
<Button (data.mediaInfo.jellyfinMediaId ||
buttonType="ghost" data.mediaInfo.jellyfinMediaId4k ||
onClick={() => setShowManager(true)} data.mediaInfo.status !== MediaStatus.UNKNOWN ||
className="relative ml-2 first:ml-0" data.mediaInfo.status4k !== MediaStatus.UNKNOWN) && (
> <Tooltip content={intl.formatMessage(messages.managemovie)}>
<CogIcon className="!mr-0" /> <Button
{hasPermission( buttonType="ghost"
[Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES], onClick={() => setShowManager(true)}
{ className="relative ml-2 first:ml-0"
type: 'or', >
} <CogIcon className="!mr-0" />
) && {hasPermission(
( [Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES],
data.mediaInfo?.issues.filter( {
(issue) => issue.status === IssueStatus.OPEN type: 'or',
) ?? [] }
).length > 0 && ( ) &&
<> (
<div className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-red-600" /> data.mediaInfo?.issues.filter(
<div className="absolute -right-1 -top-1 h-3 w-3 animate-ping rounded-full bg-red-600" /> (issue) => issue.status === IssueStatus.OPEN
</> ) ?? []
)} ).length > 0 && (
</Button> <>
</Tooltip> <div className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-red-600" />
)} <div className="absolute -right-1 -top-1 h-3 w-3 animate-ping rounded-full bg-red-600" />
</>
)}
</Button>
</Tooltip>
)}
</div> </div>
</div> </div>
<div className="media-overview"> <div className="media-overview">

View File

@@ -53,6 +53,8 @@ const messages = defineMessages({
discordId: 'Discord User ID', discordId: 'Discord User ID',
discordIdTip: discordIdTip:
'The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your Discord user account', 'The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your Discord user account',
validationemailrequired: 'Email required',
validationemailformat: 'Valid email required',
validationDiscordId: 'You must provide a valid Discord user ID', validationDiscordId: 'You must provide a valid Discord user ID',
plexwatchlistsyncmovies: 'Auto-Request Movies', plexwatchlistsyncmovies: 'Auto-Request Movies',
plexwatchlistsyncmoviestip: plexwatchlistsyncmoviestip:
@@ -88,6 +90,9 @@ const UserGeneralSettings = () => {
); );
const UserGeneralSettingsSchema = Yup.object().shape({ const UserGeneralSettingsSchema = Yup.object().shape({
email: Yup.string()
.email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired)),
discordId: Yup.string() discordId: Yup.string()
.nullable() .nullable()
.matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)), .matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)),

View File

@@ -1177,6 +1177,8 @@
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User", "components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
"components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID", "components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID",
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Valid email required",
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "Email required",
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default", "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default",
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID", "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID",
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your user account", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your user account",

View File

@@ -5033,6 +5033,11 @@ cacache@^16.0.0, cacache@^16.1.0, cacache@^16.1.3:
tar "^6.1.11" tar "^6.1.11"
unique-filename "^2.0.0" unique-filename "^2.0.0"
cacheable-lookup@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27"
integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==
cachedir@2.3.0, cachedir@^2.3.0: cachedir@2.3.0, cachedir@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"