diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1eeacd77..8f838a52 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,60 +35,60 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run: npx semantic-release - build-snap: - name: Build Snap Package (${{ matrix.architecture }}) - needs: semantic-release - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - architecture: - - amd64 - - arm64 - - armhf - steps: - - name: Checkout Code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Switch to main branch - run: git checkout main - - name: Pull latest changes - run: git pull - - name: Prepare - id: prepare - run: | - git fetch --prune --tags - if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then - echo "RELEASE=stable" >> $GITHUB_OUTPUT - else - echo "RELEASE=edge" >> $GITHUB_OUTPUT - fi - - name: Set Up QEMU - uses: docker/setup-qemu-action@v3 - with: - image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde - - name: Build Snap Package - uses: diddlesnaps/snapcraft-multiarch-action@v1 - id: build - with: - architecture: ${{ matrix.architecture }} - - name: Upload Snap Package - uses: actions/upload-artifact@v4 - with: - name: jellyseerr-snap-package-${{ matrix.architecture }} - path: ${{ steps.build.outputs.snap }} - - name: Review Snap Package - uses: diddlesnaps/snapcraft-review-tools-action@v1 - with: - snap: ${{ steps.build.outputs.snap }} - - name: Publish Snap Package - uses: snapcore/action-publish@v1 - env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }} - with: - snap: ${{ steps.build.outputs.snap }} - release: ${{ steps.prepare.outputs.RELEASE }} + # build-snap: + # name: Build Snap Package (${{ matrix.architecture }}) + # needs: semantic-release + # runs-on: ubuntu-22.04 + # strategy: + # fail-fast: false + # matrix: + # architecture: + # - amd64 + # - arm64 + # - armhf + # steps: + # - name: Checkout Code + # uses: actions/checkout@v4 + # with: + # fetch-depth: 0 + # - name: Switch to main branch + # run: git checkout main + # - name: Pull latest changes + # run: git pull + # - name: Prepare + # id: prepare + # run: | + # git fetch --prune --tags + # if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then + # echo "RELEASE=stable" >> $GITHUB_OUTPUT + # else + # echo "RELEASE=edge" >> $GITHUB_OUTPUT + # fi + # - name: Set Up QEMU + # uses: docker/setup-qemu-action@v3 + # with: + # image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde + # - name: Build Snap Package + # uses: diddlesnaps/snapcraft-multiarch-action@v1 + # id: build + # with: + # architecture: ${{ matrix.architecture }} + # - name: Upload Snap Package + # uses: actions/upload-artifact@v4 + # with: + # name: jellyseerr-snap-package-${{ matrix.architecture }} + # path: ${{ steps.build.outputs.snap }} + # - name: Review Snap Package + # uses: diddlesnaps/snapcraft-review-tools-action@v1 + # with: + # snap: ${{ steps.build.outputs.snap }} + # - name: Publish Snap Package + # uses: snapcore/action-publish@v1 + # env: + # SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }} + # with: + # snap: ${{ steps.build.outputs.snap }} + # release: ${{ steps.prepare.outputs.RELEASE }} discord: name: Send Discord Notification diff --git a/package.json b/package.json index 97c02550..32b66e19 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "axios-rate-limit": "1.3.0", "bcrypt": "5.1.0", "bowser": "2.11.0", + "cacheable-lookup": "^7.0.0", "connect-typeorm": "1.1.4", "cookie-parser": "1.4.6", "copy-to-clipboard": "3.3.3", diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index b14b8fea..6c72ad57 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -126,25 +126,31 @@ class JellyfinAPI extends ExternalAPI { Password?: string, ClientIP?: string ): Promise { - try { - const headers = ClientIP - ? { - 'X-Forwarded-For': ClientIP, - } - : {}; + const authenticate = async (useHeaders: boolean) => { + const headers = + useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {}; - const authResponse = await this.post( + return this.post( '/Users/AuthenticateByName', { - Username: Username, + Username, Pw: Password, }, - { - headers: headers, - } + { headers } ); + }; - return authResponse; + try { + return await authenticate(true); + } catch (e) { + logger.debug(`Failed to authenticate with headers: ${e.message}`, { + label: 'Jellyfin API', + ip: ClientIP, + }); + } + + try { + return await authenticate(false); } catch (e) { const status = e.response?.status; diff --git a/server/index.ts b/server/index.ts index 477864c2..a9a74656 100644 --- a/server/index.ts +++ b/server/index.ts @@ -23,19 +23,25 @@ import imageproxy from '@server/routes/imageproxy'; import { getAppVersion } from '@server/utils/appVersion'; import restartFlag from '@server/utils/restartFlag'; import { getClientIp } from '@supercharge/request-ip'; +import type CacheableLookupType from 'cacheable-lookup'; import { TypeormStore } from 'connect-typeorm/out'; import cookieParser from 'cookie-parser'; import csurf from 'csurf'; +import { lookup } from 'dns'; import type { NextFunction, Request, Response } from 'express'; import express from 'express'; import * as OpenApiValidator from 'express-openapi-validator'; import type { Store } from 'express-session'; import session from 'express-session'; import next from 'next'; +import http from 'node:http'; +import https from 'node:https'; import path from 'path'; import swaggerUi from 'swagger-ui-express'; import YAML from 'yamljs'; +const _importDynamic = new Function('modulePath', 'return import(modulePath)'); + const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml'); logger.info(`Starting Overseerr version ${getAppVersion()}`); @@ -46,6 +52,25 @@ const handle = app.getRequestHandler(); app .prepare() .then(async () => { + const CacheableLookup = (await _importDynamic('cacheable-lookup')) + .default as typeof CacheableLookupType; + const cacheable = new CacheableLookup(); + + const originalLookup = cacheable.lookup; + + // if hostname is localhost use dns.lookup instead of cacheable-lookup + cacheable.lookup = (...args: any) => { + const [hostname] = args; + if (hostname === 'localhost') { + lookup(...(args as Parameters)); + } else { + originalLookup(...(args as Parameters)); + } + }; + + cacheable.install(http.globalAgent); + cacheable.install(https.globalAgent); + const dbConnection = await dataSource.initialize(); // Run migrations in production diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index 57edec47..fa7cdb22 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -84,13 +84,17 @@ class JellyfinScanner { } 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; }); }); 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; }); }); diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 424800ae..3b0d7e38 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -15,6 +15,7 @@ import { getHostname } from '@server/utils/getHostname'; import * as EmailValidator from 'email-validator'; import { Router } from 'express'; import gravatarUrl from 'gravatar-url'; +import net from 'net'; const authRoutes = Router(); @@ -278,11 +279,21 @@ authRoutes.post('/jellyfin', async (req, res, next) => { ? externalHostname : hostname; - 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( body.username, body.password, - ip + clientIp ); // Next let's see if the user already exists diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index b7dc5917..4ed69b6b 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -434,33 +434,38 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { )} - {hasPermission(Permission.MANAGE_REQUESTS) && data.mediaInfo && ( - - - - )} + {hasPermission(Permission.MANAGE_REQUESTS) && + data.mediaInfo && + (data.mediaInfo.jellyfinMediaId || + data.mediaInfo.jellyfinMediaId4k || + data.mediaInfo.status !== MediaStatus.UNKNOWN || + data.mediaInfo.status4k !== MediaStatus.UNKNOWN) && ( + + + + )}
diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 960746ad..b6371e7d 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -53,6 +53,8 @@ const messages = defineMessages({ discordId: 'Discord User ID', discordIdTip: 'The multi-digit ID number associated with your Discord user account', + validationemailrequired: 'Email required', + validationemailformat: 'Valid email required', validationDiscordId: 'You must provide a valid Discord user ID', plexwatchlistsyncmovies: 'Auto-Request Movies', plexwatchlistsyncmoviestip: @@ -88,6 +90,9 @@ const UserGeneralSettings = () => { ); const UserGeneralSettingsSchema = Yup.object().shape({ + email: Yup.string() + .email(intl.formatMessage(messages.validationemailformat)) + .required(intl.formatMessage(messages.validationemailrequired)), discordId: Yup.string() .nullable() .matches(/^\d{17,19}$/, intl.formatMessage(messages.validationDiscordId)), diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 82b6d585..53b3f7f1 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1185,6 +1185,8 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "User", "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.discordId": "User ID", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The multi-digit ID number associated with your user account", diff --git a/yarn.lock b/yarn.lock index 09b5a3ca..b9485510 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5033,6 +5033,11 @@ cacache@^16.0.0, cacache@^16.1.0, cacache@^16.1.3: tar "^6.1.11" 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: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"