Compare commits

...

15 Commits

Author SHA1 Message Date
Gauthier
906ca61b60 fix: bypass cacheable-lookup when resolving localhost 2024-06-13 00:14:30 +02:00
fallenbagel
0342127058 fix: bypass cache-able lookups when resolving localhost 2024-06-13 01:13:07 +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
Fallenbagel
4757f1c3e5 Revert "ci: update format check command to ignore .prettierignore files (#787)" (#788)
This reverts commit 1f1ad72e9e.
2024-06-01 06:10:07 +05:00
Fallenbagel
1f1ad72e9e ci: update format check command to ignore .prettierignore files (#787)
This is to try and fix formatting issues on #773 on a file
that should be ignored.
2024-06-01 05:52:14 +05:00
allcontributors[bot]
c3ddc860b6 docs: add ThowZzy as a contributor for code (#779)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-05-29 00:32:43 +05:00
ThowZzy
2bd125d9a5 fix(auth): case-sensitive logins not updating authtokens (#778) 2024-05-28 23:42:26 +05:00
Fallenbagel
7a5e8d69bf feat(settings): stores jellyfin/emby server name in the settings (#763)
Stores jellyfin/emby(?) server name in the settings file. This might come in handy in the future
once simultaneous multi-server sync is implemented.
2024-05-26 18:21:14 +05:00
Fallenbagel
650c339d74 fix(jellyfinapi): use external api class for jellyfin api requests (#762)
* refactor(jellyfinapi): use the external api class for jellyfin api requests

refactors jellyfin api requests to be handled by the external api
to be consistent with how other external api requests are made

related #728, related #387

* style: prettier formatted

* refactor(jellyfinapi): rename device in auth header as jellyseerr

* refactor(error): rename api error code generic to unknown

* refactor(errorcodes): consistent casing of error code enums
2024-05-25 15:44:36 +05:00
Fallenbagel
4ef5a3c7c5 style: ran prettier on snap yaml file (#774) 2024-05-25 06:10:19 +05:00
14 changed files with 227 additions and 140 deletions

View File

@@ -367,6 +367,15 @@
"contributions": [ "contributions": [
"translation" "translation"
] ]
},
{
"login": "ThowZzy",
"name": "ThowZzy",
"avatar_url": "https://avatars.githubusercontent.com/u/61882536?v=4",
"profile": "https://github.com/ThowZzy",
"contributions": [
"code"
]
} }
] ]
} }

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

@@ -2,7 +2,7 @@ name: Publish Snap
# turn off edge snap builds temporarily and make it manual # turn off edge snap builds temporarily and make it manual
# on: # on:
# push: # push:
# branches: # branches:
# - develop # - develop

View File

@@ -11,7 +11,7 @@
<a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a> <a href="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a> <a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-39-orange.svg"/></a> <a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-40-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- ALL-CONTRIBUTORS-BADGE:END -->
**Jellyseerr** is a free and open source software application for managing requests for your media library. **Jellyseerr** is a free and open source software application for managing requests for your media library.
@@ -236,6 +236,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kara-Zor-El"><img src="https://avatars.githubusercontent.com/u/69772087?v=4?s=100" width="100px;" alt="Kara"/><br /><sub><b>Kara</b></sub></a><br /><a href="#infra-Kara-Zor-El" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Kara-Zor-El"><img src="https://avatars.githubusercontent.com/u/69772087?v=4?s=100" width="100px;" alt="Kara"/><br /><sub><b>Kara</b></sub></a><br /><a href="#infra-Kara-Zor-El" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

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

@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import ExternalAPI from '@server/api/externalapi';
import { ApiErrorCode } from '@server/constants/error'; import { ApiErrorCode } from '@server/constants/error';
import availabilitySync from '@server/lib/availabilitySync'; import availabilitySync from '@server/lib/availabilitySync';
import logger from '@server/logger'; import logger from '@server/logger';
import { ApiError } from '@server/types/error'; import { ApiError } from '@server/types/error';
import type { AxiosInstance } from 'axios'; import { getAppVersion } from '@server/utils/appVersion';
import axios from 'axios';
export interface JellyfinUserResponse { export interface JellyfinUserResponse {
Name: string; Name: string;
@@ -92,31 +92,33 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
DateCreated?: string; DateCreated?: string;
} }
class JellyfinAPI { class JellyfinAPI extends ExternalAPI {
private authToken?: string; private authToken?: string;
private userId?: string; private userId?: string;
private jellyfinHost: string; private jellyfinHost: string;
private axios: AxiosInstance;
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) { constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
this.jellyfinHost = jellyfinHost; let authHeaderVal: string;
this.authToken = authToken; if (authToken) {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
let authHeaderVal = '';
if (this.authToken) {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
} else { } else {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="10.8.0"`; authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}"`;
} }
this.axios = axios.create({ super(
baseURL: this.jellyfinHost, jellyfinHost,
headers: { {},
'X-Emby-Authorization': authHeaderVal, {
'Content-Type': 'application/json', headers: {
Accept: 'application/json', 'X-Emby-Authorization': authHeaderVal,
}, 'Content-Type': 'application/json',
}); Accept: 'application/json',
},
}
);
this.jellyfinHost = jellyfinHost;
this.authToken = authToken;
} }
public async login( public async login(
@@ -130,7 +132,8 @@ class JellyfinAPI {
'X-Forwarded-For': ClientIP, 'X-Forwarded-For': ClientIP,
} }
: {}; : {};
const account = await this.axios.post<JellyfinLoginResponse>(
const authResponse = await this.post<JellyfinLoginResponse>(
'/Users/AuthenticateByName', '/Users/AuthenticateByName',
{ {
Username: Username, Username: Username,
@@ -141,7 +144,7 @@ class JellyfinAPI {
} }
); );
return account.data; return authResponse;
} catch (e) { } catch (e) {
const status = e.response?.status; const status = e.response?.status;
@@ -177,66 +180,72 @@ class JellyfinAPI {
public async getServerName(): Promise<string> { public async getServerName(): Promise<string> {
try { try {
const account = await this.axios.get<JellyfinUserResponse>( const serverResponse = await this.get<JellyfinUserResponse>(
"/System/Info/Public'}" '/System/Info/Public'
); );
return account.data.ServerName;
return serverResponse.ServerName;
} catch (e) { } catch (e) {
logger.error( logger.error(
`Something went wrong while getting the server name from the Jellyfin server: ${e.message}`, `Something went wrong while getting the server name from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
throw new Error('girl idk');
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
} }
} }
public async getUsers(): Promise<JellyfinUserListResponse> { public async getUsers(): Promise<JellyfinUserListResponse> {
try { try {
const account = await this.axios.get(`/Users`); const userReponse = await this.get<JellyfinUserResponse[]>(`/Users`);
return { users: account.data };
return { users: userReponse };
} catch (e) { } catch (e) {
logger.error( logger.error(
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`, `Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
throw new Error('Invalid auth token');
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
public async getUser(): Promise<JellyfinUserResponse> { public async getUser(): Promise<JellyfinUserResponse> {
try { try {
const account = await this.axios.get<JellyfinUserResponse>( const userReponse = await this.get<JellyfinUserResponse>(
`/Users/${this.userId ?? 'Me'}` `/Users/${this.userId ?? 'Me'}`
); );
return account.data; return userReponse;
} catch (e) { } catch (e) {
logger.error( logger.error(
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`, `Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
throw new Error('Invalid auth token');
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
public async getLibraries(): Promise<JellyfinLibrary[]> { public async getLibraries(): Promise<JellyfinLibrary[]> {
try { try {
const mediaFolders = await this.axios.get<any>(`/Library/MediaFolders`); const mediaFolderResponse = await this.get<any>(`/Library/MediaFolders`);
return this.mapLibraries(mediaFolders.data.Items); return this.mapLibraries(mediaFolderResponse.Items);
} catch (mediaFoldersError) { } catch (mediaFoldersResponseError) {
// fallback to user views to get libraries // fallback to user views to get libraries
// this only affects LDAP users // this only and maybe/depending on factors affects LDAP users
try { try {
const mediaFolders = await this.axios.get<any>( const mediaFolderResponse = await this.get<any>(
`/Users/${this.userId ?? 'Me'}/Views` `/Users/${this.userId ?? 'Me'}/Views`
); );
return this.mapLibraries(mediaFolders.data.Items); return this.mapLibraries(mediaFolderResponse.Items);
} catch (e) { } catch (e) {
logger.error( logger.error(
`Something went wrong while getting libraries from the Jellyfin server: ${e.message}`, `Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
return []; return [];
} }
} }
@@ -270,11 +279,11 @@ class JellyfinAPI {
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> { public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
try { try {
const contents = await this.axios.get<any>( const libraryItemsResponse = await this.get<any>(
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}&collapseBoxSetItems=false` `/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}&collapseBoxSetItems=false`
); );
return contents.data.Items.filter( return libraryItemsResponse.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual' (item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
); );
} catch (e) { } catch (e) {
@@ -282,23 +291,25 @@ class JellyfinAPI {
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`, `Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
throw new Error('Invalid auth token');
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> { public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
try { try {
const contents = await this.axios.get<any>( const itemResponse = await this.get<any>(
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}` `/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}`
); );
return contents.data; return itemResponse;
} catch (e) { } catch (e) {
logger.error( logger.error(
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`, `Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
throw new Error('Invalid auth token');
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -306,36 +317,38 @@ class JellyfinAPI {
id: string id: string
): Promise<JellyfinLibraryItemExtended | undefined> { ): Promise<JellyfinLibraryItemExtended | undefined> {
try { try {
const contents = await this.axios.get<any>( const itemResponse = await this.get<any>(
`/Users/${this.userId}/Items/${id}` `/Users/${this.userId}/Items/${id}`
); );
return contents.data; return itemResponse;
} catch (e) { } catch (e) {
if (availabilitySync.running) { if (availabilitySync.running) {
if (e.response && e.response.status === 500) { if (e.response && e.response.status === 500) {
return undefined; return undefined;
} }
} }
logger.error( logger.error(
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`, `Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
throw new Error('Invalid auth token'); throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
public async getSeasons(seriesID: string): Promise<JellyfinLibraryItem[]> { public async getSeasons(seriesID: string): Promise<JellyfinLibraryItem[]> {
try { try {
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`); const seasonResponse = await this.get<any>(`/Shows/${seriesID}/Seasons`);
return contents.data.Items; return seasonResponse.Items;
} catch (e) { } catch (e) {
logger.error( logger.error(
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`, `Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
throw new Error('Invalid auth token');
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
@@ -344,11 +357,11 @@ class JellyfinAPI {
seasonID: string seasonID: string
): Promise<JellyfinLibraryItem[]> { ): Promise<JellyfinLibraryItem[]> {
try { try {
const contents = await this.axios.get<any>( const episodeResponse = await this.get<any>(
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}` `/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
); );
return contents.data.Items.filter( return episodeResponse.Items.filter(
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual' (item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
); );
} catch (e) { } catch (e) {
@@ -356,7 +369,8 @@ class JellyfinAPI {
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`, `Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,
{ label: 'Jellyfin API' } { label: 'Jellyfin API' }
); );
throw new Error('Invalid auth token');
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
} }
} }
} }

View File

@@ -1,5 +1,7 @@
export enum ApiErrorCode { export enum ApiErrorCode {
InvalidUrl = 'INVALID_URL', InvalidUrl = 'INVALID_URL',
InvalidCredentials = 'INVALID_CREDENTIALS', InvalidCredentials = 'INVALID_CREDENTIALS',
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
NotAdmin = 'NOT_ADMIN', NotAdmin = 'NOT_ADMIN',
Unknown = 'UNKNOWN',
} }

View File

@@ -23,19 +23,25 @@ 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';
import { lookup } from 'dns';
import type { NextFunction, Request, Response } from 'express'; import type { NextFunction, Request, Response } from 'express';
import express from 'express'; import express from 'express';
import * as OpenApiValidator from 'express-openapi-validator'; 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 +52,25 @@ 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();
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<typeof lookup>));
} else {
originalLookup(...(args as Parameters<typeof originalLookup>));
}
};
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
@@ -314,6 +325,9 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
userType: UserType.JELLYFIN, userType: UserType.JELLYFIN,
}); });
const serverName = await jellyfinserver.getServerName();
settings.jellyfin.name = serverName;
settings.jellyfin.hostname = body.hostname ?? ''; settings.jellyfin.hostname = body.hostname ?? '';
settings.jellyfin.serverId = account.User.ServerId; settings.jellyfin.serverId = account.User.ServerId;
settings.save(); settings.save();
@@ -322,7 +336,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
await userRepository.save(user); await userRepository.save(user);
} }
// User already exists, let's update their information // User already exists, let's update their information
else if (body.username === user?.jellyfinUsername) { else if (account.User.Id === user?.jellyfinUserId) {
logger.info( logger.info(
`Found matching ${ `Found matching ${
settings.main.mediaServerType === MediaServerType.JELLYFIN settings.main.mediaServerType === MediaServerType.JELLYFIN

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"