Compare commits
1 Commits
preview-dn
...
feat-makeo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e8000527e |
@@ -322,60 +322,6 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"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 @@
|
|||||||
buy_me_a_coffee: fallen.bagel
|
github: [Fallenbagel]
|
||||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
container: node:18.18-alpine
|
container: node:18.18-alpine
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
@@ -34,18 +34,18 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
OWNER: ${{ github.repository_owner }}
|
OWNER: ${{ github.repository_owner }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v2
|
||||||
|
|||||||
26
.github/workflows/conflict_labeler.yml
vendored
26
.github/workflows/conflict_labeler.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
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 }}'
|
|
||||||
4
.github/workflows/cypress.yml
vendored
4
.github/workflows/cypress.yml
vendored
@@ -13,9 +13,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Cypress run
|
- name: Cypress run
|
||||||
uses: cypress-io/github-action@v6
|
uses: cypress-io/github-action@v4
|
||||||
with:
|
with:
|
||||||
build: yarn cypress:build
|
build: yarn cypress:build
|
||||||
start: yarn start
|
start: yarn start
|
||||||
|
|||||||
10
.github/workflows/preview.yml
vendored
10
.github/workflows/preview.yml
vendored
@@ -11,21 +11,21 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Get the version
|
- name: Get the version
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -10,19 +10,19 @@ jobs:
|
|||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
- armhf
|
- armhf
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Switch to main branch
|
- name: Switch to main branch
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
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@v1
|
||||||
with:
|
with:
|
||||||
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
|
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
|
||||||
- name: Build Snap Package
|
- name: Build Snap Package
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
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@v2
|
||||||
with:
|
with:
|
||||||
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||||
path: ${{ steps.build.outputs.snap }}
|
path: ${{ steps.build.outputs.snap }}
|
||||||
|
|||||||
20
.github/workflows/snap.yaml
vendored
20
.github/workflows/snap.yaml
vendored
@@ -1,13 +1,9 @@
|
|||||||
name: Publish Snap
|
name: Publish Snap
|
||||||
|
|
||||||
# turn off edge snap builds temporarily and make it manual
|
on:
|
||||||
|
push:
|
||||||
# on:
|
branches:
|
||||||
# push:
|
- develop
|
||||||
# branches:
|
|
||||||
# - develop
|
|
||||||
|
|
||||||
on: workflow_dispatch
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
jobs:
|
jobs:
|
||||||
@@ -16,7 +12,7 @@ jobs:
|
|||||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel Previous Runs
|
- name: Cancel Previous Runs
|
||||||
uses: styfle/cancel-workflow-action@0.12.1
|
uses: styfle/cancel-workflow-action@0.10.0
|
||||||
with:
|
with:
|
||||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -33,7 +29,7 @@ jobs:
|
|||||||
- armhf
|
- armhf
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
id: prepare
|
id: prepare
|
||||||
run: |
|
run: |
|
||||||
@@ -44,7 +40,7 @@ jobs:
|
|||||||
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@v2
|
||||||
- name: Configure Git
|
- name: Configure Git
|
||||||
run: git config --add safe.directory /data/parts/jellyseerr/src
|
run: git config --add safe.directory /data/parts/jellyseerr/src
|
||||||
- name: Build Snap Package
|
- name: Build Snap Package
|
||||||
@@ -53,7 +49,7 @@ jobs:
|
|||||||
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@v3
|
||||||
with:
|
with:
|
||||||
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||||
path: ${{ steps.build.outputs.snap }}
|
path: ${{ steps.build.outputs.snap }}
|
||||||
|
|||||||
4
.github/workflows/support.yml
vendored
4
.github/workflows/support.yml
vendored
@@ -6,9 +6,9 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
support:
|
support:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/support-requests@v4
|
- uses: dessant/support-requests@v2
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
support-label: 'support'
|
support-label: 'support'
|
||||||
|
|||||||
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="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-40-orange.svg"/></a>
|
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-34-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.
|
||||||
@@ -229,14 +229,6 @@ 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="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="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://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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -2092,13 +2092,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
userId:
|
|
||||||
type: integer
|
|
||||||
/settings/jellyfin/sync:
|
/settings/jellyfin/sync:
|
||||||
get:
|
get:
|
||||||
summary: Get status of full Jellyfin library sync
|
summary: Get status of full Jellyfin library sync
|
||||||
@@ -3402,12 +3395,6 @@ paths:
|
|||||||
Updates a single slider and return the newly updated slider. Requires the `ADMIN` permission.
|
Updates a single slider and return the newly updated slider. Requires the `ADMIN` permission.
|
||||||
tags:
|
tags:
|
||||||
- settings
|
- settings
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: sliderId
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: number
|
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -3737,7 +3724,7 @@ paths:
|
|||||||
results:
|
results:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/User'
|
$ref: '#/components/schemas/User'
|
||||||
post:
|
post:
|
||||||
summary: Create new user
|
summary: Create new user
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
/* 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 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 type { AxiosInstance } from 'axios';
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
import axios from 'axios';
|
||||||
|
|
||||||
export interface JellyfinUserResponse {
|
export interface JellyfinUserResponse {
|
||||||
Name: string;
|
Name: string;
|
||||||
ServerId: string;
|
ServerId: string;
|
||||||
ServerName: string;
|
ServerName: string;
|
||||||
Id: string;
|
Id: string;
|
||||||
Configuration: {
|
|
||||||
GroupedFolders: string[];
|
|
||||||
};
|
|
||||||
Policy: {
|
Policy: {
|
||||||
IsAdministrator: boolean;
|
IsAdministrator: boolean;
|
||||||
};
|
};
|
||||||
@@ -29,13 +24,6 @@ export interface JellyfinUserListResponse {
|
|||||||
users: JellyfinUserResponse[];
|
users: JellyfinUserResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JellyfinMediaFolder {
|
|
||||||
Name: string;
|
|
||||||
Id: string;
|
|
||||||
Type: string;
|
|
||||||
CollectionType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JellyfinLibrary {
|
export interface JellyfinLibrary {
|
||||||
type: 'show' | 'movie';
|
type: 'show' | 'movie';
|
||||||
key: string;
|
key: string;
|
||||||
@@ -92,84 +80,48 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
|||||||
DateCreated?: string;
|
DateCreated?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class JellyfinAPI extends ExternalAPI {
|
class JellyfinAPI {
|
||||||
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) {
|
||||||
let authHeaderVal: string;
|
|
||||||
if (authToken) {
|
|
||||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
|
|
||||||
} else {
|
|
||||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
super(
|
|
||||||
jellyfinHost,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'X-Emby-Authorization': authHeaderVal,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.jellyfinHost = jellyfinHost;
|
this.jellyfinHost = jellyfinHost;
|
||||||
this.authToken = authToken;
|
this.authToken = authToken;
|
||||||
|
|
||||||
|
let authHeaderVal = '';
|
||||||
|
if (this.authToken) {
|
||||||
|
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`;
|
||||||
|
} else {
|
||||||
|
authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.axios = axios.create({
|
||||||
|
baseURL: this.jellyfinHost,
|
||||||
|
headers: {
|
||||||
|
'X-Emby-Authorization': authHeaderVal,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(
|
public async login(
|
||||||
Username?: string,
|
Username?: string,
|
||||||
Password?: string,
|
Password?: string
|
||||||
ClientIP?: string
|
|
||||||
): Promise<JellyfinLoginResponse> {
|
): Promise<JellyfinLoginResponse> {
|
||||||
try {
|
try {
|
||||||
const headers = ClientIP
|
const account = await this.axios.post<JellyfinLoginResponse>(
|
||||||
? {
|
|
||||||
'X-Forwarded-For': ClientIP,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const authResponse = await this.post<JellyfinLoginResponse>(
|
|
||||||
'/Users/AuthenticateByName',
|
'/Users/AuthenticateByName',
|
||||||
{
|
{
|
||||||
Username: Username,
|
Username: Username,
|
||||||
Pw: Password,
|
Pw: Password,
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: headers,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
return account.data;
|
||||||
return authResponse;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const status = e.response?.status;
|
throw new Error('Unauthorized');
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,94 +132,67 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
|
|
||||||
public async getServerName(): Promise<string> {
|
public async getServerName(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const serverResponse = await this.get<JellyfinUserResponse>(
|
const account = await this.axios.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 userReponse = await this.get<JellyfinUserResponse[]>(`/Users`);
|
const account = await this.axios.get(`/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 userReponse = await this.get<JellyfinUserResponse>(
|
const account = await this.axios.get<JellyfinUserResponse>(
|
||||||
`/Users/${this.userId ?? 'Me'}`
|
`/Users/${this.userId ?? 'Me'}`
|
||||||
);
|
);
|
||||||
return userReponse;
|
return account.data;
|
||||||
} 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 mediaFolderResponse = await this.get<any>(`/Library/MediaFolders`);
|
// TODO: Try to fix automatic grouping without fucking up LDAP users
|
||||||
|
// const libraries = await this.axios.get<any>('/Library/VirtualFolders');
|
||||||
|
|
||||||
return this.mapLibraries(mediaFolderResponse.Items);
|
const account = await this.axios.get<any>(
|
||||||
} catch (mediaFoldersResponseError) {
|
`/Users/${this.userId ?? 'Me'}/Views`
|
||||||
// fallback to user views to get libraries
|
);
|
||||||
// this only and maybe/depending on factors affects LDAP users
|
|
||||||
try {
|
|
||||||
const mediaFolderResponse = await this.get<any>(
|
|
||||||
`/Users/${this.userId ?? 'Me'}/Views`
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.mapLibraries(mediaFolderResponse.Items);
|
const response: JellyfinLibrary[] = account.data.Items.filter(
|
||||||
} catch (e) {
|
(Item: any) => {
|
||||||
logger.error(
|
return (
|
||||||
`Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
|
Item.Type === 'CollectionFolder' &&
|
||||||
{ label: 'Jellyfin API' }
|
Item.CollectionType !== 'music' &&
|
||||||
);
|
Item.CollectionType !== 'books' &&
|
||||||
|
Item.CollectionType !== 'musicvideos' &&
|
||||||
return [];
|
Item.CollectionType !== 'homevideos'
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
).map((Item: any) => {
|
||||||
|
|
||||||
private mapLibraries(mediaFolders: JellyfinMediaFolder[]): JellyfinLibrary[] {
|
|
||||||
const excludedTypes = [
|
|
||||||
'music',
|
|
||||||
'books',
|
|
||||||
'musicvideos',
|
|
||||||
'homevideos',
|
|
||||||
'boxsets',
|
|
||||||
];
|
|
||||||
|
|
||||||
return mediaFolders
|
|
||||||
.filter((Item: JellyfinMediaFolder) => {
|
|
||||||
return (
|
|
||||||
Item.Type === 'CollectionFolder' &&
|
|
||||||
!excludedTypes.includes(Item.CollectionType)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((Item: JellyfinMediaFolder) => {
|
|
||||||
return <JellyfinLibrary>{
|
return <JellyfinLibrary>{
|
||||||
key: Item.Id,
|
key: Item.Id,
|
||||||
title: Item.Name,
|
title: Item.Name,
|
||||||
@@ -275,15 +200,24 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
agent: 'jellyfin',
|
agent: 'jellyfin',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
`Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
|
||||||
|
{ label: 'Jellyfin API' }
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const libraryItemsResponse = await this.get<any>(
|
const contents = await this.axios.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}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return libraryItemsResponse.Items.filter(
|
return contents.data.Items.filter(
|
||||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -291,25 +225,23 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
`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 itemResponse = await this.get<any>(
|
const contents = await this.axios.get<any>(
|
||||||
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}`
|
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return itemResponse;
|
return contents.data;
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,38 +249,36 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
id: string
|
id: string
|
||||||
): Promise<JellyfinLibraryItemExtended | undefined> {
|
): Promise<JellyfinLibraryItemExtended | undefined> {
|
||||||
try {
|
try {
|
||||||
const itemResponse = await this.get<any>(
|
const contents = await this.axios.get<any>(
|
||||||
`/Users/${this.userId}/Items/${id}`
|
`/Users/${this.userId}/Items/${id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return itemResponse;
|
return contents.data;
|
||||||
} 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 ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new Error('Invalid auth token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSeasons(seriesID: string): Promise<JellyfinLibraryItem[]> {
|
public async getSeasons(seriesID: string): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const seasonResponse = await this.get<any>(`/Shows/${seriesID}/Seasons`);
|
const contents = await this.axios.get<any>(`/Shows/${seriesID}/Seasons`);
|
||||||
|
|
||||||
return seasonResponse.Items;
|
return contents.data.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,11 +287,11 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
seasonID: string
|
seasonID: string
|
||||||
): Promise<JellyfinLibraryItem[]> {
|
): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const episodeResponse = await this.get<any>(
|
const contents = await this.axios.get<any>(
|
||||||
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return episodeResponse.Items.filter(
|
return contents.data.Items.filter(
|
||||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -369,8 +299,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
`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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export enum ApiErrorCode {
|
|
||||||
InvalidUrl = 'INVALID_URL',
|
|
||||||
InvalidCredentials = 'INVALID_CREDENTIALS',
|
|
||||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
|
||||||
NotAdmin = 'NOT_ADMIN',
|
|
||||||
Unknown = 'UNKNOWN',
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ 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';
|
||||||
@@ -33,14 +32,10 @@ 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()}`);
|
||||||
@@ -51,12 +46,6 @@ const handle = app.getRequestHandler();
|
|||||||
app
|
app
|
||||||
.prepare()
|
.prepare()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
const CacheableLookup = (await _importDynamic('cacheable-lookup'))
|
|
||||||
.default as typeof CacheableLookupType;
|
|
||||||
const cacheable = new CacheableLookup();
|
|
||||||
cacheable.install(http.globalAgent);
|
|
||||||
cacheable.install(https.globalAgent);
|
|
||||||
|
|
||||||
const dbConnection = await dataSource.initialize();
|
const dbConnection = await dataSource.initialize();
|
||||||
|
|
||||||
// Run migrations in production
|
// Run migrations in production
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class WebhookAgent
|
|||||||
const payloadString = Buffer.from(
|
const payloadString = Buffer.from(
|
||||||
this.getSettings().options.jsonPayload,
|
this.getSettings().options.jsonPayload,
|
||||||
'base64'
|
'base64'
|
||||||
).toString('utf8');
|
).toString('ascii');
|
||||||
|
|
||||||
const parsedJSON = JSON.parse(JSON.parse(payloadString));
|
const parsedJSON = JSON.parse(JSON.parse(payloadString));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import JellyfinAPI from '@server/api/jellyfin';
|
import JellyfinAPI from '@server/api/jellyfin';
|
||||||
import PlexTvAPI from '@server/api/plextv';
|
import PlexTvAPI from '@server/api/plextv';
|
||||||
import { ApiErrorCode } from '@server/constants/error';
|
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import { UserType } from '@server/constants/user';
|
import { UserType } from '@server/constants/user';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
@@ -10,7 +9,6 @@ import { Permission } from '@server/lib/permissions';
|
|||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { isAuthenticated } from '@server/middleware/auth';
|
import { isAuthenticated } from '@server/middleware/auth';
|
||||||
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';
|
||||||
@@ -271,13 +269,7 @@ 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 account = await jellyfinserver.login(body.username, body.password);
|
||||||
const account = await jellyfinserver.login(
|
|
||||||
body.username,
|
|
||||||
body.password,
|
|
||||||
ip
|
|
||||||
);
|
|
||||||
|
|
||||||
// Next let's see if the user already exists
|
// Next let's see if the user already exists
|
||||||
user = await userRepository.findOne({
|
user = await userRepository.findOne({
|
||||||
where: { jellyfinUserId: account.User.Id },
|
where: { jellyfinUserId: account.User.Id },
|
||||||
@@ -286,7 +278,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
if (!user && !(await userRepository.count())) {
|
if (!user && !(await userRepository.count())) {
|
||||||
// Check if user is admin on jellyfin
|
// Check if user is admin on jellyfin
|
||||||
if (account.User.Policy.IsAdministrator === false) {
|
if (account.User.Policy.IsAdministrator === false) {
|
||||||
throw new ApiError(403, ApiErrorCode.NotAdmin);
|
throw new Error('not_admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -314,9 +306,6 @@ 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();
|
||||||
@@ -325,7 +314,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 (account.User.Id === user?.jellyfinUserId) {
|
else if (body.username === user?.jellyfinUsername) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Found matching ${
|
`Found matching ${
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
@@ -423,63 +412,43 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
|
|
||||||
return res.status(200).json(user?.filter() ?? {});
|
return res.status(200).json(user?.filter() ?? {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
switch (e.errorCode) {
|
if (e.message === 'Unauthorized') {
|
||||||
case ApiErrorCode.InvalidUrl:
|
logger.warn(
|
||||||
logger.error(
|
'Failed login attempt from user with incorrect Jellyfin credentials',
|
||||||
`The provided ${
|
{
|
||||||
process.env.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin'
|
label: 'Auth',
|
||||||
} is invalid or the server is not reachable.`,
|
account: {
|
||||||
{
|
ip: req.ip,
|
||||||
label: 'Auth',
|
email: body.username,
|
||||||
error: e.errorCode,
|
password: '__REDACTED__',
|
||||||
status: e.statusCode,
|
},
|
||||||
hostname: body.hostname,
|
}
|
||||||
}
|
);
|
||||||
);
|
return next({
|
||||||
return next({
|
status: 401,
|
||||||
status: e.statusCode,
|
message: 'Unauthorized',
|
||||||
message: e.errorCode,
|
});
|
||||||
});
|
} else if (e.message === 'not_admin') {
|
||||||
|
return next({
|
||||||
case ApiErrorCode.InvalidCredentials:
|
status: 403,
|
||||||
logger.warn(
|
message: 'CREDENTIAL_ERROR_NOT_ADMIN',
|
||||||
'Failed login attempt from user with incorrect Jellyfin credentials',
|
});
|
||||||
{
|
} else if (e.message === 'add_email') {
|
||||||
label: 'Auth',
|
return next({
|
||||||
account: {
|
status: 406,
|
||||||
ip: req.ip,
|
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
|
||||||
email: body.username,
|
});
|
||||||
password: '__REDACTED__',
|
} else if (e.message === 'select_server_type') {
|
||||||
},
|
return next({
|
||||||
}
|
status: 406,
|
||||||
);
|
message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE',
|
||||||
return next({
|
});
|
||||||
status: e.statusCode,
|
} else {
|
||||||
message: e.errorCode,
|
logger.error(e.message, { label: 'Auth' });
|
||||||
});
|
return next({
|
||||||
|
status: 500,
|
||||||
case ApiErrorCode.NotAdmin:
|
message: 'Something went wrong.',
|
||||||
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.',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ settingsRoutes.post('/jellyfin', (req, res) => {
|
|||||||
return res.status(200).json(settings.jellyfin);
|
return res.status(200).json(settings.jellyfin);
|
||||||
});
|
});
|
||||||
|
|
||||||
settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
settingsRoutes.get('/jellyfin/library', async (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
if (req.query.sync) {
|
if (req.query.sync) {
|
||||||
@@ -281,19 +281,6 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
|||||||
|
|
||||||
const libraries = await jellyfinClient.getLibraries();
|
const libraries = await jellyfinClient.getLibraries();
|
||||||
|
|
||||||
if (libraries.length === 0) {
|
|
||||||
// Check if no libraries are found due to the fallback to user views
|
|
||||||
// This only affects LDAP users
|
|
||||||
const account = await jellyfinClient.getUser();
|
|
||||||
|
|
||||||
// Automatic Library grouping is not supported when user views are used to get library
|
|
||||||
if (account.Configuration.GroupedFolders.length > 0) {
|
|
||||||
return next({ status: 501, message: 'SYNC_ERROR_GROUPED_FOLDERS' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return next({ status: 404, message: 'SYNC_ERROR_NO_LIBRARIES' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const newLibraries: Library[] = libraries.map((library) => {
|
const newLibraries: Library[] = libraries.map((library) => {
|
||||||
const existing = settings.jellyfin.libraries.find(
|
const existing = settings.jellyfin.libraries.find(
|
||||||
(l) => l.id === library.key && l.name === library.title
|
(l) => l.id === library.key && l.name === library.title
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
|||||||
...webhookSettings.options,
|
...webhookSettings.options,
|
||||||
jsonPayload: JSON.parse(
|
jsonPayload: JSON.parse(
|
||||||
Buffer.from(webhookSettings.options.jsonPayload, 'base64').toString(
|
Buffer.from(webhookSettings.options.jsonPayload, 'base64').toString(
|
||||||
'utf8'
|
'ascii'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ userSettingsRoutes.post<
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.username = req.body.username;
|
user.username = req.body.username;
|
||||||
user.email = req.body.email ?? user.email;
|
|
||||||
|
|
||||||
// Update quota values only if the user has the correct permissions
|
// Update quota values only if the user has the correct permissions
|
||||||
if (
|
if (
|
||||||
@@ -128,19 +127,20 @@ userSettingsRoutes.post<
|
|||||||
user.settings.originalLanguage = req.body.originalLanguage;
|
user.settings.originalLanguage = req.body.originalLanguage;
|
||||||
user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies;
|
user.settings.watchlistSyncMovies = req.body.watchlistSyncMovies;
|
||||||
user.settings.watchlistSyncTv = req.body.watchlistSyncTv;
|
user.settings.watchlistSyncTv = req.body.watchlistSyncTv;
|
||||||
|
user.email = req.body.email ?? user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedUser = await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
username: savedUser.username,
|
username: user.username,
|
||||||
discordId: savedUser.settings?.discordId,
|
discordId: user.settings.discordId,
|
||||||
locale: savedUser.settings?.locale,
|
locale: user.settings.locale,
|
||||||
region: savedUser.settings?.region,
|
region: user.settings.region,
|
||||||
originalLanguage: savedUser.settings?.originalLanguage,
|
originalLanguage: user.settings.originalLanguage,
|
||||||
watchlistSyncMovies: savedUser.settings?.watchlistSyncMovies,
|
watchlistSyncMovies: user.settings.watchlistSyncMovies,
|
||||||
watchlistSyncTv: savedUser.settings?.watchlistSyncTv,
|
watchlistSyncTv: user.settings.watchlistSyncTv,
|
||||||
email: savedUser.email,
|
email: user.email,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { ApiErrorCode } from '@server/constants/error';
|
|
||||||
|
|
||||||
export class ApiError extends Error {
|
|
||||||
constructor(public statusCode: number, public errorCode: ApiErrorCode) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.name = 'apiError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import Button from '@app/components/Common/Button';
|
|||||||
import Tooltip from '@app/components/Common/Tooltip';
|
import Tooltip from '@app/components/Common/Tooltip';
|
||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||||
import { ApiErrorCode } from '@server/constants/error';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
@@ -27,7 +26,6 @@ const messages = defineMessages({
|
|||||||
loginerror: 'Something went wrong while trying to sign in.',
|
loginerror: 'Something went wrong while trying to sign in.',
|
||||||
adminerror: 'You must use an admin account to sign in.',
|
adminerror: 'You must use an admin account to sign in.',
|
||||||
credentialerror: 'The username or password is incorrect.',
|
credentialerror: 'The username or password is incorrect.',
|
||||||
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
|
||||||
signingin: 'Signing in…',
|
signingin: 'Signing in…',
|
||||||
signin: 'Sign In',
|
signin: 'Sign In',
|
||||||
initialsigningin: 'Connecting…',
|
initialsigningin: 'Connecting…',
|
||||||
@@ -93,24 +91,14 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
email: values.email,
|
email: values.email,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} 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(
|
toasts.addToast(
|
||||||
intl.formatMessage(errorMessage, mediaServerFormatValues),
|
intl.formatMessage(
|
||||||
|
e.message == 'Request failed with status code 401'
|
||||||
|
? messages.credentialerror
|
||||||
|
: e.message == 'Request failed with status code 403'
|
||||||
|
? messages.adminerror
|
||||||
|
: messages.loginerror
|
||||||
|
),
|
||||||
{
|
{
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
appearance: 'error',
|
appearance: 'error',
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ const messages = defineMessages({
|
|||||||
externalUrl: 'External URL',
|
externalUrl: 'External URL',
|
||||||
internalUrl: 'Internal URL',
|
internalUrl: 'Internal URL',
|
||||||
jellyfinForgotPasswordUrl: 'Forgot Password URL',
|
jellyfinForgotPasswordUrl: 'Forgot Password URL',
|
||||||
jellyfinSyncFailedNoLibrariesFound: 'No libraries were found',
|
|
||||||
jellyfinSyncFailedAutomaticGroupedFolders:
|
|
||||||
'Custom authentication with Automatic Library Grouping not supported',
|
|
||||||
jellyfinSyncFailedGenericError:
|
|
||||||
'Something went wrong while syncing libraries',
|
|
||||||
validationUrl: 'You must provide a valid URL',
|
validationUrl: 'You must provide a valid URL',
|
||||||
syncing: 'Syncing',
|
syncing: 'Syncing',
|
||||||
syncJellyfin: 'Sync Libraries',
|
syncJellyfin: 'Sync Libraries',
|
||||||
@@ -75,7 +70,6 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
showAdvancedSettings,
|
showAdvancedSettings,
|
||||||
}) => {
|
}) => {
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
const toasts = useToasts();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -123,43 +117,11 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
params.enable = activeLibraries.join(',');
|
params.enable = activeLibraries.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await axios.get('/api/v1/settings/jellyfin/library', {
|
||||||
await axios.get('/api/v1/settings/jellyfin/library', {
|
params,
|
||||||
params,
|
});
|
||||||
});
|
setIsSyncing(false);
|
||||||
setIsSyncing(false);
|
revalidate();
|
||||||
revalidate();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.response.data.message === 'SYNC_ERROR_GROUPED_FOLDERS') {
|
|
||||||
toasts.addToast(
|
|
||||||
intl.formatMessage(
|
|
||||||
messages.jellyfinSyncFailedAutomaticGroupedFolders
|
|
||||||
),
|
|
||||||
{
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'warning',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (e.response.data.message === 'SYNC_ERROR_NO_LIBRARIES') {
|
|
||||||
toasts.addToast(
|
|
||||||
intl.formatMessage(messages.jellyfinSyncFailedNoLibrariesFound),
|
|
||||||
{
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'error',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toasts.addToast(
|
|
||||||
intl.formatMessage(messages.jellyfinSyncFailedGenericError),
|
|
||||||
{
|
|
||||||
autoDismiss: true,
|
|
||||||
appearance: 'error',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setIsSyncing(false);
|
|
||||||
revalidate();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const startScan = async () => {
|
const startScan = async () => {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import Modal from '@app/components/Common/Modal';
|
import Modal from '@app/components/Common/Modal';
|
||||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||||
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
import type { SonarrSettings } from '@server/lib/settings';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import { type SonarrSettings } from '@server/lib/settings';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@@ -109,6 +111,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const settings = useSettings();
|
||||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||||
profiles: [],
|
profiles: [],
|
||||||
rootFolders: [],
|
rootFolders: [],
|
||||||
@@ -255,7 +258,9 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
animeTags: sonarr?.animeTags ?? [],
|
animeTags: sonarr?.animeTags ?? [],
|
||||||
isDefault: sonarr?.isDefault ?? false,
|
isDefault: sonarr?.isDefault ?? false,
|
||||||
is4k: sonarr?.is4k ?? false,
|
is4k: sonarr?.is4k ?? false,
|
||||||
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
|
enableSeasonFolders:
|
||||||
|
sonarr?.enableSeasonFolders ??
|
||||||
|
settings.currentSettings.mediaServerType !== MediaServerType.PLEX,
|
||||||
externalUrl: sonarr?.externalUrl,
|
externalUrl: sonarr?.externalUrl,
|
||||||
syncEnabled: sonarr?.syncEnabled ?? false,
|
syncEnabled: sonarr?.syncEnabled ?? false,
|
||||||
enableSearch: !sonarr?.preventSearch,
|
enableSearch: !sonarr?.preventSearch,
|
||||||
@@ -961,11 +966,24 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
>
|
>
|
||||||
{intl.formatMessage(messages.seasonfolders)}
|
{intl.formatMessage(messages.seasonfolders)}
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div
|
||||||
|
className={`form-input-area ${
|
||||||
|
settings.currentSettings.mediaServerType ===
|
||||||
|
MediaServerType.JELLYFIN ||
|
||||||
|
settings.currentSettings.mediaServerType ===
|
||||||
|
MediaServerType.EMBY
|
||||||
|
? 'opacity-50'
|
||||||
|
: 'opacity-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<Field
|
<Field
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="enableSeasonFolders"
|
id="enableSeasonFolders"
|
||||||
name="enableSeasonFolders"
|
name="enableSeasonFolders"
|
||||||
|
disabled={
|
||||||
|
settings.currentSettings.mediaServerType !==
|
||||||
|
MediaServerType.PLEX
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export type AvailableLocale =
|
|||||||
| 'en'
|
| 'en'
|
||||||
| 'el'
|
| 'el'
|
||||||
| 'es'
|
| 'es'
|
||||||
| 'es-MX'
|
|
||||||
| 'fr'
|
| 'fr'
|
||||||
| 'hr'
|
| 'hr'
|
||||||
| 'hu'
|
| 'hu'
|
||||||
@@ -60,10 +59,6 @@ export const availableLanguages: AvailableLanguageObject = {
|
|||||||
code: 'es',
|
code: 'es',
|
||||||
display: 'Español',
|
display: 'Español',
|
||||||
},
|
},
|
||||||
'es-MX': {
|
|
||||||
code: 'es-MX',
|
|
||||||
display: 'Español (Latinoamérica)',
|
|
||||||
},
|
|
||||||
fr: {
|
fr: {
|
||||||
code: 'fr',
|
code: 'fr',
|
||||||
display: 'Français',
|
display: 'Français',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -219,9 +219,8 @@
|
|||||||
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
||||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
||||||
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
||||||
"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.credentialerror": "The username or password is incorrect.",
|
||||||
|
"components.Login.adminerror": "You must use an admin account to sign in.",
|
||||||
"components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.",
|
"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.email": "Email Address",
|
||||||
"components.Login.emailtooltip": "Address does not need to be associated with your {mediaServerName} instance.",
|
"components.Login.emailtooltip": "Address does not need to be associated with your {mediaServerName} instance.",
|
||||||
@@ -753,8 +752,8 @@
|
|||||||
"components.Settings.SettingsAbout.overseerrinformation": "About Jellyseerr",
|
"components.Settings.SettingsAbout.overseerrinformation": "About Jellyseerr",
|
||||||
"components.Settings.SettingsAbout.preferredmethod": "Preferred",
|
"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.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.supportjellyseerr": "Support Jellyseerr",
|
|
||||||
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
"components.Settings.SettingsAbout.supportoverseerr": "Support Overseerr",
|
||||||
|
"components.Settings.SettingsAbout.supportjellyseerr": "Support Jellyseerr",
|
||||||
"components.Settings.SettingsAbout.timezone": "Time Zone",
|
"components.Settings.SettingsAbout.timezone": "Time Zone",
|
||||||
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
"components.Settings.SettingsAbout.totalmedia": "Total Media",
|
||||||
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
|
||||||
@@ -939,14 +938,10 @@
|
|||||||
"components.Settings.hostname": "Hostname or IP Address",
|
"components.Settings.hostname": "Hostname or IP Address",
|
||||||
"components.Settings.internalUrl": "Internal URL",
|
"components.Settings.internalUrl": "Internal URL",
|
||||||
"components.Settings.is4k": "4K",
|
"components.Settings.is4k": "4K",
|
||||||
"components.Settings.jellyfinForgotPasswordUrl": "Forgot Password URL",
|
|
||||||
"components.Settings.jellyfinSettings": "{mediaServerName} Settings",
|
"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.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.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
|
||||||
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
|
"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.jellyfinlibraries": "{mediaServerName} Libraries",
|
||||||
"components.Settings.jellyfinlibrariesDescription": "The libraries {mediaServerName} scans for titles. Click the button below if no libraries are listed.",
|
"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.jellyfinsettings": "{mediaServerName} Settings",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,6 @@ const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
|
|||||||
return import('../i18n/locale/el.json');
|
return import('../i18n/locale/el.json');
|
||||||
case 'es':
|
case 'es':
|
||||||
return import('../i18n/locale/es.json');
|
return import('../i18n/locale/es.json');
|
||||||
case 'es-MX':
|
|
||||||
return import('../i18n/locale/es_MX.json');
|
|
||||||
case 'fr':
|
case 'fr':
|
||||||
return import('../i18n/locale/fr.json');
|
return import('../i18n/locale/fr.json');
|
||||||
case 'hr':
|
case 'hr':
|
||||||
|
|||||||
@@ -5033,11 +5033,6 @@ 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user