mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 10:49:30 -05:00
Compare commits
33 Commits
v1.8.0
...
preview-dn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce5925426 | ||
|
|
38ad875dd7 | ||
|
|
a9741fa36d | ||
|
|
b5a069901a | ||
|
|
9aeb3604e6 | ||
|
|
6eb88f8674 | ||
|
|
46ee8a4ca1 | ||
|
|
f52939e4cd | ||
|
|
d31a2c37e6 | ||
|
|
20863d4a8d | ||
|
|
4757f1c3e5 | ||
|
|
1f1ad72e9e | ||
|
|
c3ddc860b6 | ||
|
|
2bd125d9a5 | ||
|
|
7a5e8d69bf | ||
|
|
650c339d74 | ||
|
|
4ef5a3c7c5 | ||
|
|
a791b53953 | ||
|
|
68467ced9d | ||
|
|
296aee6338 | ||
|
|
0a4b38e50d | ||
|
|
bcc84d8551 | ||
|
|
783fda9621 | ||
|
|
d765055da8 | ||
|
|
fed66f0702 | ||
|
|
461202da75 | ||
|
|
0bbcfdc4f9 | ||
|
|
f486fb5e75 | ||
|
|
10082292e8 | ||
|
|
c0a0b9c8a8 | ||
|
|
d9d07c705a | ||
|
|
0eea1090df | ||
|
|
cd0fa3e223 |
@@ -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:
|
||||
|
||||
204
CHANGELOG.md
204
CHANGELOG.md
@@ -1,207 +1,3 @@
|
||||
# [1.8.0](https://github.com/fallenbagel/jellyseerr/compare/v1.7.0...v1.8.0) (2024-04-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* correct width issue in datepicker of filterSliderOver ([f564cdd](https://github.com/fallenbagel/jellyseerr/commit/f564cddff4525ccebffbf304672d49c57aefe635)), closes [#415](https://github.com/fallenbagel/jellyseerr/issues/415)
|
||||
* disable seasonfolder option in sonarr for jellyfin/Emby users ([8ec8f2a](https://github.com/fallenbagel/jellyseerr/commit/8ec8f2ac5730aad3b12dcd8ed95bb553b46b399c)), closes [#126](https://github.com/fallenbagel/jellyseerr/issues/126) [#575](https://github.com/fallenbagel/jellyseerr/issues/575)
|
||||
* **embyauth:** remove the accidentally added mediaServerType change code from another PR ([#684](https://github.com/fallenbagel/jellyseerr/issues/684)) ([c2e8771](https://github.com/fallenbagel/jellyseerr/commit/c2e87714b4c4aa11bf68dcd82b76979f82990f3c))
|
||||
* ensure watchlist updates are immediately reflected ([b85d7f3](https://github.com/fallenbagel/jellyseerr/commit/b85d7f37b931735ca2ad955dccb6599bf445fc73))
|
||||
* fix german translation for "components.Discover.FilterSlideover.tmdbuservotecount" ([e032c02](https://github.com/fallenbagel/jellyseerr/commit/e032c02f5f84dc4b6b470eecb18ba2c376c55f37))
|
||||
* fix the translations for watchlist permissions and userSettings page ([8c82a61](https://github.com/fallenbagel/jellyseerr/commit/8c82a61450a7525c0e2f1b64e6939da47a7c715d))
|
||||
* **i18n:** fixed jellyfin jobs ([7eed236](https://github.com/fallenbagel/jellyseerr/commit/7eed23637ddfb10bdcb19698e7ae171f07299502))
|
||||
* **jellyfin.ts:** process virtual seasons if they have non virtual episodes ([#639](https://github.com/fallenbagel/jellyseerr/issues/639)) ([db84f65](https://github.com/fallenbagel/jellyseerr/commit/db84f6529ab285be26c96daaab065dfabf347417))
|
||||
* **jellyfinapi:** refactors jellyfin library sync to support automatic grouping and collections ([#700](https://github.com/fallenbagel/jellyseerr/issues/700)) ([3856061](https://github.com/fallenbagel/jellyseerr/commit/3856061fe1ee4d3457996586b4979ad9dd60765a)), closes [#450](https://github.com/fallenbagel/jellyseerr/issues/450) [#524](https://github.com/fallenbagel/jellyseerr/issues/524) [#256](https://github.com/fallenbagel/jellyseerr/issues/256) [#489](https://github.com/fallenbagel/jellyseerr/issues/489) [#450](https://github.com/fallenbagel/jellyseerr/issues/450) [#524](https://github.com/fallenbagel/jellyseerr/issues/524) [#515](https://github.com/fallenbagel/jellyseerr/issues/515) [#474](https://github.com/fallenbagel/jellyseerr/issues/474) [#473](https://github.com/fallenbagel/jellyseerr/issues/473)
|
||||
* **jellyfinlogin:** use externalHostname if set for forgetpassword link ([405f6bb](https://github.com/fallenbagel/jellyseerr/commit/405f6bbb7ffc390327c99dcef2cbbf9b3bc75f01)), closes [#199](https://github.com/fallenbagel/jellyseerr/issues/199) [#424](https://github.com/fallenbagel/jellyseerr/issues/424) [#212](https://github.com/fallenbagel/jellyseerr/issues/212)
|
||||
* **jellyfinscanner:** conditionally assign the jellyfinMediaId and jellyfinMediaId4k ([#686](https://github.com/fallenbagel/jellyseerr/issues/686)) ([530be42](https://github.com/fallenbagel/jellyseerr/commit/530be4272cce1b0d74d7f4156b8d794cda6ea03f)), closes [#681](https://github.com/fallenbagel/jellyseerr/issues/681)
|
||||
* **langcode:** fixes the ukranian language code ([dc67aaa](https://github.com/fallenbagel/jellyseerr/commit/dc67aaaf53eae86ba20c6c2798c92ec40962d85f)), closes [#504](https://github.com/fallenbagel/jellyseerr/issues/504)
|
||||
* nullable type for jellyfinMediaId(4k) ([#702](https://github.com/fallenbagel/jellyseerr/issues/702)) ([0900a95](https://github.com/fallenbagel/jellyseerr/commit/0900a95532501b6f4d9698de7530a771512924fc)), closes [#668](https://github.com/fallenbagel/jellyseerr/issues/668)
|
||||
* request watchlist items sequentially to prevent bypassing quota ([#3667](https://github.com/fallenbagel/jellyseerr/issues/3667)) ([b40ba07](https://github.com/fallenbagel/jellyseerr/commit/b40ba07a4de5857b8392f667038eeb0b22aa5d9a))
|
||||
* resolved issue with region selector and all regions value ([#3652](https://github.com/fallenbagel/jellyseerr/issues/3652)) ([28a2c50](https://github.com/fallenbagel/jellyseerr/commit/28a2c50495d0ce531da7f8c442bd488a54b1e84c))
|
||||
* typos on readme ([#655](https://github.com/fallenbagel/jellyseerr/issues/655)) ([eee9a02](https://github.com/fallenbagel/jellyseerr/commit/eee9a025d246c72bcd3aca753d9e49c1f8f064ea))
|
||||
* **watchlist:** added missing prop for watchlist item removal button in watchlist page ([a0ec992](https://github.com/fallenbagel/jellyseerr/commit/a0ec992028093257e9fa043622e236014f02dea3))
|
||||
* **watchlist:** discover local watchlist item display and profile local watchlist slider visibility ([3cb9494](https://github.com/fallenbagel/jellyseerr/commit/3cb9494e6210151716587d8c4b22e0a21692cf88))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ko language ([#3619](https://github.com/fallenbagel/jellyseerr/issues/3619)) ([9250735](https://github.com/fallenbagel/jellyseerr/commit/92507359b48db08b0066047d6505660b8c8b0b12))
|
||||
* add Peacock to Network Slider ([#3545](https://github.com/fallenbagel/jellyseerr/issues/3545)) ([0c39057](https://github.com/fallenbagel/jellyseerr/commit/0c39057ca58743697e9dcc3b678440ac3688c65a))
|
||||
* add tooltips to tautulli avatars ([#3601](https://github.com/fallenbagel/jellyseerr/issues/3601)) ([c484810](https://github.com/fallenbagel/jellyseerr/commit/c484810f965f8d04643c25c6d283dd83f4bd4a23))
|
||||
* added Letterboxd links for the external link blocks for movies ([981f5e6](https://github.com/fallenbagel/jellyseerr/commit/981f5e679c4c707e119741240a58de8bb07f9d6c))
|
||||
* check if first jellyfin user is admin ([#635](https://github.com/fallenbagel/jellyseerr/issues/635)) ([010df62](https://github.com/fallenbagel/jellyseerr/commit/010df62776191fe4c195e590df338f8d8523f55b)), closes [#610](https://github.com/fallenbagel/jellyseerr/issues/610)
|
||||
* jellyseerr makeover ([#715](https://github.com/fallenbagel/jellyseerr/issues/715)) ([0c27132](https://github.com/fallenbagel/jellyseerr/commit/0c2713213c56de342f76300d12ce01fd543d2ce3))
|
||||
* **job:** media availability support for jellyfin/emby ([#522](https://github.com/fallenbagel/jellyseerr/issues/522)) ([3eb1bb3](https://github.com/fallenbagel/jellyseerr/commit/3eb1bb3d8ff22391acb2e629bbec7b6e4b65ca95)), closes [#406](https://github.com/fallenbagel/jellyseerr/issues/406) [#193](https://github.com/fallenbagel/jellyseerr/issues/193) [#516](https://github.com/fallenbagel/jellyseerr/issues/516) [#362](https://github.com/fallenbagel/jellyseerr/issues/362) [#84](https://github.com/fallenbagel/jellyseerr/issues/84)
|
||||
* **notif:** add Pushover sound options ([#2403](https://github.com/fallenbagel/jellyseerr/issues/2403)) ([3ea5076](https://github.com/fallenbagel/jellyseerr/commit/3ea5076053359b518b1b4d537e7b61580d9275a3))
|
||||
* select default seriesType for anime ([#3627](https://github.com/fallenbagel/jellyseerr/issues/3627)) ([f628635](https://github.com/fallenbagel/jellyseerr/commit/f6286359cfd2ed93fc692aa2efda37310e02c11c)), closes [#3626](https://github.com/fallenbagel/jellyseerr/issues/3626)
|
||||
* standard series type selector ([#3628](https://github.com/fallenbagel/jellyseerr/issues/3628)) ([7bdd25e](https://github.com/fallenbagel/jellyseerr/commit/7bdd25e5a45843a3e530d3fa2b0887664b53eec8))
|
||||
* translations update from Hosted Weblate ([#3258](https://github.com/fallenbagel/jellyseerr/issues/3258)) ([e62a078](https://github.com/fallenbagel/jellyseerr/commit/e62a078298ced7dec627fb3ff9fc8f99a39d5e1b))
|
||||
* update SameSite policy of session cookie to Lax ([#3650](https://github.com/fallenbagel/jellyseerr/issues/3650)) ([c84ca43](https://github.com/fallenbagel/jellyseerr/commit/c84ca4307465af4278f3dad5cf9c2b8cbae3fada))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* **jellyfinapi:** reverts [#450](https://github.com/fallenbagel/jellyseerr/issues/450) as it broke library sync support for local accounts using LDAP ([b5acc09](https://github.com/fallenbagel/jellyseerr/commit/b5acc09ba98e2dd9b61e6b78721e4dd9f42a996c)), closes [#489](https://github.com/fallenbagel/jellyseerr/issues/489)
|
||||
|
||||
# [1.7.0](https://github.com/fallenbagel/jellyseerr/compare/v1.6.0...v1.7.0) (2023-09-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adjust the plex watchlist sync schedule to have fuzziness ([#3502](https://github.com/fallenbagel/jellyseerr/issues/3502)) ([2c3f533](https://github.com/fallenbagel/jellyseerr/commit/2c3f5330764492e1323afd2d1f25e28ad78a2f2f))
|
||||
* handle issue causing incorrect media to change to unknown ([#3516](https://github.com/fallenbagel/jellyseerr/issues/3516)) ([83b008c](https://github.com/fallenbagel/jellyseerr/commit/83b008c8391459bd02dc74bcdb0d8caf27207bdf))
|
||||
* improved handling of edge case that could cause availability sync to fail ([#3497](https://github.com/fallenbagel/jellyseerr/issues/3497)) ([d0836ce](https://github.com/fallenbagel/jellyseerr/commit/d0836ce0efd55fccf2546087a0c4f94f7cb2e82a))
|
||||
* Include all defaults in payload ([#3538](https://github.com/fallenbagel/jellyseerr/issues/3538)) ([cb63bf2](https://github.com/fallenbagel/jellyseerr/commit/cb63bf217b9e8810a5210b4bf475b2a96583cc84))
|
||||
* multiple notifications for available media ([048fa96](https://github.com/fallenbagel/jellyseerr/commit/048fa967f2e5b23831ac9917c703934c50ef75f0))
|
||||
* repeat notifications for available 4k media ([30361f2](https://github.com/fallenbagel/jellyseerr/commit/30361f2ab751d9a882a9120e0f3df28dc42cc2cd))
|
||||
* resolved issue with create slider causing incorrect form submission ([#3514](https://github.com/fallenbagel/jellyseerr/issues/3514)) ([a761b7d](https://github.com/fallenbagel/jellyseerr/commit/a761b7dd35a5bd61bb4eb0275b75d1e0977e6a2d))
|
||||
* resolved user access check issue ([#3551](https://github.com/fallenbagel/jellyseerr/issues/3551)) ([2816c66](https://github.com/fallenbagel/jellyseerr/commit/2816c66300bf870d493c0665b0e984d60f707dfd))
|
||||
* **server/api/jellyfin.ts:** use /Library/VirtualFolders Jellyfin API call to fetch Jellyfin libs ([8685f57](https://github.com/fallenbagel/jellyseerr/commit/8685f5796a99d9700146bae9892319db10508d68)), closes [#256](https://github.com/fallenbagel/jellyseerr/issues/256)
|
||||
* **statusbadge:** handle missing season/episode number ([#3526](https://github.com/fallenbagel/jellyseerr/issues/3526)) ([01de972](https://github.com/fallenbagel/jellyseerr/commit/01de972a8fe2ea3c18d5b2f426d01b5b14d142d4))
|
||||
* **tautulli:** only test connection if hostname is defined ([#3573](https://github.com/fallenbagel/jellyseerr/issues/3573)) ([f7b4dfc](https://github.com/fallenbagel/jellyseerr/commit/f7b4dfcac472d08c54779a14fc1ad3c90927df26))
|
||||
* **ui:** corrected issues icon color ([#3498](https://github.com/fallenbagel/jellyseerr/issues/3498)) ([c1a47bd](https://github.com/fallenbagel/jellyseerr/commit/c1a47bd9de332cb4925974690f5a33448b5cc2e6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **rating:** added IMDB Radarr proxy ([#3496](https://github.com/fallenbagel/jellyseerr/issues/3496)) ([b4191f9](https://github.com/fallenbagel/jellyseerr/commit/b4191f9c65b7ff08764e61d18e7a75bc8d4b3325))
|
||||
|
||||
# [1.6.0](https://github.com/fallenbagel/jellyseerr/compare/v1.5.0...v1.6.0) (2023-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* availability sync file detection ([#3371](https://github.com/fallenbagel/jellyseerr/issues/3371)) ([7522aa3](https://github.com/fallenbagel/jellyseerr/commit/7522aa31743b169c903ebdf9d4d698645d27514c))
|
||||
* corrected initial fallback data load on details page ([#3395](https://github.com/fallenbagel/jellyseerr/issues/3395)) ([4bd8764](https://github.com/fallenbagel/jellyseerr/commit/4bd87647d0551c20e13589a62690a6f3e5ad8ff7))
|
||||
* correctly load series fallback modal with sonarr v4 ([#3451](https://github.com/fallenbagel/jellyseerr/issues/3451)) ([e051b1d](https://github.com/fallenbagel/jellyseerr/commit/e051b1dfea9c9320cc9dd420c475ae74cff0d901))
|
||||
* **deps:** update all non-major dependencies ([#3223](https://github.com/fallenbagel/jellyseerr/issues/3223)) ([f5191ad](https://github.com/fallenbagel/jellyseerr/commit/f5191aded680357522a65bbdcc40d162b8fbf594))
|
||||
* error deleting users with over 1000 requests ([#3376](https://github.com/fallenbagel/jellyseerr/issues/3376)) ([ac77b03](https://github.com/fallenbagel/jellyseerr/commit/ac77b037d5fb0c54f5edf4b29d04adb57aef388f))
|
||||
* external url regex is now consistent with internal url ([33ec443](https://github.com/fallenbagel/jellyseerr/commit/33ec4436fb82e1eb1bc97dd650088c27785e9d94))
|
||||
* externalLinkBlock ([46cd4d0](https://github.com/fallenbagel/jellyseerr/commit/46cd4d01d9a3cf17d79350c5e678202820272299))
|
||||
* fix regex for internal url to use a more effecient one ([e848386](https://github.com/fallenbagel/jellyseerr/commit/e848386d10f05f157e7a6dde8847ecab50c169ac))
|
||||
* fixes RT ratings for tv shows ([#3492](https://github.com/fallenbagel/jellyseerr/issues/3492)) ([04fbd00](https://github.com/fallenbagel/jellyseerr/commit/04fbd00d4ac29045592588ef8b664d1916991e37)), closes [#3491](https://github.com/fallenbagel/jellyseerr/issues/3491)
|
||||
* **genreselector:** fix searching in Genre filter ([#3468](https://github.com/fallenbagel/jellyseerr/issues/3468)) ([d7fa35e](https://github.com/fallenbagel/jellyseerr/commit/d7fa35e066cf371797aaa46ca464aa531ba8fb35))
|
||||
* handle search results with collections ([#3393](https://github.com/fallenbagel/jellyseerr/issues/3393)) ([70b1540](https://github.com/fallenbagel/jellyseerr/commit/70b1540ae23e83e01013856a9e06ad39e600922d))
|
||||
* lock body scroll when using webkit ([#3399](https://github.com/fallenbagel/jellyseerr/issues/3399)) ([c27f960](https://github.com/fallenbagel/jellyseerr/commit/c27f96096ac8cc6c387f9d1dde5b263576ac2132))
|
||||
* **logs:** jellyfin auth error now has the severity warn consistent with local login ([cc041b5](https://github.com/fallenbagel/jellyseerr/commit/cc041b5e0aa2b67573edba5919772b77a5111162)), closes [#224](https://github.com/fallenbagel/jellyseerr/issues/224)
|
||||
* make a (shallow) copy of radarr/sonarr tags into a request before adding user tags ([#3485](https://github.com/fallenbagel/jellyseerr/issues/3485)) ([48f7666](https://github.com/fallenbagel/jellyseerr/commit/48f76662d5c08156f1da3f47e216c5f02668f64b))
|
||||
* **ui:** corrected default badge hover opacity ([#3369](https://github.com/fallenbagel/jellyseerr/issues/3369)) ([a4d07f5](https://github.com/fallenbagel/jellyseerr/commit/a4d07f5afab613317d96c9c6e9b47157a5a28986))
|
||||
* **ui:** corrected mobile menu spacing in collection details ([#3432](https://github.com/fallenbagel/jellyseerr/issues/3432)) ([77a33cb](https://github.com/fallenbagel/jellyseerr/commit/77a33cb74d744bb747b791785799b632af8c7862))
|
||||
* **ui:** Make play symbol white ([1fe4bb8](https://github.com/fallenbagel/jellyseerr/commit/1fe4bb8a0415a72791ced75a2fba1027287398d5))
|
||||
* **ui:** Resize Emby icon and add margins ([ad69d67](https://github.com/fallenbagel/jellyseerr/commit/ad69d6715e976630092bfbbb1843886523551014))
|
||||
* **watchlist:** add validation for creation request ([03316c6](https://github.com/fallenbagel/jellyseerr/commit/03316c642d1ecf89753789af08caf6e3aac80113))
|
||||
* **watchlist:** fix github code scanning ([c08897b](https://github.com/fallenbagel/jellyseerr/commit/c08897bdc1cff65862c62347572bbbd01b6c36ac))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **add watchlist:** adding midding functionality from overserr ([5f1c10d](https://github.com/fallenbagel/jellyseerr/commit/5f1c10d50aaa430bcda96218ef2cc12a0eb926f3))
|
||||
* adds streaming services custom slider ([#3361](https://github.com/fallenbagel/jellyseerr/issues/3361)) ([2520d8f](https://github.com/fallenbagel/jellyseerr/commit/2520d8f739abfde608f3ef66a9fbe6b7b5c6647a))
|
||||
* auto tagging requested media with username ([#3338](https://github.com/fallenbagel/jellyseerr/issues/3338)) ([24f268b](https://github.com/fallenbagel/jellyseerr/commit/24f268b6cb67d9a8d8675cd6e09dd83a7f499add))
|
||||
* **discover:** support filtering by tmdb user vote count on discover page ([#3407](https://github.com/fallenbagel/jellyseerr/issues/3407)) ([aa84977](https://github.com/fallenbagel/jellyseerr/commit/aa849776809dfe891e67ff4db6861ef44df1a774))
|
||||
* **settings:** add internal url to jellyfin settings form ([0a30cd3](https://github.com/fallenbagel/jellyseerr/commit/0a30cd356d217a39546c016cc8bfa6ff6ad75e3e)), closes [#194](https://github.com/fallenbagel/jellyseerr/issues/194)
|
||||
* **src/components/externallinkblock/index.tsx:** support Emby icon ([672061c](https://github.com/fallenbagel/jellyseerr/commit/672061cd646c97c9954790c8e50eac88ea2666e9))
|
||||
* **tooltip:** email tooltip now appears when hovered over info icon ([cd7930e](https://github.com/fallenbagel/jellyseerr/commit/cd7930eef98451a781e5c9dc5ec223600a379f42))
|
||||
* translations update ([47287c3](https://github.com/fallenbagel/jellyseerr/commit/47287c368885d14bd1a56e3e8318ce22dd0f6ddf)), closes [#381](https://github.com/fallenbagel/jellyseerr/issues/381)
|
||||
* **watchlist:** add translation for en ([b7e3d28](https://github.com/fallenbagel/jellyseerr/commit/b7e3d285ed35b623062eceb0d99035cafbf075a6))
|
||||
|
||||
# [1.5.0](https://github.com/fallenbagel/jellyseerr/compare/v1.4.1...v1.5.0) (2023-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add better checks on 4k detection of series ([bc9017f](https://github.com/fallenbagel/jellyseerr/commit/bc9017f54d84ec24c4d74d38e1b4e24219425d41))
|
||||
* added a refresh interval if download status is in progress ([#3275](https://github.com/fallenbagel/jellyseerr/issues/3275)) ([1e2c6f4](https://github.com/fallenbagel/jellyseerr/commit/1e2c6f46ab66c836f321b5d8e34f1e8124c0b542))
|
||||
* **build:** increase threshold for amount of data to be fetched when SSR'ing ([#3320](https://github.com/fallenbagel/jellyseerr/issues/3320)) ([d7b83d2](https://github.com/fallenbagel/jellyseerr/commit/d7b83d22cee3d20db564cc0564d42802b02327e3))
|
||||
* disable availability sync temporarily ([2e5cf22](https://github.com/fallenbagel/jellyseerr/commit/2e5cf226265686012329248e7f729fec324c3deb))
|
||||
* hide remove button when default service is not configured ([7d4455b](https://github.com/fallenbagel/jellyseerr/commit/7d4455ba6bfd12e2730f7085cbb87df246f01d22))
|
||||
* **jellyfin scan:** temporary workaround fix for jellyfin scan when display specials within season ([38fb66d](https://github.com/fallenbagel/jellyseerr/commit/38fb66d31e41232c01898d0d362af8338eb7b960)), closes [#215](https://github.com/fallenbagel/jellyseerr/issues/215) [#176](https://github.com/fallenbagel/jellyseerr/issues/176) [#246](https://github.com/fallenbagel/jellyseerr/issues/246)
|
||||
* lint issues ([bcd2bb7](https://github.com/fallenbagel/jellyseerr/commit/bcd2bb7c96810f5a6932f42468a628d2db1bc771))
|
||||
* logger was set to info for the wrong logs ([#3354](https://github.com/fallenbagel/jellyseerr/issues/3354)) ([c36a4ba](https://github.com/fallenbagel/jellyseerr/commit/c36a4ba2b8df05873f5dfd0946a9bc3dc4ecfd1d))
|
||||
* remove unnecessary parenthesis from api key generation ([#3336](https://github.com/fallenbagel/jellyseerr/issues/3336)) ([6bd3f01](https://github.com/fallenbagel/jellyseerr/commit/6bd3f015d65507efca60279007bd2b86ee860643))
|
||||
* **snapcraft:** use the correct config folder for image cache ([#3302](https://github.com/fallenbagel/jellyseerr/issues/3302)) ([c93467b](https://github.com/fallenbagel/jellyseerr/commit/c93467b3acf2c256324297e7e8f21e9944005dd4))
|
||||
* **ui:** hide mini status badge if non-4K media status is unknown ([#3346](https://github.com/fallenbagel/jellyseerr/issues/3346)) ([50f06da](https://github.com/fallenbagel/jellyseerr/commit/50f06dabbffc693f0843584a64d1d96e77982820))
|
||||
* **ui:** hide search bar behind slideover when opened ([#3348](https://github.com/fallenbagel/jellyseerr/issues/3348)) ([b3882de](https://github.com/fallenbagel/jellyseerr/commit/b3882de8930a70adb2f93a27be6370bfa1826587))
|
||||
* **ui:** prevent title cards from flickering when quickly hovering across them ([#3349](https://github.com/fallenbagel/jellyseerr/issues/3349)) ([eb5502a](https://github.com/fallenbagel/jellyseerr/commit/eb5502a16f86e37a933f6beca0678c2d228e77d5))
|
||||
* **watchlist:** correctly load more than 20 watchlist items ([#3351](https://github.com/fallenbagel/jellyseerr/issues/3351)) ([af880a6](https://github.com/fallenbagel/jellyseerr/commit/af880a6c839794b34bddcd7e0fe56353aa48ba36))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a button in ManageSlideOver to remove the movie and the file from Radarr/Sonarr ([2e74584](https://github.com/fallenbagel/jellyseerr/commit/2e7458457e995dd3ec6dd96035fe997646cdd446))
|
||||
* availability sync rework ([#3219](https://github.com/fallenbagel/jellyseerr/issues/3219)) ([ae38183](https://github.com/fallenbagel/jellyseerr/commit/ae3818304b2f75222d1bd223ece94f829a3b42d0)), closes [#377](https://github.com/fallenbagel/jellyseerr/issues/377)
|
||||
* full title of download item on hover with tooltip ([#3296](https://github.com/fallenbagel/jellyseerr/issues/3296)) ([33e7691](https://github.com/fallenbagel/jellyseerr/commit/33e7691b94d7d369a0a1410e434850bc51e5572e))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **imageproxy:** do not set cookies to image proxy so CDNs can cache images ([#3332](https://github.com/fallenbagel/jellyseerr/issues/3332)) ([966639d](https://github.com/fallenbagel/jellyseerr/commit/966639df430d32f6bfebdb16314dc4590d21caf8))
|
||||
|
||||
## [1.4.1](https://github.com/fallenbagel/jellyseerr/compare/v1.4.0...v1.4.1) (2023-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pass in library type when scanning recently added items ([#3287](https://github.com/fallenbagel/jellyseerr/issues/3287)) ([8942eb8](https://github.com/fallenbagel/jellyseerr/commit/8942eb8b7c4fa1d16aa2e72e8ba7120a653c9aa2))
|
||||
* **ui:** air date will use UTC for timezone ([#3297](https://github.com/fallenbagel/jellyseerr/issues/3297)) ([3e43586](https://github.com/fallenbagel/jellyseerr/commit/3e43586acc0804c3fff524509caa890a104e132b))
|
||||
* **ui:** correct range slider styling in chrome ([#3299](https://github.com/fallenbagel/jellyseerr/issues/3299)) ([d954328](https://github.com/fallenbagel/jellyseerr/commit/d9543289111d72245564d25d300a71b0ea3954ba))
|
||||
* **ui:** show 5 icons when possible on mobile menu ([#3298](https://github.com/fallenbagel/jellyseerr/issues/3298)) ([7040da1](https://github.com/fallenbagel/jellyseerr/commit/7040da1334f6d18e19a494c73caa17f7df552dfe))
|
||||
* **ui:** style range thumbs correctly for firefox ([#3294](https://github.com/fallenbagel/jellyseerr/issues/3294)) ([9d10e6a](https://github.com/fallenbagel/jellyseerr/commit/9d10e6a88c0996671f1d9d20792e1930dbc82329))
|
||||
|
||||
# [1.4.0](https://github.com/fallenbagel/jellyseerr/compare/v1.3.0...v1.4.0) (2023-01-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add bg-opacity to in-progress status badges ([#3190](https://github.com/fallenbagel/jellyseerr/issues/3190)) ([68223f4](https://github.com/fallenbagel/jellyseerr/commit/68223f4b1e98b01825516dcba39cbb2d3df31a70))
|
||||
* added download status and title to request card/item error components ([#3186](https://github.com/fallenbagel/jellyseerr/issues/3186)) ([3309f77](https://github.com/fallenbagel/jellyseerr/commit/3309f77aa4be1d70b27693531c119a8e26822518))
|
||||
* arrow icons were misplaced on mobile in slider edit ([#3260](https://github.com/fallenbagel/jellyseerr/issues/3260)) ([d328485](https://github.com/fallenbagel/jellyseerr/commit/d328485161b9cae6a70ef0713b4878207bc6015e))
|
||||
* **build:** update usage of publish snap action ([#3272](https://github.com/fallenbagel/jellyseerr/issues/3272)) ([51b05cd](https://github.com/fallenbagel/jellyseerr/commit/51b05cd8fbb5d332807d8c00b2ffb7b10c3d0179))
|
||||
* changed overflow scroll to only if necessary ([#3184](https://github.com/fallenbagel/jellyseerr/issues/3184)) ([27feeea](https://github.com/fallenbagel/jellyseerr/commit/27feeea69121336557deda1f32b65a5daa146f82))
|
||||
* convert genre/studio to string in create slider ([#3201](https://github.com/fallenbagel/jellyseerr/issues/3201)) ([93afead](https://github.com/fallenbagel/jellyseerr/commit/93afead92e497f2e5bce67a34fffdaa08d20c7f2))
|
||||
* correct checkbox position (again) for slider edits ([#3227](https://github.com/fallenbagel/jellyseerr/issues/3227)) ([3ba6df1](https://github.com/fallenbagel/jellyseerr/commit/3ba6df1a41c084c4a6a90354338047623abef521))
|
||||
* correct grid sizing for webkit on streaming services ([#3248](https://github.com/fallenbagel/jellyseerr/issues/3248)) ([6fd11cf](https://github.com/fallenbagel/jellyseerr/commit/6fd11cf4254e1a19310592bec78a6de52bc073a8))
|
||||
* correct issue detail bottom padding on mobile displays ([#3268](https://github.com/fallenbagel/jellyseerr/issues/3268)) ([3db010b](https://github.com/fallenbagel/jellyseerr/commit/3db010b9eaec62aa08d973a61caf1801471bbf3e))
|
||||
* correct link to correct keyword results for series ([#3208](https://github.com/fallenbagel/jellyseerr/issues/3208)) ([4e9be7a](https://github.com/fallenbagel/jellyseerr/commit/4e9be7a3f7304ee7be5ee6fd34b1ea8f6c0cf399))
|
||||
* correct spacing between sliders ([#3225](https://github.com/fallenbagel/jellyseerr/issues/3225)) ([62e2de7](https://github.com/fallenbagel/jellyseerr/commit/62e2de70bf37b72d5f63370b662d4103a642775b))
|
||||
* correctly check mobile menu permissions ([#3271](https://github.com/fallenbagel/jellyseerr/issues/3271)) ([f4a22dc](https://github.com/fallenbagel/jellyseerr/commit/f4a22dc437404558f301ccfc195cf0a300dd1ff2))
|
||||
* correctly restore selected streaming service filters ([#3249](https://github.com/fallenbagel/jellyseerr/issues/3249)) ([154f3e7](https://github.com/fallenbagel/jellyseerr/commit/154f3e72efbf0b663358b3029156f54516f01a2f))
|
||||
* create shared class to add bottom spacing ([#3269](https://github.com/fallenbagel/jellyseerr/issues/3269)) ([5d1c6f7](https://github.com/fallenbagel/jellyseerr/commit/5d1c6f706555613d97ed9e61d8b665543c2f239b))
|
||||
* **deps:** pin dependency @headlessui/react to 1.7.7 ([#3194](https://github.com/fallenbagel/jellyseerr/issues/3194)) [skip ci] ([c4b16ab](https://github.com/fallenbagel/jellyseerr/commit/c4b16abc62647c74215155942a4230a31a238677))
|
||||
* **deps:** update dependency @heroicons/react to v2 ([#2970](https://github.com/fallenbagel/jellyseerr/issues/2970)) ([dd48d59](https://github.com/fallenbagel/jellyseerr/commit/dd48d59b20e2d1800ea30912116f4a4f1bb7928f))
|
||||
* **deps:** update dependency axios to v1 ([#3202](https://github.com/fallenbagel/jellyseerr/issues/3202)) ([421029e](https://github.com/fallenbagel/jellyseerr/commit/421029ebab66c9a6622ba47e56d7f6473524cce4))
|
||||
* **deps:** update dependency swr to v2 ([#3212](https://github.com/fallenbagel/jellyseerr/issues/3212)) ([7b6db50](https://github.com/fallenbagel/jellyseerr/commit/7b6db50ae55b1fc60d19a5cff62dd46bb989fa51))
|
||||
* **experimental:** use new RT API (sorta) ([#3179](https://github.com/fallenbagel/jellyseerr/issues/3179)) ([357cab8](https://github.com/fallenbagel/jellyseerr/commit/357cab87ac7752b8e119b51c938b343c661d83c2))
|
||||
* improve small screen layout for discover editing ([#3221](https://github.com/fallenbagel/jellyseerr/issues/3221)) ([d23b213](https://github.com/fallenbagel/jellyseerr/commit/d23b2132de05f072f7f9daad83d81421d747cf99))
|
||||
* include new package calendar css in build ([#3235](https://github.com/fallenbagel/jellyseerr/issues/3235)) ([c2a1a20](https://github.com/fallenbagel/jellyseerr/commit/c2a1a20a3bb20039a1936c7fe0ecb9e8311a0aea))
|
||||
* issues with issues ([#3267](https://github.com/fallenbagel/jellyseerr/issues/3267)) ([fd21971](https://github.com/fallenbagel/jellyseerr/commit/fd219717c01c558814d7a80de6304272b5a7944e))
|
||||
* multiple genre filtering now works ([#3282](https://github.com/fallenbagel/jellyseerr/issues/3282)) ([5076938](https://github.com/fallenbagel/jellyseerr/commit/507693881b939819413f0959df5ef6b7a357eb5c))
|
||||
* prevent double encode if we are on /search endpoint ([#3238](https://github.com/fallenbagel/jellyseerr/issues/3238)) ([a343f8a](https://github.com/fallenbagel/jellyseerr/commit/a343f8ad915491a9c81512c7e541a1dac8906025))
|
||||
* **request:** approve request when retrying request ([#3234](https://github.com/fallenbagel/jellyseerr/issues/3234)) ([b515701](https://github.com/fallenbagel/jellyseerr/commit/b5157010c46cd9083993d5ee0172007b83d631da))
|
||||
* **request:** mark request as approved if media is already available when retrying failed request ([#3244](https://github.com/fallenbagel/jellyseerr/issues/3244)) ([cb65074](https://github.com/fallenbagel/jellyseerr/commit/cb650745f6a33e69391a633e6d272831f314e098))
|
||||
* restore border to ghost button and fix discover slider visibility toggle position ([#3226](https://github.com/fallenbagel/jellyseerr/issues/3226)) ([2eebb7f](https://github.com/fallenbagel/jellyseerr/commit/2eebb7fd3941b34fe9472aaf9d28265df8cce311))
|
||||
* restore status badges on titles on actors page when hide available media enabled ([#3206](https://github.com/fallenbagel/jellyseerr/issues/3206)) ([9d3446d](https://github.com/fallenbagel/jellyseerr/commit/9d3446d370499c3251159393e5c791b01225e05c))
|
||||
* screen would zoom on mobile if date picker input was selected ([#3241](https://github.com/fallenbagel/jellyseerr/issues/3241)) ([3aefddd](https://github.com/fallenbagel/jellyseerr/commit/3aefddd48834d86150d5f5cceb2d08af3a78847b))
|
||||
* series displayed an empty season with series list/request modal ([#3147](https://github.com/fallenbagel/jellyseerr/issues/3147)) ([2179637](https://github.com/fallenbagel/jellyseerr/commit/2179637d437999290eaa4152f6f37c71fc3d8ba3))
|
||||
* tooltip shows properly if not in progress ([#3185](https://github.com/fallenbagel/jellyseerr/issues/3185)) ([6face8c](https://github.com/fallenbagel/jellyseerr/commit/6face8cc4564b978fb98af32659b326d8c5cede8))
|
||||
* **ui:** series first air date sorting ([#3283](https://github.com/fallenbagel/jellyseerr/issues/3283)) ([374c78c](https://github.com/fallenbagel/jellyseerr/commit/374c78c989cc86bb144a954a91d5d183c4b591c0))
|
||||
* update StatusBadgeMini to shrink on title cards (and remove ring) ([#3210](https://github.com/fallenbagel/jellyseerr/issues/3210)) ([042a1a9](https://github.com/fallenbagel/jellyseerr/commit/042a1a950fdd4d4a61edf4bc19657f9b7a526da8))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add discover customization ([#3182](https://github.com/fallenbagel/jellyseerr/issues/3182)) ([cd35748](https://github.com/fallenbagel/jellyseerr/commit/cd3574851a12517cbfadc109e6412a7a9e44c114))
|
||||
* add keywords to movie/series detail pages ([#3204](https://github.com/fallenbagel/jellyseerr/issues/3204)) ([e084649](https://github.com/fallenbagel/jellyseerr/commit/e084649878a58c296786141d12dd69a69a27ee85))
|
||||
* add streaming services filter ([#3247](https://github.com/fallenbagel/jellyseerr/issues/3247)) ([1154156](https://github.com/fallenbagel/jellyseerr/commit/1154156459403494e8daf0c89a3ba356aeea1d97))
|
||||
* discover inline customization ([#3220](https://github.com/fallenbagel/jellyseerr/issues/3220)) ([8bd10b5](https://github.com/fallenbagel/jellyseerr/commit/8bd10b5bf3d1b8069872b616c7c8596caeb4937e))
|
||||
* discover overhaul (filters!) ([#3232](https://github.com/fallenbagel/jellyseerr/issues/3232)) ([dd00e48](https://github.com/fallenbagel/jellyseerr/commit/dd00e48f59054b44bef6b32a2c169e59f6175051))
|
||||
* discover slider edit arrow buttons for reordering ([#3259](https://github.com/fallenbagel/jellyseerr/issues/3259)) ([da00d45](https://github.com/fallenbagel/jellyseerr/commit/da00d454e17e8b00d04f6e26f6dd5153ed6ced81))
|
||||
* **lang:** translations update from Hosted Weblate ([#3030](https://github.com/fallenbagel/jellyseerr/issues/3030)) ([0d8b390](https://github.com/fallenbagel/jellyseerr/commit/0d8b390b678731e76bd1f0f8a0a4952c11e77f4d))
|
||||
* new mobile menu ([#3251](https://github.com/fallenbagel/jellyseerr/issues/3251)) ([fcbca17](https://github.com/fallenbagel/jellyseerr/commit/fcbca1722f31f32633a57bc5048f46c9da057d87))
|
||||
* translations update from Hosted Weblate ([#3218](https://github.com/fallenbagel/jellyseerr/issues/3218)) ([5940ff7](https://github.com/fallenbagel/jellyseerr/commit/5940ff7f5f62eed9ac5aa6f02803418aaa09813a))
|
||||
* **ui:** add episode number to front of episode name in season details ([#3086](https://github.com/fallenbagel/jellyseerr/issues/3086)) ([a672b32](https://github.com/fallenbagel/jellyseerr/commit/a672b324ec391a20f6f3a1daed82a8d276a52c2c))
|
||||
* **ui:** request card progress bar ([#3123](https://github.com/fallenbagel/jellyseerr/issues/3123)) ([03853a1](https://github.com/fallenbagel/jellyseerr/commit/03853a1b9155c8a2153c8885022a74619af1bc15))
|
||||
|
||||
# [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
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>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"region": "",
|
||||
"originalLanguage": "",
|
||||
"trustProxy": false,
|
||||
"mediaServerType": 1,
|
||||
"partialRequestsEnabled": true,
|
||||
"locale": "en"
|
||||
},
|
||||
@@ -37,6 +38,17 @@
|
||||
],
|
||||
"machineId": "test"
|
||||
},
|
||||
"jellyfin": {
|
||||
"name": "",
|
||||
"ip": "",
|
||||
"port": 8096,
|
||||
"useSsl": false,
|
||||
"urlBase": "",
|
||||
"externalHostname": "",
|
||||
"jellyfinForgotPasswordUrl": "",
|
||||
"libraries": [],
|
||||
"serverId": ""
|
||||
},
|
||||
"tautulli": {},
|
||||
"radarr": [],
|
||||
"sonarr": [],
|
||||
@@ -139,11 +151,26 @@
|
||||
"sonarr-scan": {
|
||||
"schedule": "0 30 4 * * *"
|
||||
},
|
||||
"plex-watchlist-sync": {
|
||||
"schedule": "0 */10 * * * *"
|
||||
},
|
||||
"availability-sync": {
|
||||
"schedule": "0 0 5 * * *"
|
||||
},
|
||||
"download-sync": {
|
||||
"schedule": "0 * * * * *"
|
||||
},
|
||||
"download-sync-reset": {
|
||||
"schedule": "0 0 1 * * *"
|
||||
},
|
||||
"jellyfin-recently-added-scan": {
|
||||
"schedule": "0 */5 * * * *"
|
||||
},
|
||||
"jellyfin-full-scan": {
|
||||
"schedule": "0 0 3 * * *"
|
||||
},
|
||||
"image-cache-cleanup": {
|
||||
"schedule": "0 0 5 * * *"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.0",
|
||||
"version": "0.1.0",
|
||||
"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,90 @@ 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 authenticate = async (useHeaders: boolean) => {
|
||||
const headers =
|
||||
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
|
||||
|
||||
return this.post<JellyfinLoginResponse>(
|
||||
'/Users/AuthenticateByName',
|
||||
{
|
||||
Username: Username,
|
||||
Username,
|
||||
Pw: Password,
|
||||
}
|
||||
},
|
||||
{ headers }
|
||||
);
|
||||
return account.data;
|
||||
};
|
||||
|
||||
try {
|
||||
return await authenticate(true);
|
||||
} catch (e) {
|
||||
throw new Error('Unauthorized');
|
||||
logger.debug(`Failed to authenticate with headers: ${e.message}`, {
|
||||
label: 'Jellyfin API',
|
||||
ip: ClientIP,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await authenticate(false);
|
||||
} catch (e) {
|
||||
const status = e.response?.status;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,68 +184,84 @@ class JellyfinAPI {
|
||||
return;
|
||||
}
|
||||
|
||||
public async getSystemInfo(): Promise<any> {
|
||||
try {
|
||||
const systemInfoResponse = await this.get<any>('/System/Info');
|
||||
|
||||
return systemInfoResponse;
|
||||
} catch (e) {
|
||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||
}
|
||||
}
|
||||
|
||||
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 +295,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 +307,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 +333,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 +373,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 +385,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
server/constants/error.ts
Normal file
9
server/constants/error.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export enum ApiErrorCode {
|
||||
InvalidUrl = 'INVALID_URL',
|
||||
InvalidCredentials = 'INVALID_CREDENTIALS',
|
||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
||||
NotAdmin = 'NOT_ADMIN',
|
||||
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
||||
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
||||
Unknown = 'UNKNOWN',
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import {
|
||||
AfterLoad,
|
||||
Column,
|
||||
@@ -211,15 +212,12 @@ class Media {
|
||||
} else {
|
||||
const pageName =
|
||||
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
|
||||
const { serverId, hostname, externalHostname } = getSettings().jellyfin;
|
||||
let jellyfinHost =
|
||||
const { serverId, externalHostname } = getSettings().jellyfin;
|
||||
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
: getHostname();
|
||||
|
||||
if (this.jellyfinMediaId) {
|
||||
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
|
||||
|
||||
@@ -23,19 +23,25 @@ import imageproxy from '@server/routes/imageproxy';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import { getClientIp } from '@supercharge/request-ip';
|
||||
import type CacheableLookupType from 'cacheable-lookup';
|
||||
import { TypeormStore } from 'connect-typeorm/out';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import csurf from 'csurf';
|
||||
import { lookup } from 'dns';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import express from 'express';
|
||||
import * as OpenApiValidator from 'express-openapi-validator';
|
||||
import type { Store } from 'express-session';
|
||||
import session from 'express-session';
|
||||
import next from 'next';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import path from 'path';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import YAML from 'yamljs';
|
||||
|
||||
const _importDynamic = new Function('modulePath', 'return import(modulePath)');
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||
@@ -46,6 +52,25 @@ const handle = app.getRequestHandler();
|
||||
app
|
||||
.prepare()
|
||||
.then(async () => {
|
||||
const CacheableLookup = (await _importDynamic('cacheable-lookup'))
|
||||
.default as typeof CacheableLookupType;
|
||||
const cacheable = new CacheableLookup();
|
||||
|
||||
const originalLookup = cacheable.lookup;
|
||||
|
||||
// if hostname is localhost use dns.lookup instead of cacheable-lookup
|
||||
cacheable.lookup = (...args: any) => {
|
||||
const [hostname] = args;
|
||||
if (hostname === 'localhost') {
|
||||
lookup(...(args as Parameters<typeof lookup>));
|
||||
} else {
|
||||
originalLookup(...(args as Parameters<typeof originalLookup>));
|
||||
}
|
||||
};
|
||||
|
||||
cacheable.install(http.globalAgent);
|
||||
cacheable.install(https.globalAgent);
|
||||
|
||||
const dbConnection = await dataSource.initialize();
|
||||
|
||||
// Run migrations in production
|
||||
@@ -59,6 +84,16 @@ app
|
||||
const settings = getSettings().load();
|
||||
restartFlag.initializeSettings(settings.main);
|
||||
|
||||
// Overwrite DNS servers
|
||||
if (settings.main.overwriteDnsServers) {
|
||||
cacheable.servers = settings.main.overwriteDnsServers
|
||||
.split(',')
|
||||
.map((server) => server.trim());
|
||||
logger.info('Using custom DNS servers', {
|
||||
label: 'Settings',
|
||||
});
|
||||
}
|
||||
|
||||
// Migrate library types
|
||||
if (
|
||||
settings.plex.libraries.length > 1 &&
|
||||
|
||||
@@ -16,6 +16,7 @@ import { User } from '@server/entity/User';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
|
||||
class AvailabilitySync {
|
||||
public running = false;
|
||||
@@ -84,7 +85,7 @@ class AvailabilitySync {
|
||||
) {
|
||||
if (admin) {
|
||||
this.jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
getHostname(),
|
||||
admin.jellyfinAuthToken,
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Library } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import AsyncLock from '@server/utils/asyncLock';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import { randomUUID as uuid } from 'crypto';
|
||||
import { uniqWith } from 'lodash';
|
||||
|
||||
@@ -83,13 +84,17 @@ class JellyfinScanner {
|
||||
}
|
||||
|
||||
const has4k = metadata.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
return MediaSource.MediaStreams.filter(
|
||||
(MediaStream) => MediaStream.Type === 'Video'
|
||||
).some((MediaStream) => {
|
||||
return (MediaStream.Width ?? 0) > 2000;
|
||||
});
|
||||
});
|
||||
|
||||
const hasOtherResolution = metadata.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
return MediaSource.MediaStreams.filter(
|
||||
(MediaStream) => MediaStream.Type === 'Video'
|
||||
).some((MediaStream) => {
|
||||
return (MediaStream.Width ?? 0) <= 2000;
|
||||
});
|
||||
});
|
||||
@@ -590,8 +595,10 @@ class JellyfinScanner {
|
||||
return this.log('No admin configured. Jellyfin sync skipped.', 'warn');
|
||||
}
|
||||
|
||||
const hostname = getHostname();
|
||||
|
||||
this.jfClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
hostname,
|
||||
admin.jellyfinAuthToken,
|
||||
admin.jellyfinDeviceId
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { runMigrations } from '@server/lib/settings/migrator';
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import path from 'path';
|
||||
import webpush from 'web-push';
|
||||
import { Permission } from './permissions';
|
||||
|
||||
export interface Library {
|
||||
id: string;
|
||||
@@ -38,7 +39,10 @@ export interface PlexSettings {
|
||||
|
||||
export interface JellyfinSettings {
|
||||
name: string;
|
||||
hostname: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
useSsl?: boolean;
|
||||
urlBase?: string;
|
||||
externalHostname?: string;
|
||||
jellyfinForgotPasswordUrl?: string;
|
||||
libraries: Library[];
|
||||
@@ -114,6 +118,7 @@ export interface MainSettings {
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
locale: string;
|
||||
overwriteDnsServers?: string;
|
||||
}
|
||||
|
||||
interface PublicSettings {
|
||||
@@ -130,7 +135,6 @@ interface FullPublicSettings extends PublicSettings {
|
||||
region: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
jellyfinHost?: string;
|
||||
jellyfinExternalHost?: string;
|
||||
jellyfinForgotPasswordUrl?: string;
|
||||
jellyfinServerName?: string;
|
||||
@@ -274,7 +278,7 @@ export type JobId =
|
||||
| 'image-cache-cleanup'
|
||||
| 'availability-sync';
|
||||
|
||||
interface AllSettings {
|
||||
export interface AllSettings {
|
||||
clientId: string;
|
||||
vapidPublic: string;
|
||||
vapidPrivate: string;
|
||||
@@ -291,7 +295,7 @@ interface AllSettings {
|
||||
|
||||
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/settings.json`
|
||||
: path.join(__dirname, '../../config/settings.json');
|
||||
: path.join(__dirname, '../../../config/settings.json');
|
||||
|
||||
class Settings {
|
||||
private data: AllSettings;
|
||||
@@ -331,7 +335,10 @@ class Settings {
|
||||
},
|
||||
jellyfin: {
|
||||
name: '',
|
||||
hostname: '',
|
||||
ip: '',
|
||||
port: 8096,
|
||||
useSsl: false,
|
||||
urlBase: '',
|
||||
externalHostname: '',
|
||||
jellyfinForgotPasswordUrl: '',
|
||||
libraries: [],
|
||||
@@ -547,8 +554,6 @@ class Settings {
|
||||
region: this.data.main.region,
|
||||
originalLanguage: this.data.main.originalLanguage,
|
||||
mediaServerType: this.main.mediaServerType,
|
||||
jellyfinHost: this.jellyfin.hostname,
|
||||
jellyfinExternalHost: this.jellyfin.externalHostname,
|
||||
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
||||
cacheImages: this.data.main.cacheImages,
|
||||
vapidPublic: this.vapidPublic,
|
||||
@@ -637,7 +642,11 @@ class Settings {
|
||||
const data = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
||||
|
||||
if (data) {
|
||||
this.data = merge(this.data, JSON.parse(data));
|
||||
const parsedJson = JSON.parse(data);
|
||||
this.data = runMigrations(parsedJson);
|
||||
|
||||
this.data = merge(this.data, parsedJson);
|
||||
|
||||
this.save();
|
||||
}
|
||||
return this;
|
||||
30
server/lib/settings/migrations/0001_migrate_hostname.ts
Normal file
30
server/lib/settings/migrations/0001_migrate_hostname.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateHostname = (settings: any): AllSettings => {
|
||||
const oldJellyfinSettings = settings.jellyfin;
|
||||
if (oldJellyfinSettings && oldJellyfinSettings.hostname) {
|
||||
const { hostname } = oldJellyfinSettings;
|
||||
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
||||
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
||||
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
||||
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
||||
|
||||
delete oldJellyfinSettings.hostname;
|
||||
if (urlMatch) {
|
||||
const [, ip, , port, urlBase] = urlMatch;
|
||||
settings.jellyfin = {
|
||||
...settings.jellyfin,
|
||||
ip,
|
||||
port: port || (useSsl ? 443 : 80),
|
||||
useSsl,
|
||||
urlBase: urlBase ? urlBase.replace(/\/$/, '') : '',
|
||||
};
|
||||
}
|
||||
}
|
||||
if (settings.jellyfin && settings.jellyfin.hostname) {
|
||||
delete settings.jellyfin.hostname;
|
||||
}
|
||||
return settings;
|
||||
};
|
||||
|
||||
export default migrateHostname;
|
||||
21
server/lib/settings/migrator.ts
Normal file
21
server/lib/settings/migrator.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const migrationsDir = path.join(__dirname, 'migrations');
|
||||
|
||||
export const runMigrations = (settings: AllSettings): AllSettings => {
|
||||
const migrations = fs
|
||||
.readdirSync(migrationsDir)
|
||||
.filter((file) => file.endsWith('.js') || file.endsWith('.ts'))
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
.map((file) => require(path.join(migrationsDir, file)).default);
|
||||
|
||||
let migrated = settings;
|
||||
|
||||
for (const migration of migrations) {
|
||||
migrated = migration(migrated);
|
||||
}
|
||||
|
||||
return migrated;
|
||||
};
|
||||
@@ -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,9 +10,12 @@ 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 { getHostname } from '@server/utils/getHostname';
|
||||
import * as EmailValidator from 'email-validator';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import net from 'net';
|
||||
|
||||
const authRoutes = Router();
|
||||
|
||||
@@ -219,30 +223,39 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
username?: string;
|
||||
password?: string;
|
||||
hostname?: string;
|
||||
port?: number;
|
||||
urlBase?: string;
|
||||
useSsl?: boolean;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured
|
||||
if (
|
||||
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
|
||||
settings.jellyfin.hostname !== ''
|
||||
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
|
||||
) {
|
||||
return res.status(500).json({ error: 'Jellyfin login is disabled' });
|
||||
} else if (!body.username) {
|
||||
return res.status(500).json({ error: 'You must provide an username' });
|
||||
} else if (settings.jellyfin.hostname !== '' && body.hostname) {
|
||||
} else if (settings.jellyfin.ip !== '' && body.hostname) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ error: 'Jellyfin hostname already configured' });
|
||||
} else if (settings.jellyfin.hostname === '' && !body.hostname) {
|
||||
} else if (settings.jellyfin.ip === '' && !body.hostname) {
|
||||
return res.status(500).json({ error: 'No hostname provided.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const hostname =
|
||||
settings.jellyfin.hostname !== ''
|
||||
? settings.jellyfin.hostname
|
||||
: body.hostname ?? '';
|
||||
settings.jellyfin.ip !== ''
|
||||
? getHostname()
|
||||
: getHostname({
|
||||
useSsl: body.useSsl,
|
||||
ip: body.hostname,
|
||||
port: body.port,
|
||||
urlBase: body.urlBase,
|
||||
});
|
||||
|
||||
const { externalHostname } = getSettings().jellyfin;
|
||||
|
||||
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
||||
@@ -258,18 +271,31 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
'base64'
|
||||
);
|
||||
}
|
||||
|
||||
// First we need to attempt to log the user in to jellyfin
|
||||
const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
|
||||
let jellyfinHost =
|
||||
const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId);
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
const ip = req.ip;
|
||||
let clientIp;
|
||||
|
||||
if (ip) {
|
||||
if (net.isIPv4(ip)) {
|
||||
clientIp = ip;
|
||||
} else if (net.isIPv6(ip)) {
|
||||
clientIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||
}
|
||||
}
|
||||
|
||||
const account = await jellyfinserver.login(
|
||||
body.username,
|
||||
body.password,
|
||||
clientIp
|
||||
);
|
||||
|
||||
const account = await jellyfinserver.login(body.username, body.password);
|
||||
// Next let's see if the user already exists
|
||||
user = await userRepository.findOne({
|
||||
where: { jellyfinUserId: account.User.Id },
|
||||
@@ -278,7 +304,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,15 +332,21 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
|
||||
settings.jellyfin.hostname = body.hostname ?? '';
|
||||
const serverName = await jellyfinserver.getServerName();
|
||||
|
||||
settings.jellyfin.name = serverName;
|
||||
settings.jellyfin.serverId = account.User.ServerId;
|
||||
settings.jellyfin.ip = body.hostname ?? '';
|
||||
settings.jellyfin.port = body.port ?? 8096;
|
||||
settings.jellyfin.urlBase = body.urlBase ?? '';
|
||||
settings.jellyfin.useSsl = body.useSsl ?? false;
|
||||
settings.save();
|
||||
startJobs();
|
||||
|
||||
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 +444,68 @@ 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: getHostname({
|
||||
useSsl: body.useSsl,
|
||||
ip: body.hostname,
|
||||
port: body.port,
|
||||
urlBase: body.urlBase,
|
||||
}),
|
||||
}
|
||||
);
|
||||
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.',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import JellyfinAPI from '@server/api/jellyfin';
|
||||
import PlexAPI from '@server/api/plexapi';
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import TautulliAPI from '@server/api/tautulli';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { MediaRequest } from '@server/entity/MediaRequest';
|
||||
@@ -24,8 +25,10 @@ import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import discoverSettingRoutes from '@server/routes/settings/discover';
|
||||
import { ApiError } from '@server/types/error';
|
||||
import { appDataPath } from '@server/utils/appDataVolume';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import fs from 'fs';
|
||||
@@ -252,11 +255,59 @@ settingsRoutes.get('/jellyfin', (_req, res) => {
|
||||
res.status(200).json(settings.jellyfin);
|
||||
});
|
||||
|
||||
settingsRoutes.post('/jellyfin', (req, res) => {
|
||||
settingsRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
const userRepository = getRepository(User);
|
||||
const settings = getSettings();
|
||||
|
||||
settings.jellyfin = merge(settings.jellyfin, req.body);
|
||||
settings.save();
|
||||
try {
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
where: { id: 1 },
|
||||
select: ['id', 'jellyfinAuthToken', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
|
||||
const tempJellyfinSettings = { ...settings.jellyfin, ...req.body };
|
||||
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
getHostname(tempJellyfinSettings),
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
|
||||
const result = await jellyfinClient.getSystemInfo();
|
||||
|
||||
if (!result?.Id) {
|
||||
throw new ApiError(result?.status, ApiErrorCode.InvalidUrl);
|
||||
}
|
||||
|
||||
Object.assign(settings.jellyfin, req.body);
|
||||
settings.jellyfin.serverId = result.Id;
|
||||
settings.jellyfin.name = result.ServerName;
|
||||
settings.save();
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
logger.error('Something went wrong testing Jellyfin connection', {
|
||||
label: 'API',
|
||||
status: e.statusCode,
|
||||
errorMessage: ApiErrorCode.InvalidUrl,
|
||||
});
|
||||
|
||||
return next({
|
||||
status: e.statusCode,
|
||||
message: ApiErrorCode.InvalidUrl,
|
||||
});
|
||||
} else {
|
||||
logger.error('Something went wrong', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
|
||||
return next({
|
||||
status: e.statusCode ?? 500,
|
||||
message: ApiErrorCode.Unknown,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json(settings.jellyfin);
|
||||
});
|
||||
@@ -272,7 +323,7 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
getHostname(),
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
@@ -288,10 +339,13 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
||||
|
||||
// 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: 501,
|
||||
message: ApiErrorCode.SyncErrorGroupedFolders,
|
||||
});
|
||||
}
|
||||
|
||||
return next({ status: 404, message: 'SYNC_ERROR_NO_LIBRARIES' });
|
||||
return next({ status: 404, message: ApiErrorCode.SyncErrorNoLibraries });
|
||||
}
|
||||
|
||||
const newLibraries: Library[] = libraries.map((library) => {
|
||||
@@ -322,16 +376,12 @@ settingsRoutes.get('/jellyfin/library', async (req, res, next) => {
|
||||
});
|
||||
|
||||
settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
const settings = getSettings();
|
||||
const { hostname, externalHostname } = getSettings().jellyfin;
|
||||
let jellyfinHost =
|
||||
const { externalHostname } = getSettings().jellyfin;
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
: getHostname();
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId', 'jellyfinUserId'],
|
||||
@@ -339,7 +389,6 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
|
||||
@@ -275,7 +275,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
||||
...webhookSettings.options,
|
||||
jsonPayload: JSON.parse(
|
||||
Buffer.from(webhookSettings.options.jsonPayload, 'base64').toString(
|
||||
'ascii'
|
||||
'utf8'
|
||||
)
|
||||
),
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ import { hasPermission, Permission } from '@server/lib/permissions';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { findIndex, sortBy } from 'lodash';
|
||||
@@ -496,7 +497,6 @@ router.post(
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const jellyfinClient = new JellyfinAPI(
|
||||
settings.jellyfin.hostname ?? '',
|
||||
admin.jellyfinAuthToken ?? '',
|
||||
admin.jellyfinDeviceId ?? ''
|
||||
);
|
||||
@@ -504,15 +504,14 @@ router.post(
|
||||
|
||||
//const jellyfinUsersResponse = await jellyfinClient.getUsers();
|
||||
const createdUsers: User[] = [];
|
||||
const { hostname, externalHostname } = getSettings().jellyfin;
|
||||
let jellyfinHost =
|
||||
const { externalHostname } = getSettings().jellyfin;
|
||||
const hostname = getHostname();
|
||||
|
||||
const jellyfinHost =
|
||||
externalHostname && externalHostname.length > 0
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
const jellyfinUsers = await jellyfinClient.getUsers();
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
18
server/utils/getHostname.ts
Normal file
18
server/utils/getHostname.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
|
||||
interface HostnameParams {
|
||||
useSsl?: boolean;
|
||||
ip?: string;
|
||||
port?: number;
|
||||
urlBase?: string;
|
||||
}
|
||||
|
||||
export const getHostname = (params?: HostnameParams): string => {
|
||||
const settings = params ? params : getSettings().jellyfin;
|
||||
|
||||
const { useSsl, ip, port, urlBase } = settings;
|
||||
|
||||
const hostname = `${useSsl ? 'https' : 'http'}://${ip}:${port}${urlBase}`;
|
||||
|
||||
return hostname;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -13,7 +14,10 @@ import * as Yup from 'yup';
|
||||
const messages = defineMessages({
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
host: '{mediaServerName} URL',
|
||||
hostname: '{mediaServerName} URL',
|
||||
port: 'Port',
|
||||
enablessl: 'Use SSL',
|
||||
urlBase: 'URL Base',
|
||||
email: 'Email',
|
||||
emailtooltip:
|
||||
'Address does not need to be associated with your {mediaServerName} instance.',
|
||||
@@ -23,9 +27,15 @@ const messages = defineMessages({
|
||||
validationemailformat: 'Valid email required',
|
||||
validationusernamerequired: 'Username required',
|
||||
validationpasswordrequired: 'Password required',
|
||||
validationHostnameRequired: 'You must provide a valid hostname or IP address',
|
||||
validationPortRequired: 'You must provide a valid port number',
|
||||
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
validationUrlBaseLeadingSlash: 'URL base must have a leading slash',
|
||||
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
|
||||
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…',
|
||||
@@ -49,16 +59,23 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
|
||||
if (initial) {
|
||||
const LoginSchema = Yup.object().shape({
|
||||
host: Yup.string()
|
||||
hostname: Yup.string().required(
|
||||
intl.formatMessage(messages.validationhostrequired, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||
})
|
||||
),
|
||||
port: Yup.number().required(
|
||||
intl.formatMessage(messages.validationPortRequired)
|
||||
),
|
||||
urlBase: Yup.string()
|
||||
.matches(
|
||||
/^(?:(?:(?:https?):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/,
|
||||
intl.formatMessage(messages.validationhostformat)
|
||||
/^(\/[^/].*[^/]$)/,
|
||||
intl.formatMessage(messages.validationUrlBaseLeadingSlash)
|
||||
)
|
||||
.required(
|
||||
intl.formatMessage(messages.validationhostrequired, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||
})
|
||||
.matches(
|
||||
/^(.*[^/])$/,
|
||||
intl.formatMessage(messages.validationUrlBaseTrailingSlash)
|
||||
),
|
||||
email: Yup.string()
|
||||
.email(intl.formatMessage(messages.validationemailformat))
|
||||
@@ -73,12 +90,16 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
username: '',
|
||||
password: '',
|
||||
host: '',
|
||||
hostname: '',
|
||||
port: 8096,
|
||||
useSsl: false,
|
||||
urlBase: '',
|
||||
email: '',
|
||||
}}
|
||||
validationSchema={LoginSchema}
|
||||
@@ -87,18 +108,31 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
await axios.post('/api/v1/auth/jellyfin', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
hostname: values.host,
|
||||
hostname: values.hostname,
|
||||
port: values.port,
|
||||
useSsl: values.useSsl,
|
||||
urlBase: values.urlBase,
|
||||
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',
|
||||
@@ -109,32 +143,100 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, isSubmitting, isValid }) => (
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
values,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
}) => (
|
||||
<Form>
|
||||
<div className="sm:border-t sm:border-gray-800">
|
||||
<label htmlFor="host" className="text-label">
|
||||
{intl.formatMessage(messages.host, mediaServerFormatValues)}
|
||||
<div className="flex flex-col sm:flex-row sm:gap-4">
|
||||
<div className="w-full">
|
||||
<label htmlFor="hostname" className="text-label">
|
||||
{intl.formatMessage(
|
||||
messages.hostname,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
</label>
|
||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mb-0 sm:mt-0">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
|
||||
{values.useSsl ? 'https://' : 'http://'}
|
||||
</span>
|
||||
<Field
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
type="text"
|
||||
className="rounded-r-only flex-1"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.hostname,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{errors.hostname && touched.hostname && (
|
||||
<div className="error">{errors.hostname}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label htmlFor="port" className="text-label">
|
||||
{intl.formatMessage(messages.port)}
|
||||
</label>
|
||||
<div className="mt-1 sm:mt-0">
|
||||
<Field
|
||||
id="port"
|
||||
name="port"
|
||||
inputMode="numeric"
|
||||
type="text"
|
||||
className="short flex-1"
|
||||
placeholder={intl.formatMessage(messages.port)}
|
||||
/>
|
||||
{errors.port && touched.port && (
|
||||
<div className="error">{errors.port}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label htmlFor="useSsl" className="text-label mt-2">
|
||||
{intl.formatMessage(messages.enablessl)}
|
||||
</label>
|
||||
<div className="mt-1 mb-2 sm:col-span-2">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<Field
|
||||
id="useSsl"
|
||||
name="useSsl"
|
||||
type="checkbox"
|
||||
onChange={() => {
|
||||
setFieldValue('useSsl', !values.useSsl);
|
||||
setFieldValue('port', values.useSsl ? 8096 : 443);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label htmlFor="urlBase" className="text-label mt-1">
|
||||
{intl.formatMessage(messages.urlBase)}
|
||||
</label>
|
||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<Field
|
||||
id="host"
|
||||
name="host"
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.host,
|
||||
mediaServerFormatValues
|
||||
)}
|
||||
inputMode="url"
|
||||
id="urlBase"
|
||||
name="urlBase"
|
||||
placeholder={intl.formatMessage(messages.urlBase)}
|
||||
/>
|
||||
</div>
|
||||
{errors.host && touched.host && (
|
||||
<div className="error">{errors.host}</div>
|
||||
{errors.urlBase && touched.urlBase && (
|
||||
<div className="error">{errors.urlBase}</div>
|
||||
)}
|
||||
</div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="text-label"
|
||||
style={{ display: 'inline-flex' }}
|
||||
className="text-label inline-flex gap-1 align-middle"
|
||||
>
|
||||
{intl.formatMessage(messages.email)}
|
||||
<span className="label-tip">
|
||||
@@ -150,7 +252,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
||||
</Tooltip>
|
||||
</span>
|
||||
</label>
|
||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
||||
<div className="mt-1 sm:col-span-2 sm:mb-2 sm:mt-0">
|
||||
<div className="flex rounded-md shadow-sm">
|
||||
<Field
|
||||
id="email"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -4,6 +4,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||
import LibraryItem from '@app/components/Settings/LibraryItem';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
||||
import { ApiErrorCode } from '@server/constants/error';
|
||||
import type { JellyfinSettings } from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import { Field, Formik } from 'formik';
|
||||
@@ -32,14 +33,17 @@ const messages = defineMessages({
|
||||
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.',
|
||||
externalUrl: 'External URL',
|
||||
internalUrl: 'Internal URL',
|
||||
hostname: 'Hostname or IP Address',
|
||||
port: 'Port',
|
||||
enablessl: 'Use SSL',
|
||||
urlBase: 'URL Base',
|
||||
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',
|
||||
invalidurlerror: 'Unable to connect to {mediaServerName} server.',
|
||||
syncing: 'Syncing',
|
||||
syncJellyfin: 'Sync Libraries',
|
||||
manualscanJellyfin: 'Manual Library Scan',
|
||||
@@ -50,6 +54,12 @@ const messages = defineMessages({
|
||||
librariesRemaining: 'Libraries Remaining: {count}',
|
||||
startscan: 'Start Scan',
|
||||
cancelscan: 'Cancel Scan',
|
||||
validationUrl: 'You must provide a valid URL',
|
||||
validationHostnameRequired: 'You must provide a valid hostname or IP address',
|
||||
validationPortRequired: 'You must provide a valid port number',
|
||||
validationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
validationUrlBaseLeadingSlash: 'URL base must have a leading slash',
|
||||
validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash',
|
||||
});
|
||||
|
||||
interface Library {
|
||||
@@ -65,6 +75,7 @@ interface SyncStatus {
|
||||
currentLibrary?: Library;
|
||||
libraries: Library[];
|
||||
}
|
||||
|
||||
interface SettingsJellyfinProps {
|
||||
showAdvancedSettings?: boolean;
|
||||
onComplete?: () => void;
|
||||
@@ -93,18 +104,50 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
const JellyfinSettingsSchema = Yup.object().shape({
|
||||
jellyfinExternalUrl: Yup.string().matches(
|
||||
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
|
||||
intl.formatMessage(messages.validationUrl)
|
||||
),
|
||||
jellyfinInternalUrl: Yup.string().matches(
|
||||
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
|
||||
intl.formatMessage(messages.validationUrl)
|
||||
),
|
||||
jellyfinForgotPasswordUrl: Yup.string().matches(
|
||||
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
|
||||
intl.formatMessage(messages.validationUrl)
|
||||
),
|
||||
hostname: Yup.string()
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationHostnameRequired))
|
||||
.matches(
|
||||
/^(((([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])):((([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))@)?(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i,
|
||||
intl.formatMessage(messages.validationHostnameRequired)
|
||||
),
|
||||
port: Yup.number().when(['hostname'], {
|
||||
is: (value: unknown) => !!value,
|
||||
then: Yup.number()
|
||||
.typeError(intl.formatMessage(messages.validationPortRequired))
|
||||
.nullable()
|
||||
.required(intl.formatMessage(messages.validationPortRequired)),
|
||||
otherwise: Yup.number()
|
||||
.typeError(intl.formatMessage(messages.validationPortRequired))
|
||||
.nullable(),
|
||||
}),
|
||||
urlBase: Yup.string()
|
||||
.test(
|
||||
'leading-slash',
|
||||
intl.formatMessage(messages.validationUrlBaseLeadingSlash),
|
||||
(value) => !value || value.startsWith('/')
|
||||
)
|
||||
.test(
|
||||
'trailing-slash',
|
||||
intl.formatMessage(messages.validationUrlBaseTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
jellyfinExternalUrl: Yup.string()
|
||||
.nullable()
|
||||
.url(intl.formatMessage(messages.validationUrl))
|
||||
.test(
|
||||
'no-trailing-slash',
|
||||
intl.formatMessage(messages.validationUrlTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
jellyfinForgotPasswordUrl: Yup.string()
|
||||
.nullable()
|
||||
.url(intl.formatMessage(messages.validationUrl))
|
||||
.test(
|
||||
'no-trailing-slash',
|
||||
intl.formatMessage(messages.validationUrlTrailingSlash),
|
||||
(value) => !value || !value.endsWith('/')
|
||||
),
|
||||
});
|
||||
|
||||
const activeLibraries =
|
||||
@@ -394,7 +437,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
</div>
|
||||
<Formik
|
||||
initialValues={{
|
||||
jellyfinInternalUrl: data?.hostname || '',
|
||||
hostname: data?.ip,
|
||||
port: data?.port ?? 8096,
|
||||
useSsl: data?.useSsl,
|
||||
urlBase: data?.urlBase || '',
|
||||
jellyfinExternalUrl: data?.externalHostname || '',
|
||||
jellyfinForgotPasswordUrl: data?.jellyfinForgotPasswordUrl || '',
|
||||
}}
|
||||
@@ -402,7 +448,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await axios.post('/api/v1/settings/jellyfin', {
|
||||
hostname: values.jellyfinInternalUrl,
|
||||
ip: values.hostname,
|
||||
port: Number(values.port),
|
||||
useSsl: values.useSsl,
|
||||
urlBase: values.urlBase,
|
||||
externalHostname: values.jellyfinExternalUrl,
|
||||
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
|
||||
} as JellyfinSettings);
|
||||
@@ -420,44 +469,127 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
addToast(
|
||||
intl.formatMessage(messages.jellyfinSettingsFailure, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
if (e.response?.data?.message === ApiErrorCode.InvalidUrl) {
|
||||
addToast(
|
||||
intl.formatMessage(messages.invalidurlerror, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
addToast(
|
||||
intl.formatMessage(messages.jellyfinSettingsFailure, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
}),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
revalidate();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ errors, touched, handleSubmit, isSubmitting, isValid }) => {
|
||||
{({
|
||||
errors,
|
||||
touched,
|
||||
values,
|
||||
setFieldValue,
|
||||
handleSubmit,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
}) => {
|
||||
return (
|
||||
<form className="section" onSubmit={handleSubmit}>
|
||||
<div className="form-row">
|
||||
<label htmlFor="jellyfinInternalUrl" className="text-label">
|
||||
{intl.formatMessage(messages.internalUrl)}
|
||||
<label htmlFor="hostname" className="text-label">
|
||||
{intl.formatMessage(messages.hostname)}
|
||||
<span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-gray-100 sm:text-sm">
|
||||
{values.useSsl ? 'https://' : 'http://'}
|
||||
</span>
|
||||
<Field
|
||||
type="text"
|
||||
inputMode="url"
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
className="rounded-r-only"
|
||||
/>
|
||||
</div>
|
||||
{errors.hostname &&
|
||||
touched.hostname &&
|
||||
typeof errors.hostname === 'string' && (
|
||||
<div className="error">{errors.hostname}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="port" className="text-label">
|
||||
{intl.formatMessage(messages.port)}
|
||||
<span className="label-required">*</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
id="port"
|
||||
name="port"
|
||||
className="short"
|
||||
/>
|
||||
{errors.port &&
|
||||
touched.port &&
|
||||
typeof errors.port === 'string' && (
|
||||
<div className="error">{errors.port}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="useSsl" className="checkbox-label">
|
||||
{intl.formatMessage(messages.enablessl)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="useSsl"
|
||||
name="useSsl"
|
||||
onChange={() => {
|
||||
setFieldValue('useSsl', !values.useSsl);
|
||||
setFieldValue('port', values.useSsl ? 8096 : 443);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="urlBase" className="text-label">
|
||||
{intl.formatMessage(messages.urlBase)}
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
type="text"
|
||||
inputMode="url"
|
||||
id="jellyfinInternalUrl"
|
||||
name="jellyfinInternalUrl"
|
||||
id="urlBase"
|
||||
name="urlBase"
|
||||
/>
|
||||
</div>
|
||||
{errors.jellyfinInternalUrl &&
|
||||
touched.jellyfinInternalUrl && (
|
||||
<div className="error">
|
||||
{errors.jellyfinInternalUrl}
|
||||
</div>
|
||||
{errors.urlBase &&
|
||||
touched.urlBase &&
|
||||
typeof errors.urlBase === 'string' && (
|
||||
<div className="error">{errors.urlBase}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,9 @@ const messages = defineMessages({
|
||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||
locale: 'Display Language',
|
||||
overwriteDnsServers: 'Overwrite Default DNS Servers',
|
||||
overwriteDnsServersTip:
|
||||
'A comma-separated list of DNS server to use instead of the default ones',
|
||||
});
|
||||
|
||||
const SettingsMain = () => {
|
||||
@@ -134,6 +137,7 @@ const SettingsMain = () => {
|
||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||
trustProxy: data?.trustProxy,
|
||||
cacheImages: data?.cacheImages,
|
||||
overwriteDnsServers: data?.overwriteDnsServers,
|
||||
}}
|
||||
enableReinitialize
|
||||
validationSchema={MainSettingsSchema}
|
||||
@@ -150,6 +154,7 @@ const SettingsMain = () => {
|
||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||
trustProxy: values.trustProxy,
|
||||
cacheImages: values.cacheImages,
|
||||
overwriteDnsServers: values.overwriteDnsServers,
|
||||
});
|
||||
mutate('/api/v1/settings/public');
|
||||
mutate('/api/v1/status');
|
||||
@@ -427,6 +432,37 @@ const SettingsMain = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label
|
||||
htmlFor="overwriteDnsServers"
|
||||
className="checkbox-label"
|
||||
>
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.overwriteDnsServers)}
|
||||
</span>
|
||||
<SettingsBadge badgeType="advanced" className="mr-2" />
|
||||
<SettingsBadge badgeType="restartRequired" />
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.overwriteDnsServersTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
id="overwriteDnsServers"
|
||||
name="overwriteDnsServers"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
{errors.overwriteDnsServers &&
|
||||
touched.overwriteDnsServers &&
|
||||
typeof errors.overwriteDnsServers === 'string' && (
|
||||
<div className="error">
|
||||
{errors.overwriteDnsServers}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className="flex justify-end">
|
||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import Modal from '@app/components/Common/Modal';
|
||||
import SensitiveInput from '@app/components/Common/SensitiveInput';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { type SonarrSettings } from '@server/lib/settings';
|
||||
import type { SonarrSettings } from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import { Field, Formik } from 'formik';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
@@ -111,7 +109,6 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
const { addToast } = useToasts();
|
||||
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const settings = useSettings();
|
||||
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||
profiles: [],
|
||||
rootFolders: [],
|
||||
@@ -258,9 +255,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
animeTags: sonarr?.animeTags ?? [],
|
||||
isDefault: sonarr?.isDefault ?? false,
|
||||
is4k: sonarr?.is4k ?? false,
|
||||
enableSeasonFolders:
|
||||
sonarr?.enableSeasonFolders ??
|
||||
settings.currentSettings.mediaServerType !== MediaServerType.PLEX,
|
||||
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
|
||||
externalUrl: sonarr?.externalUrl,
|
||||
syncEnabled: sonarr?.syncEnabled ?? false,
|
||||
enableSearch: !sonarr?.preventSearch,
|
||||
@@ -966,24 +961,11 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
||||
>
|
||||
{intl.formatMessage(messages.seasonfolders)}
|
||||
</label>
|
||||
<div
|
||||
className={`form-input-area ${
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.JELLYFIN ||
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
? 'opacity-50'
|
||||
: 'opacity-100'
|
||||
}`}
|
||||
>
|
||||
<div className="form-input-area">
|
||||
<Field
|
||||
type="checkbox"
|
||||
id="enableSeasonFolders"
|
||||
name="enableSeasonFolders"
|
||||
disabled={
|
||||
settings.currentSettings.mediaServerType !==
|
||||
MediaServerType.PLEX
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,17 +219,20 @@
|
||||
"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.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.",
|
||||
"components.Login.enablessl": "Use SSL",
|
||||
"components.Login.forgotpassword": "Forgot Password?",
|
||||
"components.Login.host": "{mediaServerName} URL",
|
||||
"components.Login.hostname": "{mediaServerName} URL",
|
||||
"components.Login.initialsignin": "Connect",
|
||||
"components.Login.initialsigningin": "Connecting…",
|
||||
"components.Login.invalidurlerror": "Unable to connect to {mediaServerName} server.",
|
||||
"components.Login.loginerror": "Something went wrong while trying to sign in.",
|
||||
"components.Login.password": "Password",
|
||||
"components.Login.port": "Port",
|
||||
"components.Login.save": "Add",
|
||||
"components.Login.saving": "Adding…",
|
||||
"components.Login.signin": "Sign In",
|
||||
@@ -239,9 +242,15 @@
|
||||
"components.Login.signinwithoverseerr": "Use your {applicationTitle} account",
|
||||
"components.Login.signinwithplex": "Use your Plex account",
|
||||
"components.Login.title": "Add Email",
|
||||
"components.Login.urlBase": "URL Base",
|
||||
"components.Login.username": "Username",
|
||||
"components.Login.validationEmailFormat": "Invalid email",
|
||||
"components.Login.validationEmailRequired": "You must provide an email",
|
||||
"components.Login.validationHostnameRequired": "You must provide a valid hostname or IP address",
|
||||
"components.Login.validationPortRequired": "You must provide a valid port number",
|
||||
"components.Login.validationUrlBaseLeadingSlash": "URL base must have a leading slash",
|
||||
"components.Login.validationUrlBaseTrailingSlash": "URL base must not end in a trailing slash",
|
||||
"components.Login.validationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||
"components.Login.validationemailformat": "Valid email required",
|
||||
"components.Login.validationemailrequired": "You must provide a valid email address",
|
||||
"components.Login.validationhostformat": "Valid URL required",
|
||||
@@ -752,8 +761,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",
|
||||
@@ -936,19 +945,20 @@
|
||||
"components.Settings.experimentalTooltip": "Enabling this setting may result in unexpected application behavior",
|
||||
"components.Settings.externalUrl": "External URL",
|
||||
"components.Settings.hostname": "Hostname or IP Address",
|
||||
"components.Settings.internalUrl": "Internal URL",
|
||||
"components.Settings.invalidurlerror": "Unable to connect to {mediaServerName} server.",
|
||||
"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 +1185,8 @@
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.user": "User",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationDiscordId": "You must provide a valid Discord user ID",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailformat": "Valid email required",
|
||||
"components.UserProfile.UserSettings.UserGeneralSettings.validationemailrequired": "Email required",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <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