mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f735d86064 | ||
|
|
66c5de2bfa | ||
|
|
6eb88f8674 | ||
|
|
6cf1ac7295 | ||
|
|
46ee8a4ca1 | ||
|
|
f52939e4cd | ||
|
|
d31a2c37e6 | ||
|
|
20863d4a8d | ||
|
|
4757f1c3e5 | ||
|
|
1f1ad72e9e | ||
|
|
25bf4b275a | ||
|
|
103f028d99 | ||
|
|
c3ddc860b6 | ||
|
|
2bd125d9a5 | ||
|
|
7a5e8d69bf | ||
|
|
650c339d74 | ||
|
|
4ef5a3c7c5 | ||
|
|
a791b53953 | ||
|
|
68467ced9d | ||
|
|
296aee6338 | ||
|
|
0a4b38e50d | ||
|
|
bcc84d8551 | ||
|
|
783fda9621 | ||
|
|
d765055da8 | ||
|
|
fed66f0702 | ||
|
|
461202da75 | ||
|
|
0bbcfdc4f9 | ||
|
|
f486fb5e75 | ||
|
|
10082292e8 | ||
|
|
c0a0b9c8a8 | ||
|
|
d9d07c705a | ||
|
|
0eea1090df |
@@ -322,6 +322,60 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joshuaboniface",
|
||||
"name": "Joshua M. Boniface",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4031396?v=4",
|
||||
"profile": "https://www.boniface.me",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gauthier-th",
|
||||
"name": "Gauthier",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
|
||||
"profile": "https://gauthierth.fr/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Kara-Zor-El",
|
||||
"name": "Kara",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/69772087?v=4",
|
||||
"profile": "https://github.com/Kara-Zor-El",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JoaquinOlivero",
|
||||
"name": "Joaquin Olivero",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/66050823?v=4",
|
||||
"profile": "https://joaquinolivero.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Bretterteig",
|
||||
"name": "Julian Behr",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/47298401?v=4",
|
||||
"profile": "https://github.com/Bretterteig",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ThowZzy",
|
||||
"name": "ThowZzy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/61882536?v=4",
|
||||
"profile": "https://github.com/ThowZzy",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
github: [Fallenbagel]
|
||||
buy_me_a_coffee: fallen.bagel
|
||||
|
||||
26
.github/workflows/conflict_labeler.yml
vendored
Normal file
26
.github/workflows/conflict_labeler.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Merge Conflict Labeler
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request_target:
|
||||
branches:
|
||||
- develop
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
label:
|
||||
name: Labeling
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'Fallenbagel/jellyseerr' }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
108
.github/workflows/release.yml
vendored
108
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/snap.yaml
vendored
12
.github/workflows/snap.yaml
vendored
@@ -1,9 +1,13 @@
|
||||
name: Publish Snap
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
# turn off edge snap builds temporarily and make it manual
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches:
|
||||
# - develop
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
jobs:
|
||||
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,3 +1,38 @@
|
||||
## [1.9.1](https://github.com/fallenbagel/jellyseerr/compare/v1.9.0...v1.9.1) (2024-06-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** add DNS caching ([#810](https://github.com/fallenbagel/jellyseerr/issues/810)) ([46ee8a4](https://github.com/fallenbagel/jellyseerr/commit/46ee8a4ca13b026bd929b4027eb001cc74064bb8)), closes [#387](https://github.com/fallenbagel/jellyseerr/issues/387) [#657](https://github.com/fallenbagel/jellyseerr/issues/657) [#728](https://github.com/fallenbagel/jellyseerr/issues/728)
|
||||
* empty email in user settings ([#807](https://github.com/fallenbagel/jellyseerr/issues/807)) ([20863d4](https://github.com/fallenbagel/jellyseerr/commit/20863d4a8dabe78fb5c52995b5bcb2da557a804e)), closes [#803](https://github.com/fallenbagel/jellyseerr/issues/803)
|
||||
* **jellyfinscanner:** assign only 4k available badge for a 4k request instead of both badges ([#805](https://github.com/fallenbagel/jellyseerr/issues/805)) ([d31a2c3](https://github.com/fallenbagel/jellyseerr/commit/d31a2c37e639c1126b446277fa5d666d8102fef5))
|
||||
* remove the settings button of media when useless ([#809](https://github.com/fallenbagel/jellyseerr/issues/809)) ([f52939e](https://github.com/fallenbagel/jellyseerr/commit/f52939e4cdcbee94fc35165f613f6b3e21599e3c))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "ci: update format check command to ignore .prettierignore files (#787)" (#788) ([4757f1c](https://github.com/fallenbagel/jellyseerr/commit/4757f1c3e599304410a737c11f97db92a2bfcefd)), closes [#787](https://github.com/fallenbagel/jellyseerr/issues/787) [#788](https://github.com/fallenbagel/jellyseerr/issues/788)
|
||||
|
||||
# [1.9.0](https://github.com/fallenbagel/jellyseerr/compare/v1.8.1...v1.9.0) (2024-05-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** save user email on the first try ([#760](https://github.com/fallenbagel/jellyseerr/issues/760)) ([0bbcfdc](https://github.com/fallenbagel/jellyseerr/commit/0bbcfdc4f9ff9735f45232a2412ac8444f525de9)), closes [#227](https://github.com/fallenbagel/jellyseerr/issues/227) [#748](https://github.com/fallenbagel/jellyseerr/issues/748)
|
||||
* **api:** small errors on overseerr-api.yaml ([#721](https://github.com/fallenbagel/jellyseerr/issues/721)) ([0eea109](https://github.com/fallenbagel/jellyseerr/commit/0eea1090dfdba4333646280c84b09b0197fefa74))
|
||||
* **auth:** case-sensitive logins not updating authtokens ([#778](https://github.com/fallenbagel/jellyseerr/issues/778)) ([2bd125d](https://github.com/fallenbagel/jellyseerr/commit/2bd125d9a55d15a398ceb5f2996105a5e861b6e0))
|
||||
* **jellyfinapi:** use external api class for jellyfin api requests ([#762](https://github.com/fallenbagel/jellyseerr/issues/762)) ([650c339](https://github.com/fallenbagel/jellyseerr/commit/650c339d74d4fe85ef7f76184901e86f4eeada85)), closes [#728](https://github.com/fallenbagel/jellyseerr/issues/728) [#387](https://github.com/fallenbagel/jellyseerr/issues/387)
|
||||
* **logging:** handle media server connection refused error/toast ([#748](https://github.com/fallenbagel/jellyseerr/issues/748)) ([f486fb5](https://github.com/fallenbagel/jellyseerr/commit/f486fb5e75f9ea21456952b6a52cb841e30f3556))
|
||||
* use UTF8 encoding for webhook JSON ([#714](https://github.com/fallenbagel/jellyseerr/issues/714)) ([c0a0b9c](https://github.com/fallenbagel/jellyseerr/commit/c0a0b9c8a8b0c2eeaf3fa9159f10742baa9f6c1f))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Latin American Spanish translation ([#725](https://github.com/fallenbagel/jellyseerr/issues/725)) ([783fda9](https://github.com/fallenbagel/jellyseerr/commit/783fda9621aef8ffd46e5f036136de82ed502ccc)), closes [#677](https://github.com/fallenbagel/jellyseerr/issues/677)
|
||||
* add merge conflict labeler workflow ([#719](https://github.com/fallenbagel/jellyseerr/issues/719)) ([d9d07c7](https://github.com/fallenbagel/jellyseerr/commit/d9d07c705a24d5c49905066aac45a3c6a2e36a53))
|
||||
* **auth:** send real information on login ([#470](https://github.com/fallenbagel/jellyseerr/issues/470)) ([d765055](https://github.com/fallenbagel/jellyseerr/commit/d765055da83ee94546399f6348aee14d8427d462))
|
||||
* **settings:** stores jellyfin/emby server name in the settings ([#763](https://github.com/fallenbagel/jellyseerr/issues/763)) ([7a5e8d6](https://github.com/fallenbagel/jellyseerr/commit/7a5e8d69bf620c8e7bf5f284840b1a5fe757ae5f))
|
||||
|
||||
## [1.8.1](https://github.com/fallenbagel/jellyseerr/compare/v1.8.0...v1.8.1) (2024-04-17)
|
||||
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -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="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 -->
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-34-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 -->
|
||||
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
||||
@@ -229,6 +229,14 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</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://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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -2092,6 +2092,13 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
userId:
|
||||
type: integer
|
||||
/settings/jellyfin/sync:
|
||||
get:
|
||||
summary: Get status of full Jellyfin library sync
|
||||
@@ -3395,6 +3402,12 @@ paths:
|
||||
Updates a single slider and return the newly updated slider. Requires the `ADMIN` permission.
|
||||
tags:
|
||||
- settings
|
||||
parameters:
|
||||
- in: path
|
||||
name: sliderId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -3724,7 +3737,7 @@ paths:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: '#/components/schemas/User'
|
||||
post:
|
||||
summary: Create new user
|
||||
description: |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellyseerr",
|
||||
"version": "1.8.1",
|
||||
"version": "1.9.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
|
||||
@@ -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",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import logger from '@server/logger';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
|
||||
export interface JellyfinUserResponse {
|
||||
Name: string;
|
||||
@@ -90,48 +92,84 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
||||
DateCreated?: string;
|
||||
}
|
||||
|
||||
class JellyfinAPI {
|
||||
class JellyfinAPI extends ExternalAPI {
|
||||
private authToken?: string;
|
||||
private userId?: string;
|
||||
private jellyfinHost: string;
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
||||
this.jellyfinHost = jellyfinHost;
|
||||
this.authToken = authToken;
|
||||
|
||||
let authHeaderVal = '';
|
||||
if (this.authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||
let authHeaderVal: string;
|
||||
if (authToken) {
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
|
||||
} else {
|
||||
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}"`;
|
||||
}
|
||||
|
||||
this.axios = axios.create({
|
||||
baseURL: this.jellyfinHost,
|
||||
headers: {
|
||||
'X-Emby-Authorization': authHeaderVal,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
super(
|
||||
jellyfinHost,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'X-Emby-Authorization': authHeaderVal,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.jellyfinHost = jellyfinHost;
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
public async login(
|
||||
Username?: string,
|
||||
Password?: string
|
||||
Password?: string,
|
||||
ClientIP?: string
|
||||
): Promise<JellyfinLoginResponse> {
|
||||
try {
|
||||
const account = await this.axios.post<JellyfinLoginResponse>(
|
||||
const headers = ClientIP
|
||||
? {
|
||||
'X-Forwarded-For': ClientIP,
|
||||
}
|
||||
: {};
|
||||
|
||||
const authResponse = await this.post<JellyfinLoginResponse>(
|
||||
'/Users/AuthenticateByName',
|
||||
{
|
||||
Username: Username,
|
||||
Pw: Password,
|
||||
},
|
||||
{
|
||||
headers: headers,
|
||||
}
|
||||
);
|
||||
return account.data;
|
||||
|
||||
return authResponse;
|
||||
} catch (e) {
|
||||
throw new Error('Unauthorized');
|
||||
const status = e.response?.status;
|
||||
|
||||
const networkErrorCodes = new Set([
|
||||
'ECONNREFUSED',
|
||||
'EHOSTUNREACH',
|
||||
'ENOTFOUND',
|
||||
'ETIMEDOUT',
|
||||
'ECONNRESET',
|
||||
'EADDRINUSE',
|
||||
'ENETDOWN',
|
||||
'ENETUNREACH',
|
||||
'EPIPE',
|
||||
'ECONNABORTED',
|
||||
'EPROTO',
|
||||
'EHOSTDOWN',
|
||||
'EAI_AGAIN',
|
||||
'ERR_INVALID_URL',
|
||||
]);
|
||||
|
||||
if (networkErrorCodes.has(e.code) || status === 404) {
|
||||
throw new ApiError(status, ApiErrorCode.InvalidUrl);
|
||||
}
|
||||
|
||||
throw new ApiError(status, ApiErrorCode.InvalidCredentials);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,66 +180,72 @@ class JellyfinAPI {
|
||||
|
||||
public async getServerName(): Promise<string> {
|
||||
try {
|
||||
const account = await this.axios.get<JellyfinUserResponse>(
|
||||
"/System/Info/Public'}"
|
||||
const serverResponse = await this.get<JellyfinUserResponse>(
|
||||
'/System/Info/Public'
|
||||
);
|
||||
return account.data.ServerName;
|
||||
|
||||
return serverResponse.ServerName;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the server name from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('girl idk');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUsers(): Promise<JellyfinUserListResponse> {
|
||||
try {
|
||||
const account = await this.axios.get(`/Users`);
|
||||
return { users: account.data };
|
||||
const userReponse = await this.get<JellyfinUserResponse[]>(`/Users`);
|
||||
|
||||
return { users: userReponse };
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUser(): Promise<JellyfinUserResponse> {
|
||||
try {
|
||||
const account = await this.axios.get<JellyfinUserResponse>(
|
||||
const userReponse = await this.get<JellyfinUserResponse>(
|
||||
`/Users/${this.userId ?? 'Me'}`
|
||||
);
|
||||
return account.data;
|
||||
return userReponse;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getLibraries(): Promise<JellyfinLibrary[]> {
|
||||
try {
|
||||
const mediaFolders = await this.axios.get<any>(`/Library/MediaFolders`);
|
||||
const mediaFolderResponse = await this.get<any>(`/Library/MediaFolders`);
|
||||
|
||||
return this.mapLibraries(mediaFolders.data.Items);
|
||||
} catch (mediaFoldersError) {
|
||||
return this.mapLibraries(mediaFolderResponse.Items);
|
||||
} catch (mediaFoldersResponseError) {
|
||||
// fallback to user views to get libraries
|
||||
// this only affects LDAP users
|
||||
// this only and maybe/depending on factors affects LDAP users
|
||||
try {
|
||||
const mediaFolders = await this.axios.get<any>(
|
||||
const mediaFolderResponse = await this.get<any>(
|
||||
`/Users/${this.userId ?? 'Me'}/Views`
|
||||
);
|
||||
|
||||
return this.mapLibraries(mediaFolders.data.Items);
|
||||
return this.mapLibraries(mediaFolderResponse.Items);
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -235,11 +279,11 @@ class JellyfinAPI {
|
||||
|
||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
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`
|
||||
);
|
||||
|
||||
return contents.data.Items.filter(
|
||||
return libraryItemsResponse.Items.filter(
|
||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -247,23 +291,25 @@ class JellyfinAPI {
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
const itemResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}`
|
||||
);
|
||||
|
||||
return contents.data;
|
||||
return itemResponse;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,36 +317,38 @@ class JellyfinAPI {
|
||||
id: string
|
||||
): Promise<JellyfinLibraryItemExtended | undefined> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
const itemResponse = await this.get<any>(
|
||||
`/Users/${this.userId}/Items/${id}`
|
||||
);
|
||||
|
||||
return contents.data;
|
||||
return itemResponse;
|
||||
} catch (e) {
|
||||
if (availabilitySync.running) {
|
||||
if (e.response && e.response.status === 500) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(
|
||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async getSeasons(seriesID: string): Promise<JellyfinLibraryItem[]> {
|
||||
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) {
|
||||
logger.error(
|
||||
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,11 +357,11 @@ class JellyfinAPI {
|
||||
seasonID: string
|
||||
): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
const episodeResponse = await this.get<any>(
|
||||
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
||||
);
|
||||
|
||||
return contents.data.Items.filter(
|
||||
return episodeResponse.Items.filter(
|
||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -321,7 +369,8 @@ class JellyfinAPI {
|
||||
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,
|
||||
{ label: 'Jellyfin API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
server/constants/error.ts
Normal file
7
server/constants/error.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum ApiErrorCode {
|
||||
InvalidUrl = 'INVALID_URL',
|
||||
InvalidCredentials = 'INVALID_CREDENTIALS',
|
||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
||||
NotAdmin = 'NOT_ADMIN',
|
||||
Unknown = 'UNKNOWN',
|
||||
}
|
||||
@@ -23,6 +23,7 @@ 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';
|
||||
@@ -32,10 +33,14 @@ 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 +51,12 @@ const handle = app.getRequestHandler();
|
||||
app
|
||||
.prepare()
|
||||
.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();
|
||||
|
||||
// Run migrations in production
|
||||
|
||||
@@ -141,7 +141,7 @@ class WebhookAgent
|
||||
const payloadString = Buffer.from(
|
||||
this.getSettings().options.jsonPayload,
|
||||
'base64'
|
||||
).toString('ascii');
|
||||
).toString('utf8');
|
||||
|
||||
const parsedJSON = JSON.parse(JSON.parse(payloadString));
|
||||
|
||||
|
||||
@@ -83,13 +83,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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { UserType } from '@server/constants/user';
|
||||
import { getRepository } from '@server/datasource';
|
||||
@@ -9,6 +10,7 @@ import { Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
@@ -269,7 +271,13 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
|
||||
const account = await jellyfinserver.login(body.username, body.password);
|
||||
const ip = req.ip ? req.ip.split(':').reverse()[0] : undefined;
|
||||
const account = await jellyfinserver.login(
|
||||
body.username,
|
||||
body.password,
|
||||
ip
|
||||
);
|
||||
|
||||
// Next let's see if the user already exists
|
||||
user = await userRepository.findOne({
|
||||
where: { jellyfinUserId: account.User.Id },
|
||||
@@ -278,7 +286,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
if (!user && !(await userRepository.count())) {
|
||||
// Check if user is admin on jellyfin
|
||||
if (account.User.Policy.IsAdministrator === false) {
|
||||
throw new Error('not_admin');
|
||||
throw new ApiError(403, ApiErrorCode.NotAdmin);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -306,6 +314,9 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
|
||||
const serverName = await jellyfinserver.getServerName();
|
||||
|
||||
settings.jellyfin.name = serverName;
|
||||
settings.jellyfin.hostname = body.hostname ?? '';
|
||||
settings.jellyfin.serverId = account.User.ServerId;
|
||||
settings.save();
|
||||
@@ -314,7 +325,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
await userRepository.save(user);
|
||||
}
|
||||
// User already exists, let's update their information
|
||||
else if (body.username === user?.jellyfinUsername) {
|
||||
else if (account.User.Id === user?.jellyfinUserId) {
|
||||
logger.info(
|
||||
`Found matching ${
|
||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||
@@ -412,43 +423,63 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
|
||||
return res.status(200).json(user?.filter() ?? {});
|
||||
} catch (e) {
|
||||
if (e.message === 'Unauthorized') {
|
||||
logger.warn(
|
||||
'Failed login attempt from user with incorrect Jellyfin credentials',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.username,
|
||||
password: '__REDACTED__',
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
} else if (e.message === 'not_admin') {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'CREDENTIAL_ERROR_NOT_ADMIN',
|
||||
});
|
||||
} else if (e.message === 'add_email') {
|
||||
return next({
|
||||
status: 406,
|
||||
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
|
||||
});
|
||||
} else if (e.message === 'select_server_type') {
|
||||
return next({
|
||||
status: 406,
|
||||
message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE',
|
||||
});
|
||||
} else {
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong.',
|
||||
});
|
||||
switch (e.errorCode) {
|
||||
case ApiErrorCode.InvalidUrl:
|
||||
logger.error(
|
||||
`The provided ${
|
||||
process.env.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin'
|
||||
} is invalid or the server is not reachable.`,
|
||||
{
|
||||
label: 'Auth',
|
||||
error: e.errorCode,
|
||||
status: e.statusCode,
|
||||
hostname: body.hostname,
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: e.errorCode,
|
||||
});
|
||||
|
||||
case ApiErrorCode.InvalidCredentials:
|
||||
logger.warn(
|
||||
'Failed login attempt from user with incorrect Jellyfin credentials',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.username,
|
||||
password: '__REDACTED__',
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: e.errorCode,
|
||||
});
|
||||
|
||||
case ApiErrorCode.NotAdmin:
|
||||
logger.warn(
|
||||
'Failed login attempt from user without admin permissions',
|
||||
{
|
||||
label: 'Auth',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.username,
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: e.errorCode,
|
||||
});
|
||||
|
||||
default:
|
||||
logger.error(e.message, { label: 'Auth' });
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong.',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -275,7 +275,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
||||
...webhookSettings.options,
|
||||
jsonPayload: JSON.parse(
|
||||
Buffer.from(webhookSettings.options.jsonPayload, 'base64').toString(
|
||||
'ascii'
|
||||
'utf8'
|
||||
)
|
||||
),
|
||||
},
|
||||
|
||||
@@ -98,6 +98,7 @@ userSettingsRoutes.post<
|
||||
}
|
||||
|
||||
user.username = req.body.username;
|
||||
user.email = req.body.email ?? user.email;
|
||||
|
||||
// Update quota values only if the user has the correct permissions
|
||||
if (
|
||||
@@ -127,20 +128,19 @@ userSettingsRoutes.post<
|
||||
user.settings.originalLanguage = req.body.originalLanguage;
|
||||
user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies;
|
||||
user.settings.watchlistSyncTv = req.body.watchlistSyncTv;
|
||||
user.email = req.body.email ?? user.email;
|
||||
}
|
||||
|
||||
await userRepository.save(user);
|
||||
const savedUser = await userRepository.save(user);
|
||||
|
||||
return res.status(200).json({
|
||||
username: user.username,
|
||||
discordId: user.settings.discordId,
|
||||
locale: user.settings.locale,
|
||||
region: user.settings.region,
|
||||
originalLanguage: user.settings.originalLanguage,
|
||||
watchlistSyncMovies: user.settings.watchlistSyncMovies,
|
||||
watchlistSyncTv: user.settings.watchlistSyncTv,
|
||||
email: user.email,
|
||||
username: savedUser.username,
|
||||
discordId: savedUser.settings?.discordId,
|
||||
locale: savedUser.settings?.locale,
|
||||
region: savedUser.settings?.region,
|
||||
originalLanguage: savedUser.settings?.originalLanguage,
|
||||
watchlistSyncMovies: savedUser.settings?.watchlistSyncMovies,
|
||||
watchlistSyncTv: savedUser.settings?.watchlistSyncTv,
|
||||
email: savedUser.email,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
|
||||
9
server/types/error.ts
Normal file
9
server/types/error.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { ApiErrorCode } from '@server/constants/error';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public statusCode: number, public errorCode: ApiErrorCode) {
|
||||
super();
|
||||
|
||||
this.name = 'apiError';
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import axios from 'axios';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import getConfig from 'next/config';
|
||||
@@ -26,6 +27,7 @@ const messages = defineMessages({
|
||||
loginerror: 'Something went wrong while trying to sign in.',
|
||||
adminerror: 'You must use an admin account to sign in.',
|
||||
credentialerror: 'The username or password is incorrect.',
|
||||
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
||||
signingin: 'Signing in…',
|
||||
signin: 'Sign In',
|
||||
initialsigningin: 'Connecting…',
|
||||
@@ -91,14 +93,24 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
email: values.email,
|
||||
});
|
||||
} catch (e) {
|
||||
let errorMessage = null;
|
||||
switch (e.response?.data?.message) {
|
||||
case ApiErrorCode.InvalidUrl:
|
||||
errorMessage = messages.invalidurlerror;
|
||||
break;
|
||||
case ApiErrorCode.InvalidCredentials:
|
||||
errorMessage = messages.credentialerror;
|
||||
break;
|
||||
case ApiErrorCode.NotAdmin:
|
||||
errorMessage = messages.adminerror;
|
||||
break;
|
||||
default:
|
||||
errorMessage = messages.loginerror;
|
||||
break;
|
||||
}
|
||||
|
||||
toasts.addToast(
|
||||
intl.formatMessage(
|
||||
e.message == 'Request failed with status code 401'
|
||||
? messages.credentialerror
|
||||
: e.message == 'Request failed with status code 403'
|
||||
? messages.adminerror
|
||||
: messages.loginerror
|
||||
),
|
||||
intl.formatMessage(errorMessage, mediaServerFormatValues),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
|
||||
@@ -434,33 +434,38 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) && data.mediaInfo && (
|
||||
<Tooltip content={intl.formatMessage(messages.managemovie)}>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() => setShowManager(true)}
|
||||
className="relative ml-2 first:ml-0"
|
||||
>
|
||||
<CogIcon className="!mr-0" />
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
) &&
|
||||
(
|
||||
data.mediaInfo?.issues.filter(
|
||||
(issue) => issue.status === IssueStatus.OPEN
|
||||
) ?? []
|
||||
).length > 0 && (
|
||||
<>
|
||||
<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>
|
||||
)}
|
||||
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
||||
data.mediaInfo &&
|
||||
(data.mediaInfo.jellyfinMediaId ||
|
||||
data.mediaInfo.jellyfinMediaId4k ||
|
||||
data.mediaInfo.status !== MediaStatus.UNKNOWN ||
|
||||
data.mediaInfo.status4k !== MediaStatus.UNKNOWN) && (
|
||||
<Tooltip content={intl.formatMessage(messages.managemovie)}>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
onClick={() => setShowManager(true)}
|
||||
className="relative ml-2 first:ml-0"
|
||||
>
|
||||
<CogIcon className="!mr-0" />
|
||||
{hasPermission(
|
||||
[Permission.MANAGE_ISSUES, Permission.VIEW_ISSUES],
|
||||
{
|
||||
type: 'or',
|
||||
}
|
||||
) &&
|
||||
(
|
||||
data.mediaInfo?.issues.filter(
|
||||
(issue) => issue.status === IssueStatus.OPEN
|
||||
) ?? []
|
||||
).length > 0 && (
|
||||
<>
|
||||
<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 className="media-overview">
|
||||
|
||||
@@ -53,6 +53,8 @@ const messages = defineMessages({
|
||||
discordId: 'Discord User ID',
|
||||
discordIdTip:
|
||||
'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',
|
||||
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)),
|
||||
|
||||
@@ -9,6 +9,7 @@ export type AvailableLocale =
|
||||
| 'en'
|
||||
| 'el'
|
||||
| 'es'
|
||||
| 'es-MX'
|
||||
| 'fr'
|
||||
| 'hr'
|
||||
| 'hu'
|
||||
@@ -59,6 +60,10 @@ export const availableLanguages: AvailableLanguageObject = {
|
||||
code: 'es',
|
||||
display: 'Español',
|
||||
},
|
||||
'es-MX': {
|
||||
code: 'es-MX',
|
||||
display: 'Español (Latinoamérica)',
|
||||
},
|
||||
fr: {
|
||||
code: 'fr',
|
||||
display: 'Français',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -219,8 +219,9 @@
|
||||
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
||||
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
||||
"components.Login.credentialerror": "The username or password is incorrect.",
|
||||
"components.Login.adminerror": "You must use an admin account to sign in.",
|
||||
"components.Login.invalidurlerror": "Unable to connect to {mediaServerName} server.",
|
||||
"components.Login.credentialerror": "The username or password is incorrect.",
|
||||
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
|
||||
"components.Login.email": "Email Address",
|
||||
"components.Login.emailtooltip": "Address does not need to be associated with your {mediaServerName} instance.",
|
||||
@@ -752,8 +753,8 @@
|
||||
"components.Settings.SettingsAbout.overseerrinformation": "About Jellyseerr",
|
||||
"components.Settings.SettingsAbout.preferredmethod": "Preferred",
|
||||
"components.Settings.SettingsAbout.runningDevelop": "You are running the <code>develop</code> branch of Jellyseerr, which is only recommended for those contributing to development or assisting with bleeding-edge testing.",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
||||
"components.Settings.SettingsAbout.supportjellyseerr": "Support Jellyseerr",
|
||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
||||
"components.Settings.SettingsAbout.timezone": "Time Zone",
|
||||
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
||||
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
||||
@@ -938,17 +939,18 @@
|
||||
"components.Settings.hostname": "Hostname or IP Address",
|
||||
"components.Settings.internalUrl": "Internal URL",
|
||||
"components.Settings.is4k": "4K",
|
||||
"components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL",
|
||||
"components.Settings.jellyfinSettings": "{mediaServerName} Settings",
|
||||
"components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.",
|
||||
"components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
|
||||
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
|
||||
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",
|
||||
"components.Settings.jellyfinSyncFailedGenericError": "Something went wrong while syncing libraries",
|
||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "No libraries were found",
|
||||
"components.Settings.jellyfinlibraries": "{mediaServerName} Libraries",
|
||||
"components.Settings.jellyfinlibrariesDescription": "The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.",
|
||||
"components.Settings.jellyfinsettings": "{mediaServerName} Settings",
|
||||
"components.Settings.jellyfinsettingsDescription": "Configure the settings for your {mediaServerName} server. {mediaServerName} scans your {mediaServerName} libraries to see what content is available.",
|
||||
"components.Settings.jellyfinSyncFailedNoLibrariesFound": "No libraries were found",
|
||||
"components.Settings.jellyfinSyncFailedAutomaticGroupedFolders": "Custom authentication with Automatic Library Grouping not supported",
|
||||
"components.Settings.jellyfinSyncFailedGenericError": "Something went wrong while syncing libraries",
|
||||
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
|
||||
"components.Settings.manualscan": "Manual Library Scan",
|
||||
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Jellyseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
|
||||
@@ -1175,6 +1177,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 <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your user account",
|
||||
|
||||
1205
src/i18n/locale/es_MX.json
Normal file
1205
src/i18n/locale/es_MX.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,8 @@ const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
|
||||
return import('../i18n/locale/el.json');
|
||||
case 'es':
|
||||
return import('../i18n/locale/es.json');
|
||||
case 'es-MX':
|
||||
return import('../i18n/locale/es_MX.json');
|
||||
case 'fr':
|
||||
return import('../i18n/locale/fr.json');
|
||||
case 'hr':
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user