Compare commits
62 Commits
preview-pr
...
v1.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5712e19804 | ||
|
|
4b549763e5 | ||
|
|
da2d8fe35b | ||
|
|
4f89286fa8 | ||
|
|
4740476c9a | ||
|
|
c167d3ac38 | ||
|
|
55baca57c1 | ||
|
|
0b797964a8 | ||
|
|
030cbc535a | ||
|
|
b0fd0f59c4 | ||
|
|
47287c3688 | ||
|
|
cc041b5e0a | ||
|
|
b4c74de7b3 | ||
|
|
9daceb7017 | ||
|
|
ff7f9725f8 | ||
|
|
cd7930eef9 | ||
|
|
24d94ef6fd | ||
|
|
04fbd00d4a | ||
|
|
33ec4436fb | ||
|
|
e848386d10 | ||
|
|
235cee1d28 | ||
|
|
8d4943997e | ||
|
|
2ab814574c | ||
|
|
c6b2dd3728 | ||
|
|
825fa75ee2 | ||
|
|
21231186d1 | ||
|
|
48f76662d5 | ||
|
|
4920670495 | ||
|
|
0a30cd356d | ||
|
|
1fe4bb8a04 | ||
|
|
21c1bbec90 | ||
|
|
ad69d6715e | ||
|
|
46cd4d01d9 | ||
|
|
672061cd64 | ||
|
|
df332cec84 | ||
|
|
d7fa35e066 | ||
|
|
f33eb862fd | ||
|
|
0a007ca805 | ||
|
|
24f268b6cb | ||
|
|
b8e3c07c47 | ||
|
|
aa84977680 | ||
|
|
e051b1dfea | ||
|
|
c27f96096a | ||
|
|
4bd87647d0 | ||
|
|
c1e10338c1 | ||
|
|
cd1cacad55 | ||
|
|
ac77b037d5 | ||
|
|
10eb69a7dc | ||
|
|
70b1540ae2 | ||
|
|
7522aa3174 | ||
|
|
77a33cb74d | ||
|
|
24151d27f7 | ||
|
|
f3cc8cba0a | ||
|
|
9637c3f4ab | ||
|
|
a15c85cbd1 | ||
|
|
a4d07f5afa | ||
|
|
f5191aded6 | ||
|
|
2520d8f739 | ||
|
|
57e7d68092 | ||
|
|
d3622f7bb3 | ||
|
|
20c821e2eb | ||
|
|
7b82ced5e6 |
@@ -809,6 +809,69 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Alexays",
|
||||||
|
"name": "Alex",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/13947260?v=4",
|
||||||
|
"profile": "https://arouillard.fr",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Zebebles",
|
||||||
|
"name": "Zeb Muller",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/11425451?v=4",
|
||||||
|
"profile": "https://github.com/Zebebles",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "SMores",
|
||||||
|
"name": "Shane Friedman",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5354254?v=4",
|
||||||
|
"profile": "http://smoores.dev",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "IzaacJ",
|
||||||
|
"name": "Izaac Brånn",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/711323?v=4",
|
||||||
|
"profile": "https://izaacj.me",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "SalmanTariq",
|
||||||
|
"name": "Salman Tariq",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/13284494?v=4",
|
||||||
|
"profile": "https://github.com/SalmanTariq",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "andrew-kennedy",
|
||||||
|
"name": "Andrew Kennedy",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/2387159?v=4",
|
||||||
|
"profile": "https://github.com/andrew-kennedy",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Fallenbagel",
|
||||||
|
"name": "Fallenbagel",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/98979876?v=4",
|
||||||
|
"profile": "https://github.com/Fallenbagel",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||||
@@ -818,5 +881,6 @@
|
|||||||
"repoType": "github",
|
"repoType": "github",
|
||||||
"repoHost": "https://github.com",
|
"repoHost": "https://github.com",
|
||||||
"skipCi": false,
|
"skipCi": false,
|
||||||
"commitConvention": "angular"
|
"commitConvention": "angular",
|
||||||
|
"commitType": "docs"
|
||||||
}
|
}
|
||||||
|
|||||||
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -1,7 +1,2 @@
|
|||||||
# Global code ownership
|
# Global code ownership
|
||||||
|
* @Fallenbagel
|
||||||
- @Fallenbagel
|
|
||||||
|
|
||||||
# i18n locale files
|
|
||||||
|
|
||||||
src/i18n/locale/ @Fallenbagel
|
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
build_and_push:
|
build_and_push:
|
||||||
name: Build & Publish Docker Images
|
name: Build & Publish Docker Images
|
||||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||||
runs-on: ubuntu-20.04
|
runs-on: self-hosted
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|||||||
2
.github/workflows/snap.yaml
vendored
2
.github/workflows/snap.yaml
vendored
@@ -41,6 +41,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Set Up QEMU
|
- name: Set Up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Configure Git
|
||||||
|
run: git config --add safe.directory /data/parts/jellyseerr/src
|
||||||
- name: Build Snap Package
|
- name: Build Snap Package
|
||||||
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||||
id: build
|
id: build
|
||||||
|
|||||||
136
CHANGELOG.md
136
CHANGELOG.md
@@ -1,3 +1,139 @@
|
|||||||
|
# [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)
|
# [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ describe('Discover', () => {
|
|||||||
|
|
||||||
cy.wait('@getWatchlist');
|
cy.wait('@getWatchlist');
|
||||||
|
|
||||||
const sliderHeader = cy.contains('.slider-header', 'Your Watchlist');
|
const sliderHeader = cy.contains('.slider-header', 'Watchlist');
|
||||||
|
|
||||||
sliderHeader.scrollIntoView();
|
sliderHeader.scrollIntoView();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ describe('Pull To Refresh', () => {
|
|||||||
url: '/api/v1/*',
|
url: '/api/v1/*',
|
||||||
}).as('apiCall');
|
}).as('apiCall');
|
||||||
|
|
||||||
cy.get('.searchbar').swipe('bottom', [190, 400]);
|
cy.get('.searchbar').swipe('bottom', [190, 500]);
|
||||||
|
|
||||||
cy.wait('@apiCall').then((interception) => {
|
cy.wait('@apiCall').then((interception) => {
|
||||||
assert.isNotNull(
|
assert.isNotNull(
|
||||||
|
|||||||
@@ -4505,6 +4505,16 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: number
|
type: number
|
||||||
example: 10
|
example: 10
|
||||||
|
- in: query
|
||||||
|
name: voteCountGte
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
example: 7
|
||||||
|
- in: query
|
||||||
|
name: voteCountLte
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
example: 10
|
||||||
- in: query
|
- in: query
|
||||||
name: watchRegion
|
name: watchRegion
|
||||||
schema:
|
schema:
|
||||||
@@ -4784,6 +4794,16 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: number
|
type: number
|
||||||
example: 10
|
example: 10
|
||||||
|
- in: query
|
||||||
|
name: voteCountGte
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
example: 7
|
||||||
|
- in: query
|
||||||
|
name: voteCountLte
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
example: 10
|
||||||
- in: query
|
- in: query
|
||||||
name: watchRegion
|
name: watchRegion
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
97
package.json
97
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jellyseerr",
|
"name": "jellyseerr",
|
||||||
"version": "0.1.0",
|
"version": "1.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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",
|
"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",
|
||||||
@@ -30,17 +30,17 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-displaynames": "6.2.3",
|
"@formatjs/intl-displaynames": "6.2.6",
|
||||||
"@formatjs/intl-locale": "3.0.11",
|
"@formatjs/intl-locale": "3.1.1",
|
||||||
"@formatjs/intl-pluralrules": "5.1.8",
|
"@formatjs/intl-pluralrules": "5.1.10",
|
||||||
"@formatjs/intl-utils": "3.8.4",
|
"@formatjs/intl-utils": "3.8.4",
|
||||||
"@headlessui/react": "1.7.7",
|
"@headlessui/react": "1.7.12",
|
||||||
"@heroicons/react": "2.0.13",
|
"@heroicons/react": "2.0.16",
|
||||||
"@supercharge/request-ip": "1.2.0",
|
"@supercharge/request-ip": "1.2.0",
|
||||||
"@svgr/webpack": "6.5.1",
|
"@svgr/webpack": "6.5.1",
|
||||||
"@tanem/react-nprogress": "5.0.22",
|
"@tanem/react-nprogress": "5.0.30",
|
||||||
"ace-builds": "1.14.0",
|
"ace-builds": "1.15.2",
|
||||||
"axios": "1.2.2",
|
"axios": "1.3.4",
|
||||||
"axios-rate-limit": "1.3.0",
|
"axios-rate-limit": "1.3.0",
|
||||||
"bcrypt": "5.1.0",
|
"bcrypt": "5.1.0",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"cookie-parser": "1.4.6",
|
"cookie-parser": "1.4.6",
|
||||||
"copy-to-clipboard": "3.3.3",
|
"copy-to-clipboard": "3.3.3",
|
||||||
"country-flag-icons": "1.5.5",
|
"country-flag-icons": "1.5.5",
|
||||||
"cronstrue": "2.21.0",
|
"cronstrue": "2.23.0",
|
||||||
"csurf": "1.11.0",
|
"csurf": "1.11.0",
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"dayjs": "1.11.7",
|
"dayjs": "1.11.7",
|
||||||
@@ -65,23 +65,22 @@
|
|||||||
"next": "12.3.4",
|
"next": "12.3.4",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-gyp": "9.3.1",
|
"node-gyp": "9.3.1",
|
||||||
"node-schedule": "2.1.0",
|
"node-schedule": "2.1.1",
|
||||||
"nodemailer": "6.8.0",
|
"nodemailer": "6.9.1",
|
||||||
"openpgp": "5.5.0",
|
"openpgp": "5.7.0",
|
||||||
"plex-api": "5.3.2",
|
"plex-api": "5.3.2",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
"pulltorefreshjs": "0.1.22",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-ace": "10.1.0",
|
"react-ace": "10.1.0",
|
||||||
"react-animate-height": "2.1.2",
|
"react-animate-height": "2.1.2",
|
||||||
"react-aria": "3.22.0",
|
"react-aria": "3.23.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-intersection-observer": "9.4.1",
|
"react-intersection-observer": "9.4.3",
|
||||||
"react-intl": "6.2.5",
|
"react-intl": "6.2.10",
|
||||||
"react-markdown": "8.0.4",
|
"react-markdown": "8.0.5",
|
||||||
"react-popper-tooltip": "4.4.2",
|
"react-popper-tooltip": "4.4.2",
|
||||||
"react-select": "5.7.0",
|
"react-select": "5.7.0",
|
||||||
"react-spring": "9.6.1",
|
"react-spring": "9.7.1",
|
||||||
"react-tailwindcss-datepicker-sct": "1.3.4",
|
"react-tailwindcss-datepicker-sct": "1.3.4",
|
||||||
"react-toast-notifications": "2.5.1",
|
"react-toast-notifications": "2.5.1",
|
||||||
"react-truncate-markup": "5.1.2",
|
"react-truncate-markup": "5.1.2",
|
||||||
@@ -90,42 +89,41 @@
|
|||||||
"secure-random-password": "0.2.3",
|
"secure-random-password": "0.2.3",
|
||||||
"semver": "7.3.8",
|
"semver": "7.3.8",
|
||||||
"sqlite3": "5.1.4",
|
"sqlite3": "5.1.4",
|
||||||
"swagger-ui-express": "4.6.0",
|
"swagger-ui-express": "4.6.2",
|
||||||
"swr": "2.0.0",
|
"swr": "2.0.4",
|
||||||
"typeorm": "0.3.11",
|
"typeorm": "0.3.12",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"winston": "3.8.2",
|
"winston": "3.8.2",
|
||||||
"winston-daily-rotate-file": "4.7.1",
|
"winston-daily-rotate-file": "4.7.1",
|
||||||
"xml2js": "0.4.23",
|
"xml2js": "0.4.23",
|
||||||
"yamljs": "0.3.0",
|
"yamljs": "0.3.0",
|
||||||
"yup": "0.32.11",
|
"yup": "0.32.11",
|
||||||
"zod": "3.20.2"
|
"zod": "3.20.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "7.20.7",
|
"@babel/cli": "7.21.0",
|
||||||
"@commitlint/cli": "17.4.0",
|
"@commitlint/cli": "17.4.4",
|
||||||
"@commitlint/config-conventional": "17.4.0",
|
"@commitlint/config-conventional": "17.4.4",
|
||||||
"@semantic-release/changelog": "6.0.2",
|
"@semantic-release/changelog": "6.0.2",
|
||||||
"@semantic-release/commit-analyzer": "9.0.2",
|
"@semantic-release/commit-analyzer": "9.0.2",
|
||||||
"@semantic-release/exec": "6.0.3",
|
"@semantic-release/exec": "6.0.3",
|
||||||
"@semantic-release/git": "10.0.1",
|
"@semantic-release/git": "10.0.1",
|
||||||
"@tailwindcss/aspect-ratio": "0.4.2",
|
"@tailwindcss/aspect-ratio": "0.4.2",
|
||||||
"@tailwindcss/forms": "0.5.3",
|
"@tailwindcss/forms": "0.5.3",
|
||||||
"@tailwindcss/typography": "0.5.8",
|
"@tailwindcss/typography": "0.5.9",
|
||||||
"@types/bcrypt": "5.0.0",
|
"@types/bcrypt": "5.0.0",
|
||||||
"@types/cookie-parser": "1.4.3",
|
"@types/cookie-parser": "1.4.3",
|
||||||
"@types/country-flag-icons": "1.2.0",
|
"@types/country-flag-icons": "1.2.0",
|
||||||
"@types/csurf": "1.11.2",
|
"@types/csurf": "1.11.2",
|
||||||
"@types/email-templates": "8.0.4",
|
"@types/email-templates": "8.0.4",
|
||||||
"@types/express": "4.17.15",
|
"@types/express": "4.17.17",
|
||||||
"@types/express-session": "1.17.5",
|
"@types/express-session": "1.17.6",
|
||||||
"@types/lodash": "4.14.191",
|
"@types/lodash": "4.14.191",
|
||||||
"@types/node": "17.0.36",
|
"@types/node": "17.0.36",
|
||||||
"@types/node-schedule": "2.1.0",
|
"@types/node-schedule": "2.1.0",
|
||||||
"@types/nodemailer": "6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/pulltorefreshjs": "0.1.5",
|
"@types/react": "18.0.28",
|
||||||
"@types/react": "18.0.26",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/react-dom": "18.0.10",
|
|
||||||
"@types/react-transition-group": "4.4.5",
|
"@types/react-transition-group": "4.4.5",
|
||||||
"@types/secure-random-password": "0.2.1",
|
"@types/secure-random-password": "0.2.1",
|
||||||
"@types/semver": "7.3.13",
|
"@types/semver": "7.3.13",
|
||||||
@@ -134,45 +132,46 @@
|
|||||||
"@types/xml2js": "0.4.11",
|
"@types/xml2js": "0.4.11",
|
||||||
"@types/yamljs": "0.2.31",
|
"@types/yamljs": "0.2.31",
|
||||||
"@types/yup": "0.29.14",
|
"@types/yup": "0.29.14",
|
||||||
"@typescript-eslint/eslint-plugin": "5.48.0",
|
"@typescript-eslint/eslint-plugin": "5.54.0",
|
||||||
"@typescript-eslint/parser": "5.48.0",
|
"@typescript-eslint/parser": "5.54.0",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.13",
|
||||||
"babel-plugin-react-intl": "8.2.25",
|
"babel-plugin-react-intl": "8.2.25",
|
||||||
"babel-plugin-react-intl-auto": "3.3.0",
|
"babel-plugin-react-intl-auto": "3.3.0",
|
||||||
"commitizen": "4.2.6",
|
"commitizen": "4.3.0",
|
||||||
"copyfiles": "2.4.1",
|
"copyfiles": "2.4.1",
|
||||||
"cy-mobile-commands": "0.3.0",
|
"cy-mobile-commands": "0.3.0",
|
||||||
"cypress": "12.3.0",
|
"cypress": "12.7.0",
|
||||||
"cz-conventional-changelog": "3.3.0",
|
"cz-conventional-changelog": "3.3.0",
|
||||||
"eslint": "8.31.0",
|
"eslint": "8.35.0",
|
||||||
"eslint-config-next": "12.3.4",
|
"eslint-config-next": "12.3.4",
|
||||||
"eslint-config-prettier": "8.6.0",
|
"eslint-config-prettier": "8.6.0",
|
||||||
"eslint-plugin-formatjs": "4.3.9",
|
"eslint-plugin-formatjs": "4.9.0",
|
||||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||||
"eslint-plugin-no-relative-import-paths": "1.5.2",
|
"eslint-plugin-no-relative-import-paths": "1.5.2",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-react": "7.31.11",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"extract-react-intl-messages": "4.1.1",
|
"extract-react-intl-messages": "4.1.1",
|
||||||
"husky": "8.0.3",
|
"husky": "8.0.3",
|
||||||
"lint-staged": "13.1.0",
|
"lint-staged": "13.1.2",
|
||||||
"nodemon": "2.0.20",
|
"nodemon": "2.0.20",
|
||||||
"postcss": "8.4.20",
|
"postcss": "8.4.21",
|
||||||
"prettier": "2.8.1",
|
"prettier": "2.8.4",
|
||||||
"prettier-plugin-organize-imports": "3.2.1",
|
"prettier-plugin-organize-imports": "3.2.2",
|
||||||
"prettier-plugin-tailwindcss": "0.2.1",
|
"prettier-plugin-tailwindcss": "0.2.3",
|
||||||
"semantic-release": "19.0.5",
|
"semantic-release": "19.0.5",
|
||||||
"semantic-release-docker-buildx": "1.0.1",
|
"semantic-release-docker-buildx": "1.0.1",
|
||||||
"tailwindcss": "3.2.4",
|
"tailwindcss": "3.2.7",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"tsc-alias": "1.8.2",
|
"tsc-alias": "1.8.2",
|
||||||
"tsconfig-paths": "4.1.2",
|
"tsconfig-paths": "4.1.2",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"sqlite3/node-gyp": "8.4.1",
|
"sqlite3/node-gyp": "8.4.1",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-dom": "18.0.10"
|
"@types/react-dom": "18.0.11",
|
||||||
|
"@types/express-session": "1.17.6"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface RTAlgoliaHit {
|
|||||||
title: string;
|
title: string;
|
||||||
titles: string[];
|
titles: string[];
|
||||||
description: string;
|
description: string;
|
||||||
releaseYear: string;
|
releaseYear: number;
|
||||||
rating: string;
|
rating: string;
|
||||||
genres: string[];
|
genres: string[];
|
||||||
updateDate: string;
|
updateDate: string;
|
||||||
@@ -111,22 +111,19 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
|
|
||||||
// First, attempt to match exact name and year
|
// First, attempt to match exact name and year
|
||||||
let movie = contentResults.hits.find(
|
let movie = contentResults.hits.find(
|
||||||
(movie) => movie.releaseYear === year.toString() && movie.title === name
|
(movie) => movie.releaseYear === year && movie.title === name
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we don't find a movie, try to match partial name and year
|
// If we don't find a movie, try to match partial name and year
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = contentResults.hits.find(
|
movie = contentResults.hits.find(
|
||||||
(movie) =>
|
(movie) => movie.releaseYear === year && movie.title.includes(name)
|
||||||
movie.releaseYear === year.toString() && movie.title.includes(name)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we still dont find a movie, try to match just on year
|
// If we still dont find a movie, try to match just on year
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
movie = contentResults.hits.find(
|
movie = contentResults.hits.find((movie) => movie.releaseYear === year);
|
||||||
(movie) => movie.releaseYear === year.toString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One last try, try exact name match only
|
// One last try, try exact name match only
|
||||||
@@ -181,7 +178,7 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
|
|
||||||
if (year) {
|
if (year) {
|
||||||
tvshow = contentResults.hits.find(
|
tvshow = contentResults.hits.find(
|
||||||
(series) => series.releaseYear === year.toString()
|
(series) => series.releaseYear === year
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ export interface SonarrSeries {
|
|||||||
ignoreEpisodesWithoutFiles?: boolean;
|
ignoreEpisodesWithoutFiles?: boolean;
|
||||||
searchForMissingEpisodes?: boolean;
|
searchForMissingEpisodes?: boolean;
|
||||||
};
|
};
|
||||||
|
statistics: {
|
||||||
|
seasonCount: number;
|
||||||
|
episodeFileCount: number;
|
||||||
|
episodeCount: number;
|
||||||
|
totalEpisodeCount: number;
|
||||||
|
sizeOnDisk: number;
|
||||||
|
releaseGroups: string[];
|
||||||
|
percentOfEpisodes: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddSeriesOptions {
|
export interface AddSeriesOptions {
|
||||||
@@ -116,6 +125,16 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSeriesById(id: number): Promise<SonarrSeries> {
|
||||||
|
try {
|
||||||
|
const response = await this.axios.get<SonarrSeries>(`/series/${id}`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ interface DiscoverMovieOptions {
|
|||||||
withRuntimeLte?: string;
|
withRuntimeLte?: string;
|
||||||
voteAverageGte?: string;
|
voteAverageGte?: string;
|
||||||
voteAverageLte?: string;
|
voteAverageLte?: string;
|
||||||
|
voteCountGte?: string;
|
||||||
|
voteCountLte?: string;
|
||||||
originalLanguage?: string;
|
originalLanguage?: string;
|
||||||
genre?: string;
|
genre?: string;
|
||||||
studio?: string;
|
studio?: string;
|
||||||
@@ -83,6 +85,8 @@ interface DiscoverTvOptions {
|
|||||||
withRuntimeLte?: string;
|
withRuntimeLte?: string;
|
||||||
voteAverageGte?: string;
|
voteAverageGte?: string;
|
||||||
voteAverageLte?: string;
|
voteAverageLte?: string;
|
||||||
|
voteCountGte?: string;
|
||||||
|
voteCountLte?: string;
|
||||||
includeEmptyReleaseDate?: boolean;
|
includeEmptyReleaseDate?: boolean;
|
||||||
originalLanguage?: string;
|
originalLanguage?: string;
|
||||||
genre?: string;
|
genre?: string;
|
||||||
@@ -460,6 +464,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
withRuntimeLte,
|
withRuntimeLte,
|
||||||
voteAverageGte,
|
voteAverageGte,
|
||||||
voteAverageLte,
|
voteAverageLte,
|
||||||
|
voteCountGte,
|
||||||
|
voteCountLte,
|
||||||
watchProviders,
|
watchProviders,
|
||||||
watchRegion,
|
watchRegion,
|
||||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||||
@@ -504,6 +510,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
'with_runtime.lte': withRuntimeLte,
|
'with_runtime.lte': withRuntimeLte,
|
||||||
'vote_average.gte': voteAverageGte,
|
'vote_average.gte': voteAverageGte,
|
||||||
'vote_average.lte': voteAverageLte,
|
'vote_average.lte': voteAverageLte,
|
||||||
|
'vote_count.gte': voteCountGte,
|
||||||
|
'vote_count.lte': voteCountLte,
|
||||||
watch_region: watchRegion,
|
watch_region: watchRegion,
|
||||||
with_watch_providers: watchProviders,
|
with_watch_providers: watchProviders,
|
||||||
},
|
},
|
||||||
@@ -530,6 +538,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
withRuntimeLte,
|
withRuntimeLte,
|
||||||
voteAverageGte,
|
voteAverageGte,
|
||||||
voteAverageLte,
|
voteAverageLte,
|
||||||
|
voteCountGte,
|
||||||
|
voteCountLte,
|
||||||
watchProviders,
|
watchProviders,
|
||||||
watchRegion,
|
watchRegion,
|
||||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||||
@@ -574,6 +584,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
'with_runtime.lte': withRuntimeLte,
|
'with_runtime.lte': withRuntimeLte,
|
||||||
'vote_average.gte': voteAverageGte,
|
'vote_average.gte': voteAverageGte,
|
||||||
'vote_average.lte': voteAverageLte,
|
'vote_average.lte': voteAverageLte,
|
||||||
|
'vote_count.gte': voteCountGte,
|
||||||
|
'vote_count.lte': voteCountLte,
|
||||||
with_watch_providers: watchProviders,
|
with_watch_providers: watchProviders,
|
||||||
watch_region: watchRegion,
|
watch_region: watchRegion,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ export interface TmdbTvResult extends TmdbMediaResult {
|
|||||||
first_air_date: string;
|
first_air_date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TmdbCollectionResult {
|
||||||
|
id: number;
|
||||||
|
media_type: 'collection';
|
||||||
|
title: string;
|
||||||
|
original_title: string;
|
||||||
|
adult: boolean;
|
||||||
|
poster_path?: string;
|
||||||
|
backdrop_path?: string;
|
||||||
|
overview: string;
|
||||||
|
original_language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TmdbPersonResult {
|
export interface TmdbPersonResult {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -45,7 +57,12 @@ interface TmdbPaginatedResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
||||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
|
results: (
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
|
)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export enum DiscoverSliderType {
|
|||||||
TMDB_SEARCH,
|
TMDB_SEARCH,
|
||||||
TMDB_STUDIO,
|
TMDB_STUDIO,
|
||||||
TMDB_NETWORK,
|
TMDB_NETWORK,
|
||||||
|
TMDB_MOVIE_STREAMING_SERVICES,
|
||||||
|
TMDB_TV_STREAMING_SERVICES,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSliders: Partial<DiscoverSlider>[] = [
|
export const defaultSliders: Partial<DiscoverSlider>[] = [
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ export class MediaRequest {
|
|||||||
|
|
||||||
let rootFolder = radarrSettings.activeDirectory;
|
let rootFolder = radarrSettings.activeDirectory;
|
||||||
let qualityProfile = radarrSettings.activeProfileId;
|
let qualityProfile = radarrSettings.activeProfileId;
|
||||||
let tags = radarrSettings.tags;
|
let tags = radarrSettings.tags ? [...radarrSettings.tags] : [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.rootFolder &&
|
this.rootFolder &&
|
||||||
@@ -764,6 +764,38 @@ export class MediaRequest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (radarrSettings.tagRequests) {
|
||||||
|
let userTag = (await radarr.getTags()).find((v) =>
|
||||||
|
v.label.startsWith(this.requestedBy.id + ' - ')
|
||||||
|
);
|
||||||
|
if (!userTag) {
|
||||||
|
logger.info(`Requester has no active tag. Creating new`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
userId: this.requestedBy.id,
|
||||||
|
newTag:
|
||||||
|
this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
userTag = await radarr.createTag({
|
||||||
|
label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (userTag.id) {
|
||||||
|
if (!tags?.find((v) => v === userTag?.id)) {
|
||||||
|
tags?.push(userTag.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`Requester has no tag and failed to add one`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
userId: this.requestedBy.id,
|
||||||
|
radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||||
) {
|
) {
|
||||||
@@ -970,7 +1002,11 @@ export class MediaRequest {
|
|||||||
let tags =
|
let tags =
|
||||||
seriesType === 'anime'
|
seriesType === 'anime'
|
||||||
? sonarrSettings.animeTags
|
? sonarrSettings.animeTags
|
||||||
: sonarrSettings.tags;
|
? [...sonarrSettings.animeTags]
|
||||||
|
: []
|
||||||
|
: sonarrSettings.tags
|
||||||
|
? [...sonarrSettings.tags]
|
||||||
|
: [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.rootFolder &&
|
this.rootFolder &&
|
||||||
@@ -1022,6 +1058,38 @@ export class MediaRequest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sonarrSettings.tagRequests) {
|
||||||
|
let userTag = (await sonarr.getTags()).find((v) =>
|
||||||
|
v.label.startsWith(this.requestedBy.id + ' - ')
|
||||||
|
);
|
||||||
|
if (!userTag) {
|
||||||
|
logger.info(`Requester has no active tag. Creating new`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
userId: this.requestedBy.id,
|
||||||
|
newTag:
|
||||||
|
this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
userTag = await sonarr.createTag({
|
||||||
|
label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (userTag.id) {
|
||||||
|
if (!tags?.find((v) => v === userTag?.id)) {
|
||||||
|
tags?.push(userTag.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`Requester has no tag and failed to add one`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
userId: this.requestedBy.id,
|
||||||
|
sonarrServer: sonarrSettings.hostname + ':' + sonarrSettings.port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sonarrSeriesOptions: AddSeriesOptions = {
|
const sonarrSeriesOptions: AddSeriesOptions = {
|
||||||
profileId: qualityProfile,
|
profileId: qualityProfile,
|
||||||
languageProfileId: languageProfile,
|
languageProfileId: languageProfile,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { PlexMetadata } from '@server/api/plexapi';
|
import type { PlexMetadata } from '@server/api/plexapi';
|
||||||
import PlexAPI from '@server/api/plexapi';
|
import PlexAPI from '@server/api/plexapi';
|
||||||
|
import type { RadarrMovie } from '@server/api/servarr/radarr';
|
||||||
import RadarrAPI from '@server/api/servarr/radarr';
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
import type { SonarrSeason } from '@server/api/servarr/sonarr';
|
import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr';
|
||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import { MediaStatus } from '@server/constants/media';
|
import { MediaStatus } from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
@@ -47,158 +48,150 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
|
for await (const media of this.loadAvailableMediaPaginated(pageSize)) {
|
||||||
try {
|
if (!this.running) {
|
||||||
if (!this.running) {
|
throw new Error('Job aborted');
|
||||||
throw new Error('Job aborted');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const mediaExists = await this.mediaExists(media);
|
const mediaExists = await this.mediaExists(media);
|
||||||
|
|
||||||
//We can not delete media so if both versions do not exist, we will change both columns to unknown or null
|
// We can not delete media so if both versions do not exist, we will change both columns to unknown or null
|
||||||
if (!mediaExists) {
|
if (!mediaExists) {
|
||||||
if (
|
if (
|
||||||
media.status !== MediaStatus.UNKNOWN ||
|
media.status !== MediaStatus.UNKNOWN ||
|
||||||
media.status4k !== MediaStatus.UNKNOWN
|
media.status4k !== MediaStatus.UNKNOWN
|
||||||
) {
|
) {
|
||||||
const request = await requestRepository.find({
|
const request = await requestRepository.find({
|
||||||
relations: {
|
relations: {
|
||||||
media: true,
|
media: true,
|
||||||
},
|
},
|
||||||
where: { media: { id: media.id } },
|
where: { media: { id: media.id } },
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`${
|
|
||||||
media.mediaType === 'tv' ? media.tvdbId : media.tmdbId
|
|
||||||
} does not exist in any of your media instances. We will change its status to unknown.`,
|
|
||||||
{ label: 'AvailabilitySync' }
|
|
||||||
);
|
|
||||||
|
|
||||||
await mediaRepository.update(media.id, {
|
|
||||||
status: MediaStatus.UNKNOWN,
|
|
||||||
status4k: MediaStatus.UNKNOWN,
|
|
||||||
serviceId: null,
|
|
||||||
serviceId4k: null,
|
|
||||||
externalServiceId: null,
|
|
||||||
externalServiceId4k: null,
|
|
||||||
externalServiceSlug: null,
|
|
||||||
externalServiceSlug4k: null,
|
|
||||||
ratingKey: null,
|
|
||||||
ratingKey4k: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
await requestRepository.remove(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.mediaType === 'tv') {
|
|
||||||
// ok, the show itself exists, but do all it's seasons?
|
|
||||||
const seasons = await seasonRepository.find({
|
|
||||||
where: [
|
|
||||||
{ status: MediaStatus.AVAILABLE, media: { id: media.id } },
|
|
||||||
{
|
|
||||||
status: MediaStatus.PARTIALLY_AVAILABLE,
|
|
||||||
media: { id: media.id },
|
|
||||||
},
|
|
||||||
{ status4k: MediaStatus.AVAILABLE, media: { id: media.id } },
|
|
||||||
{
|
|
||||||
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
|
||||||
media: { id: media.id },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let didDeleteSeasons = false;
|
logger.info(
|
||||||
for (const season of seasons) {
|
`Media ID ${media.id} does not exist in any of your media instances. Status will be changed to unknown.`,
|
||||||
if (
|
{ label: 'AvailabilitySync' }
|
||||||
!mediaExists &&
|
);
|
||||||
(season.status !== MediaStatus.UNKNOWN ||
|
|
||||||
season.status4k !== MediaStatus.UNKNOWN)
|
await mediaRepository.update(media.id, {
|
||||||
) {
|
status: MediaStatus.UNKNOWN,
|
||||||
await seasonRepository.update(
|
status4k: MediaStatus.UNKNOWN,
|
||||||
{ id: season.id },
|
serviceId: null,
|
||||||
|
serviceId4k: null,
|
||||||
|
externalServiceId: null,
|
||||||
|
externalServiceId4k: null,
|
||||||
|
externalServiceSlug: null,
|
||||||
|
externalServiceSlug4k: null,
|
||||||
|
ratingKey: null,
|
||||||
|
ratingKey4k: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestRepository.remove(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.mediaType === 'tv') {
|
||||||
|
// ok, the show itself exists, but do all it's seasons?
|
||||||
|
const seasons = await seasonRepository.find({
|
||||||
|
where: [
|
||||||
|
{ status: MediaStatus.AVAILABLE, media: { id: media.id } },
|
||||||
|
{
|
||||||
|
status: MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
|
media: { id: media.id },
|
||||||
|
},
|
||||||
|
{ status4k: MediaStatus.AVAILABLE, media: { id: media.id } },
|
||||||
|
{
|
||||||
|
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
|
media: { id: media.id },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let didDeleteSeasons = false;
|
||||||
|
for (const season of seasons) {
|
||||||
|
if (
|
||||||
|
!mediaExists &&
|
||||||
|
(season.status !== MediaStatus.UNKNOWN ||
|
||||||
|
season.status4k !== MediaStatus.UNKNOWN)
|
||||||
|
) {
|
||||||
|
await seasonRepository.update(
|
||||||
|
{ id: season.id },
|
||||||
|
{
|
||||||
|
status: MediaStatus.UNKNOWN,
|
||||||
|
status4k: MediaStatus.UNKNOWN,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const seasonExists = await this.seasonExists(media, season);
|
||||||
|
|
||||||
|
if (!seasonExists) {
|
||||||
|
logger.info(
|
||||||
|
`Removing season ${season.seasonNumber}, media ID ${media.id} because it does not exist in any of your media instances.`,
|
||||||
|
{ label: 'AvailabilitySync' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
season.status !== MediaStatus.UNKNOWN ||
|
||||||
|
season.status4k !== MediaStatus.UNKNOWN
|
||||||
|
) {
|
||||||
|
await seasonRepository.update(
|
||||||
|
{ id: season.id },
|
||||||
|
{
|
||||||
|
status: MediaStatus.UNKNOWN,
|
||||||
|
status4k: MediaStatus.UNKNOWN,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonToBeDeleted = await seasonRequestRepository.findOne(
|
||||||
{
|
{
|
||||||
status: MediaStatus.UNKNOWN,
|
relations: {
|
||||||
status4k: MediaStatus.UNKNOWN,
|
request: {
|
||||||
|
media: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
request: {
|
||||||
|
media: {
|
||||||
|
id: media.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seasonNumber: season.seasonNumber,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
const seasonExists = await this.seasonExists(media, season);
|
|
||||||
|
|
||||||
if (!seasonExists) {
|
if (seasonToBeDeleted) {
|
||||||
logger.info(
|
await seasonRequestRepository.remove(seasonToBeDeleted);
|
||||||
`Removing season ${season.seasonNumber}, media id: ${media.tvdbId} because it does not exist in any of your media instances.`,
|
|
||||||
{ label: 'AvailabilitySync' }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
season.status !== MediaStatus.UNKNOWN ||
|
|
||||||
season.status4k !== MediaStatus.UNKNOWN
|
|
||||||
) {
|
|
||||||
await seasonRepository.update(
|
|
||||||
{ id: season.id },
|
|
||||||
{
|
|
||||||
status: MediaStatus.UNKNOWN,
|
|
||||||
status4k: MediaStatus.UNKNOWN,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const seasonToBeDeleted =
|
|
||||||
await seasonRequestRepository.findOne({
|
|
||||||
relations: {
|
|
||||||
request: {
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
request: {
|
|
||||||
media: {
|
|
||||||
id: media.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
seasonNumber: season.seasonNumber,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (seasonToBeDeleted) {
|
|
||||||
await seasonRequestRepository.remove(seasonToBeDeleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
didDeleteSeasons = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
didDeleteSeasons = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (didDeleteSeasons) {
|
if (didDeleteSeasons) {
|
||||||
if (
|
if (
|
||||||
media.status === MediaStatus.AVAILABLE ||
|
media.status === MediaStatus.AVAILABLE ||
|
||||||
media.status4k === MediaStatus.AVAILABLE
|
media.status4k === MediaStatus.AVAILABLE
|
||||||
) {
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted some of its seasons.`,
|
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (media.status === MediaStatus.AVAILABLE) {
|
if (media.status === MediaStatus.AVAILABLE) {
|
||||||
await mediaRepository.update(media.id, {
|
await mediaRepository.update(media.id, {
|
||||||
status: MediaStatus.PARTIALLY_AVAILABLE,
|
status: MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.status4k === MediaStatus.AVAILABLE) {
|
if (media.status4k === MediaStatus.AVAILABLE) {
|
||||||
await mediaRepository.update(media.id, {
|
await mediaRepository.update(media.id, {
|
||||||
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
status4k: MediaStatus.PARTIALLY_AVAILABLE,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
|
||||||
logger.error('Failure with media.', {
|
|
||||||
errorMessage: ex.message,
|
|
||||||
label: 'AvailabilitySync',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -254,9 +247,9 @@ class AvailabilitySync {
|
|||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`${media.tmdbId} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
|
`Media ID ${media.id} does not exist in your ${is4k ? '4k' : 'non-4k'} ${
|
||||||
isTVType ? 'sonarr' : 'radarr'
|
isTVType ? 'Sonarr' : 'Radarr'
|
||||||
} and plex instance. We will change its status to unknown.`,
|
} and Plex instance. Status will be changed to unknown.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -306,46 +299,70 @@ class AvailabilitySync {
|
|||||||
apiKey: server.apiKey,
|
apiKey: server.apiKey,
|
||||||
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
||||||
});
|
});
|
||||||
const meta = await api.getMovieByTmdbId(media.tmdbId);
|
try {
|
||||||
|
// Check if both exist or if a single non-4k or 4k exists
|
||||||
|
// If both do not exist we will return false
|
||||||
|
|
||||||
//check if both exist or if a single non-4k or 4k exists
|
let meta: RadarrMovie | undefined;
|
||||||
//if both do not exist we will return false
|
|
||||||
if (!server.is4k && !meta.id) {
|
|
||||||
existsInRadarr = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (server.is4k && !meta.id) {
|
if (!server.is4k && media.externalServiceId) {
|
||||||
existsInRadarr4k = false;
|
meta = await api.getMovie({ id: media.externalServiceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && media.externalServiceId4k) {
|
||||||
|
meta = await api.getMovie({ id: media.externalServiceId4k });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server.is4k && (!meta || !meta.hasFile)) {
|
||||||
|
existsInRadarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && (!meta || !meta.hasFile)) {
|
||||||
|
existsInRadarr4k = false;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
logger.debug(
|
||||||
|
`Failure retrieving media ID ${media.id} from your ${
|
||||||
|
!server.is4k ? 'non-4K' : '4K'
|
||||||
|
} Radarr.`,
|
||||||
|
{
|
||||||
|
errorMessage: ex.message,
|
||||||
|
label: 'AvailabilitySync',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!server.is4k) {
|
||||||
|
existsInRadarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k) {
|
||||||
|
existsInRadarr4k = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInRadarr && existsInRadarr4k) {
|
// If only a single non-4k or 4k exists, then change entity columns accordingly
|
||||||
return true;
|
// Related media request will then be deleted
|
||||||
}
|
if (
|
||||||
|
!existsInRadarr &&
|
||||||
if (!existsInRadarr && existsInPlex) {
|
(existsInRadarr4k || existsInPlex4k) &&
|
||||||
return true;
|
!existsInPlex
|
||||||
}
|
) {
|
||||||
|
|
||||||
if (!existsInRadarr4k && existsInPlex4k) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//if only a single non-4k or 4k exists, then change entity columns accordingly
|
|
||||||
//related media request will then be deleted
|
|
||||||
if (!existsInRadarr && existsInRadarr4k && !existsInPlex) {
|
|
||||||
if (media.status !== MediaStatus.UNKNOWN) {
|
if (media.status !== MediaStatus.UNKNOWN) {
|
||||||
this.mediaUpdater(media, false);
|
this.mediaUpdater(media, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInRadarr && !existsInRadarr4k && !existsInPlex4k) {
|
if (
|
||||||
|
(existsInRadarr || existsInPlex) &&
|
||||||
|
!existsInRadarr4k &&
|
||||||
|
!existsInPlex4k
|
||||||
|
) {
|
||||||
if (media.status4k !== MediaStatus.UNKNOWN) {
|
if (media.status4k !== MediaStatus.UNKNOWN) {
|
||||||
this.mediaUpdater(media, true);
|
this.mediaUpdater(media, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInRadarr || existsInRadarr4k) {
|
if (existsInRadarr || existsInRadarr4k || existsInPlex || existsInPlex4k) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,10 +374,6 @@ class AvailabilitySync {
|
|||||||
existsInPlex: boolean,
|
existsInPlex: boolean,
|
||||||
existsInPlex4k: boolean
|
existsInPlex4k: boolean
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!media.tvdbId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let existsInSonarr = true;
|
let existsInSonarr = true;
|
||||||
let existsInSonarr4k = true;
|
let existsInSonarr4k = true;
|
||||||
|
|
||||||
@@ -369,49 +382,75 @@ class AvailabilitySync {
|
|||||||
apiKey: server.apiKey,
|
apiKey: server.apiKey,
|
||||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
// Check if both exist or if a single non-4k or 4k exists
|
||||||
|
// If both do not exist we will return false
|
||||||
|
|
||||||
const meta = await api.getSeriesByTvdbId(media.tvdbId);
|
let meta: SonarrSeries | undefined;
|
||||||
|
|
||||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = meta.seasons;
|
if (!server.is4k && media.externalServiceId) {
|
||||||
|
meta = await api.getSeriesById(media.externalServiceId);
|
||||||
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
|
||||||
|
meta.seasons;
|
||||||
|
}
|
||||||
|
|
||||||
//check if both exist or if a single non-4k or 4k exists
|
if (server.is4k && media.externalServiceId4k) {
|
||||||
//if both do not exist we will return false
|
meta = await api.getSeriesById(media.externalServiceId4k);
|
||||||
if (!server.is4k && !meta.id) {
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
|
||||||
existsInSonarr = false;
|
meta.seasons;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.is4k && !meta.id) {
|
if (!server.is4k && (!meta || meta.statistics.episodeFileCount === 0)) {
|
||||||
existsInSonarr4k = false;
|
existsInSonarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && (!meta || meta.statistics.episodeFileCount === 0)) {
|
||||||
|
existsInSonarr4k = false;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
logger.debug(
|
||||||
|
`Failure retrieving media ID ${media.id} from your ${
|
||||||
|
!server.is4k ? 'non-4K' : '4K'
|
||||||
|
} Sonarr.`,
|
||||||
|
{
|
||||||
|
errorMessage: ex.message,
|
||||||
|
label: 'AvailabilitySync',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server.is4k) {
|
||||||
|
existsInSonarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k) {
|
||||||
|
existsInSonarr4k = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInSonarr && existsInSonarr4k) {
|
// If only a single non-4k or 4k exists, then change entity columns accordingly
|
||||||
return true;
|
// Related media request will then be deleted
|
||||||
}
|
if (
|
||||||
|
!existsInSonarr &&
|
||||||
if (!existsInSonarr && existsInPlex) {
|
(existsInSonarr4k || existsInPlex4k) &&
|
||||||
return true;
|
!existsInPlex
|
||||||
}
|
) {
|
||||||
|
|
||||||
if (!existsInSonarr4k && existsInPlex4k) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//if only a single non-4k or 4k exists, then change entity columns accordingly
|
|
||||||
//related media request will then be deleted
|
|
||||||
if (!existsInSonarr && existsInSonarr4k && !existsInPlex) {
|
|
||||||
if (media.status !== MediaStatus.UNKNOWN) {
|
if (media.status !== MediaStatus.UNKNOWN) {
|
||||||
this.mediaUpdater(media, false);
|
this.mediaUpdater(media, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInSonarr && !existsInSonarr4k && !existsInPlex4k) {
|
if (
|
||||||
|
(existsInSonarr || existsInPlex) &&
|
||||||
|
!existsInSonarr4k &&
|
||||||
|
!existsInPlex4k
|
||||||
|
) {
|
||||||
if (media.status4k !== MediaStatus.UNKNOWN) {
|
if (media.status4k !== MediaStatus.UNKNOWN) {
|
||||||
this.mediaUpdater(media, true);
|
this.mediaUpdater(media, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsInSonarr || existsInSonarr4k) {
|
if (existsInSonarr || existsInSonarr4k || existsInPlex || existsInPlex4k) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,10 +463,6 @@ class AvailabilitySync {
|
|||||||
seasonExistsInPlex: boolean,
|
seasonExistsInPlex: boolean,
|
||||||
seasonExistsInPlex4k: boolean
|
seasonExistsInPlex4k: boolean
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!media.tvdbId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seasonExistsInSonarr = true;
|
let seasonExistsInSonarr = true;
|
||||||
let seasonExistsInSonarr4k = true;
|
let seasonExistsInSonarr4k = true;
|
||||||
|
|
||||||
@@ -441,35 +476,67 @@ class AvailabilitySync {
|
|||||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const seasons =
|
try {
|
||||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] ??
|
// Here we can use the cache we built when we fetched the series with mediaExistsInSonarr
|
||||||
(await api.getSeriesByTvdbId(media.tvdbId)).seasons;
|
// If the cache does not have data, we will fetch with the api route
|
||||||
this.sonarrSeasonsCache[`${server.id}-${media.tvdbId}`] = seasons;
|
|
||||||
|
|
||||||
const hasMonitoredSeason = seasons.find(
|
let seasons: SonarrSeason[] =
|
||||||
({ monitored, seasonNumber }) =>
|
this.sonarrSeasonsCache[
|
||||||
monitored && season.seasonNumber === seasonNumber
|
`${server.id}-${
|
||||||
);
|
!server.is4k ? media.externalServiceId : media.externalServiceId4k
|
||||||
|
}`
|
||||||
|
];
|
||||||
|
|
||||||
if (!server.is4k && !hasMonitoredSeason) {
|
if (!server.is4k && media.externalServiceId) {
|
||||||
seasonExistsInSonarr = false;
|
seasons =
|
||||||
|
this.sonarrSeasonsCache[
|
||||||
|
`${server.id}-${media.externalServiceId}`
|
||||||
|
] ?? (await api.getSeriesById(media.externalServiceId)).seasons;
|
||||||
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
|
||||||
|
seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && media.externalServiceId4k) {
|
||||||
|
seasons =
|
||||||
|
this.sonarrSeasonsCache[
|
||||||
|
`${server.id}-${media.externalServiceId4k}`
|
||||||
|
] ?? (await api.getSeriesById(media.externalServiceId4k)).seasons;
|
||||||
|
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
|
||||||
|
seasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonIsUnavailable = seasons?.find(
|
||||||
|
({ seasonNumber, statistics }) =>
|
||||||
|
season.seasonNumber === seasonNumber &&
|
||||||
|
statistics?.episodeFileCount === 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server.is4k && seasonIsUnavailable) {
|
||||||
|
seasonExistsInSonarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k && seasonIsUnavailable) {
|
||||||
|
seasonExistsInSonarr4k = false;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
logger.debug(
|
||||||
|
`Failure retrieving media ID ${media.id} from your ${
|
||||||
|
!server.is4k ? 'non-4K' : '4K'
|
||||||
|
} Sonarr.`,
|
||||||
|
{
|
||||||
|
errorMessage: ex.message,
|
||||||
|
label: 'AvailabilitySync',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server.is4k) {
|
||||||
|
seasonExistsInSonarr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.is4k) {
|
||||||
|
seasonExistsInSonarr4k = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.is4k && !hasMonitoredSeason) {
|
|
||||||
seasonExistsInSonarr4k = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seasonExistsInSonarr && seasonExistsInSonarr4k) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!seasonExistsInSonarr && seasonExistsInPlex) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!seasonExistsInSonarr4k && seasonExistsInPlex4k) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const seasonToBeDeleted = await seasonRequestRepository.findOne({
|
const seasonToBeDeleted = await seasonRequestRepository.findOne({
|
||||||
@@ -489,16 +556,16 @@ class AvailabilitySync {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//if season does not exist, we will change status to unknown and delete related season request
|
// If season does not exist, we will change status to unknown and delete related season request
|
||||||
//if parent media request is empty(all related seasons have been removed), parent is automatically deleted
|
// If parent media request is empty(all related seasons have been removed), parent is automatically deleted
|
||||||
if (
|
if (
|
||||||
!seasonExistsInSonarr &&
|
!seasonExistsInSonarr &&
|
||||||
seasonExistsInSonarr4k &&
|
(seasonExistsInSonarr4k || seasonExistsInPlex4k) &&
|
||||||
!seasonExistsInPlex
|
!seasonExistsInPlex
|
||||||
) {
|
) {
|
||||||
if (season.status !== MediaStatus.UNKNOWN) {
|
if (season.status !== MediaStatus.UNKNOWN) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`${media.tvdbId}, season: ${season.seasonNumber} does not exist in your non-4k sonarr and plex instance. We will change its status to unknown.`,
|
`Season ${season.seasonNumber}, media ID ${media.id} does not exist in your non-4k Sonarr and Plex instance. Status will be changed to unknown.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
await seasonRepository.update(season.id, {
|
await seasonRepository.update(season.id, {
|
||||||
@@ -511,7 +578,7 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
if (media.status === MediaStatus.AVAILABLE) {
|
if (media.status === MediaStatus.AVAILABLE) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
|
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
await mediaRepository.update(media.id, {
|
await mediaRepository.update(media.id, {
|
||||||
@@ -522,13 +589,13 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
seasonExistsInSonarr &&
|
(seasonExistsInSonarr || seasonExistsInPlex) &&
|
||||||
!seasonExistsInSonarr4k &&
|
!seasonExistsInSonarr4k &&
|
||||||
!seasonExistsInPlex4k
|
!seasonExistsInPlex4k
|
||||||
) {
|
) {
|
||||||
if (season.status4k !== MediaStatus.UNKNOWN) {
|
if (season.status4k !== MediaStatus.UNKNOWN) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`${media.tvdbId}, season: ${season.seasonNumber} does not exist in your 4k sonarr and plex instance. We will change its status to unknown.`,
|
`Season ${season.seasonNumber}, media ID ${media.id} does not exist in your 4k Sonarr and Plex instance. Status will be changed to unknown.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
await seasonRepository.update(season.id, {
|
await seasonRepository.update(season.id, {
|
||||||
@@ -541,7 +608,7 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
if (media.status4k === MediaStatus.AVAILABLE) {
|
if (media.status4k === MediaStatus.AVAILABLE) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Marking media id: ${media.tvdbId} as PARTIALLY_AVAILABLE because we deleted one of its seasons.`,
|
`Marking media ID ${media.id} as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||||
{ label: 'AvailabilitySync' }
|
{ label: 'AvailabilitySync' }
|
||||||
);
|
);
|
||||||
await mediaRepository.update(media.id, {
|
await mediaRepository.update(media.id, {
|
||||||
@@ -551,7 +618,12 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seasonExistsInSonarr || seasonExistsInSonarr4k) {
|
if (
|
||||||
|
seasonExistsInSonarr ||
|
||||||
|
seasonExistsInSonarr4k ||
|
||||||
|
seasonExistsInPlex ||
|
||||||
|
seasonExistsInPlex4k
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +637,7 @@ class AvailabilitySync {
|
|||||||
let existsInPlex = false;
|
let existsInPlex = false;
|
||||||
let existsInPlex4k = false;
|
let existsInPlex4k = false;
|
||||||
|
|
||||||
//check each plex instance to see if media exists
|
// Check each plex instance to see if media exists
|
||||||
try {
|
try {
|
||||||
if (ratingKey) {
|
if (ratingKey) {
|
||||||
const meta = await this.plexClient?.getMetadata(ratingKey);
|
const meta = await this.plexClient?.getMetadata(ratingKey);
|
||||||
@@ -573,6 +645,7 @@ class AvailabilitySync {
|
|||||||
existsInPlex = true;
|
existsInPlex = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ratingKey4k) {
|
if (ratingKey4k) {
|
||||||
const meta4k = await this.plexClient?.getMetadata(ratingKey4k);
|
const meta4k = await this.plexClient?.getMetadata(ratingKey4k);
|
||||||
if (meta4k) {
|
if (meta4k) {
|
||||||
@@ -580,18 +653,17 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
// TODO: oof, not the nicest way of handling this, but plex-api does not leave us with any other options...
|
|
||||||
if (!ex.message.includes('response code: 404')) {
|
if (!ex.message.includes('response code: 404')) {
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//base case for if both media versions exist in plex
|
// Base case if both media versions exist in plex
|
||||||
if (existsInPlex && existsInPlex4k) {
|
if (existsInPlex && existsInPlex4k) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
//we then check radarr or sonarr has that specific media. If not, then we will move to delete
|
// We then check radarr or sonarr has that specific media. If not, then we will move to delete
|
||||||
//if a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
|
// If a non-4k or 4k version exists in at least one of the instances, we will only update that specific version
|
||||||
if (media.mediaType === 'movie') {
|
if (media.mediaType === 'movie') {
|
||||||
const existsInRadarr = await this.mediaExistsInRadarr(
|
const existsInRadarr = await this.mediaExistsInRadarr(
|
||||||
media,
|
media,
|
||||||
@@ -599,10 +671,10 @@ class AvailabilitySync {
|
|||||||
existsInPlex4k
|
existsInPlex4k
|
||||||
);
|
);
|
||||||
|
|
||||||
//if true, media exists in at least one radarr or plex instance.
|
// If true, media exists in at least one radarr or plex instance.
|
||||||
if (existsInRadarr) {
|
if (existsInRadarr) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${media.tmdbId} exists in at least one radarr or plex instance. Media will be updated if set to available.`,
|
`${media.id} exists in at least one Radarr or Plex instance. Media will be updated if set to available.`,
|
||||||
{
|
{
|
||||||
label: 'AvailabilitySync',
|
label: 'AvailabilitySync',
|
||||||
}
|
}
|
||||||
@@ -619,10 +691,10 @@ class AvailabilitySync {
|
|||||||
existsInPlex4k
|
existsInPlex4k
|
||||||
);
|
);
|
||||||
|
|
||||||
//if true, media exists in at least one sonarr or plex instance.
|
// If true, media exists in at least one sonarr or plex instance.
|
||||||
if (existsInSonarr) {
|
if (existsInSonarr) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${media.tvdbId} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
|
`${media.id} exists in at least one Sonarr or Plex instance. Media will be updated if set to available.`,
|
||||||
{
|
{
|
||||||
label: 'AvailabilitySync',
|
label: 'AvailabilitySync',
|
||||||
}
|
}
|
||||||
@@ -672,7 +744,7 @@ class AvailabilitySync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//base case for if both season versions exist in plex
|
// Base case if both season versions exist in plex
|
||||||
if (seasonExistsInPlex && seasonExistsInPlex4k) {
|
if (seasonExistsInPlex && seasonExistsInPlex4k) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -686,7 +758,7 @@ class AvailabilitySync {
|
|||||||
|
|
||||||
if (existsInSonarr) {
|
if (existsInSonarr) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`${media.tvdbId}, season: ${season.seasonNumber} exists in at least one sonarr or plex instance. Media will be updated if set to available.`,
|
`Season ${season.seasonNumber}, media ID ${media.id} exists in at least one Sonarr or Plex instance. Media will be updated if set to available.`,
|
||||||
{
|
{
|
||||||
label: 'AvailabilitySync',
|
label: 'AvailabilitySync',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export interface DVRSettings {
|
|||||||
externalUrl?: string;
|
externalUrl?: string;
|
||||||
syncEnabled: boolean;
|
syncEnabled: boolean;
|
||||||
preventSearch: boolean;
|
preventSearch: boolean;
|
||||||
|
tagRequests: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RadarrSettings extends DVRSettings {
|
export interface RadarrSettings extends DVRSettings {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
TmdbCollectionResult,
|
||||||
TmdbMovieDetails,
|
TmdbMovieDetails,
|
||||||
TmdbMovieResult,
|
TmdbMovieResult,
|
||||||
TmdbPersonDetails,
|
TmdbPersonDetails,
|
||||||
@@ -9,7 +10,7 @@ import type {
|
|||||||
import { MediaType as MainMediaType } from '@server/constants/media';
|
import { MediaType as MainMediaType } from '@server/constants/media';
|
||||||
import type Media from '@server/entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
|
|
||||||
export type MediaType = 'tv' | 'movie' | 'person';
|
export type MediaType = 'tv' | 'movie' | 'person' | 'collection';
|
||||||
|
|
||||||
interface SearchResult {
|
interface SearchResult {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -43,6 +44,18 @@ export interface TvResult extends SearchResult {
|
|||||||
firstAirDate: string;
|
firstAirDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CollectionResult {
|
||||||
|
id: number;
|
||||||
|
mediaType: 'collection';
|
||||||
|
title: string;
|
||||||
|
originalTitle: string;
|
||||||
|
adult: boolean;
|
||||||
|
posterPath?: string;
|
||||||
|
backdropPath?: string;
|
||||||
|
overview: string;
|
||||||
|
originalLanguage: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PersonResult {
|
export interface PersonResult {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -53,7 +66,7 @@ export interface PersonResult {
|
|||||||
knownFor: (MovieResult | TvResult)[];
|
knownFor: (MovieResult | TvResult)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Results = MovieResult | TvResult | PersonResult;
|
export type Results = MovieResult | TvResult | PersonResult | CollectionResult;
|
||||||
|
|
||||||
export const mapMovieResult = (
|
export const mapMovieResult = (
|
||||||
movieResult: TmdbMovieResult,
|
movieResult: TmdbMovieResult,
|
||||||
@@ -99,6 +112,20 @@ export const mapTvResult = (
|
|||||||
mediaInfo: media,
|
mediaInfo: media,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mapCollectionResult = (
|
||||||
|
collectionResult: TmdbCollectionResult
|
||||||
|
): CollectionResult => ({
|
||||||
|
id: collectionResult.id,
|
||||||
|
mediaType: collectionResult.media_type || 'collection',
|
||||||
|
adult: collectionResult.adult,
|
||||||
|
originalLanguage: collectionResult.original_language,
|
||||||
|
originalTitle: collectionResult.original_title,
|
||||||
|
title: collectionResult.title,
|
||||||
|
overview: collectionResult.overview,
|
||||||
|
backdropPath: collectionResult.backdrop_path,
|
||||||
|
posterPath: collectionResult.poster_path,
|
||||||
|
});
|
||||||
|
|
||||||
export const mapPersonResult = (
|
export const mapPersonResult = (
|
||||||
personResult: TmdbPersonResult
|
personResult: TmdbPersonResult
|
||||||
): PersonResult => ({
|
): PersonResult => ({
|
||||||
@@ -118,7 +145,12 @@ export const mapPersonResult = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const mapSearchResults = (
|
export const mapSearchResults = (
|
||||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[],
|
results: (
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
|
)[],
|
||||||
media?: Media[]
|
media?: Media[]
|
||||||
): Results[] =>
|
): Results[] =>
|
||||||
results.map((result) => {
|
results.map((result) => {
|
||||||
@@ -139,6 +171,8 @@ export const mapSearchResults = (
|
|||||||
req.tmdbId === result.id && req.mediaType === MainMediaType.TV
|
req.tmdbId === result.id && req.mediaType === MainMediaType.TV
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
case 'collection':
|
||||||
|
return mapCollectionResult(result);
|
||||||
default:
|
default:
|
||||||
return mapPersonResult(result);
|
return mapPersonResult(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
return res.status(200).json(user?.filter() ?? {});
|
return res.status(200).json(user?.filter() ?? {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === 'Unauthorized') {
|
if (e.message === 'Unauthorized') {
|
||||||
logger.info(
|
logger.warn(
|
||||||
'Failed login attempt from user with incorrect Jellyfin credentials',
|
'Failed login attempt from user with incorrect Jellyfin credentials',
|
||||||
{
|
{
|
||||||
label: 'Auth',
|
label: 'Auth',
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ import { getSettings } from '@server/lib/settings';
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { mapProductionCompany } from '@server/models/Movie';
|
import { mapProductionCompany } from '@server/models/Movie';
|
||||||
import {
|
import {
|
||||||
|
mapCollectionResult,
|
||||||
mapMovieResult,
|
mapMovieResult,
|
||||||
mapPersonResult,
|
mapPersonResult,
|
||||||
mapTvResult,
|
mapTvResult,
|
||||||
} from '@server/models/Search';
|
} from '@server/models/Search';
|
||||||
import { mapNetwork } from '@server/models/Tv';
|
import { mapNetwork } from '@server/models/Tv';
|
||||||
import { isMovie, isPerson } from '@server/utils/typeHelpers';
|
import { isCollection, isMovie, isPerson } from '@server/utils/typeHelpers';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -65,6 +66,8 @@ const QueryFilterOptions = z.object({
|
|||||||
withRuntimeLte: z.coerce.string().optional(),
|
withRuntimeLte: z.coerce.string().optional(),
|
||||||
voteAverageGte: z.coerce.string().optional(),
|
voteAverageGte: z.coerce.string().optional(),
|
||||||
voteAverageLte: z.coerce.string().optional(),
|
voteAverageLte: z.coerce.string().optional(),
|
||||||
|
voteCountGte: z.coerce.string().optional(),
|
||||||
|
voteCountLte: z.coerce.string().optional(),
|
||||||
network: z.coerce.string().optional(),
|
network: z.coerce.string().optional(),
|
||||||
watchProviders: z.coerce.string().optional(),
|
watchProviders: z.coerce.string().optional(),
|
||||||
watchRegion: z.coerce.string().optional(),
|
watchRegion: z.coerce.string().optional(),
|
||||||
@@ -96,6 +99,8 @@ discoverRoutes.get('/movies', async (req, res, next) => {
|
|||||||
withRuntimeLte: query.withRuntimeLte,
|
withRuntimeLte: query.withRuntimeLte,
|
||||||
voteAverageGte: query.voteAverageGte,
|
voteAverageGte: query.voteAverageGte,
|
||||||
voteAverageLte: query.voteAverageLte,
|
voteAverageLte: query.voteAverageLte,
|
||||||
|
voteCountGte: query.voteCountGte,
|
||||||
|
voteCountLte: query.voteCountLte,
|
||||||
watchProviders: query.watchProviders,
|
watchProviders: query.watchProviders,
|
||||||
watchRegion: query.watchRegion,
|
watchRegion: query.watchRegion,
|
||||||
});
|
});
|
||||||
@@ -376,6 +381,8 @@ discoverRoutes.get('/tv', async (req, res, next) => {
|
|||||||
withRuntimeLte: query.withRuntimeLte,
|
withRuntimeLte: query.withRuntimeLte,
|
||||||
voteAverageGte: query.voteAverageGte,
|
voteAverageGte: query.voteAverageGte,
|
||||||
voteAverageLte: query.voteAverageLte,
|
voteAverageLte: query.voteAverageLte,
|
||||||
|
voteCountGte: query.voteCountGte,
|
||||||
|
voteCountLte: query.voteCountLte,
|
||||||
watchProviders: query.watchProviders,
|
watchProviders: query.watchProviders,
|
||||||
watchRegion: query.watchRegion,
|
watchRegion: query.watchRegion,
|
||||||
});
|
});
|
||||||
@@ -659,6 +666,8 @@ discoverRoutes.get('/trending', async (req, res, next) => {
|
|||||||
)
|
)
|
||||||
: isPerson(result)
|
: isPerson(result)
|
||||||
? mapPersonResult(result)
|
? mapPersonResult(result)
|
||||||
|
: isCollection(result)
|
||||||
|
? mapCollectionResult(result)
|
||||||
: mapTvResult(
|
: mapTvResult(
|
||||||
result,
|
result,
|
||||||
media.find(
|
media.find(
|
||||||
|
|||||||
@@ -183,9 +183,7 @@ serviceRoutes.get<{ tmdbId: string }>(
|
|||||||
|
|
||||||
const sonarr = new SonarrAPI({
|
const sonarr = new SonarrAPI({
|
||||||
apiKey: sonarrSettings.apiKey,
|
apiKey: sonarrSettings.apiKey,
|
||||||
url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${
|
url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
|
||||||
sonarrSettings.hostname
|
|
||||||
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}/api`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -383,7 +383,14 @@ router.delete<{ id: string }>(
|
|||||||
* we manually remove all requests from the user here so the parent media's
|
* we manually remove all requests from the user here so the parent media's
|
||||||
* properly reflect the change.
|
* properly reflect the change.
|
||||||
*/
|
*/
|
||||||
await requestRepository.remove(user.requests);
|
await requestRepository.remove(user.requests, {
|
||||||
|
/**
|
||||||
|
* Break-up into groups of 1000 requests to be removed at a time.
|
||||||
|
* Necessary for users with >1000 requests, else an SQLite 'Expression tree is too large' error occurs.
|
||||||
|
* https://typeorm.io/repository-api#additional-options
|
||||||
|
*/
|
||||||
|
chunk: user.requests.length / 1000,
|
||||||
|
});
|
||||||
|
|
||||||
await userRepository.delete(user.id);
|
await userRepository.delete(user.id);
|
||||||
return res.status(200).json(user.filter());
|
return res.status(200).json(user.filter());
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
TmdbCollectionResult,
|
||||||
TmdbMovieDetails,
|
TmdbMovieDetails,
|
||||||
TmdbMovieResult,
|
TmdbMovieResult,
|
||||||
TmdbPersonDetails,
|
TmdbPersonDetails,
|
||||||
@@ -8,17 +9,35 @@ import type {
|
|||||||
} from '@server/api/themoviedb/interfaces';
|
} from '@server/api/themoviedb/interfaces';
|
||||||
|
|
||||||
export const isMovie = (
|
export const isMovie = (
|
||||||
movie: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
|
movie:
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
): movie is TmdbMovieResult => {
|
): movie is TmdbMovieResult => {
|
||||||
return (movie as TmdbMovieResult).title !== undefined;
|
return (movie as TmdbMovieResult).title !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isPerson = (
|
export const isPerson = (
|
||||||
person: TmdbMovieResult | TmdbTvResult | TmdbPersonResult
|
person:
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
): person is TmdbPersonResult => {
|
): person is TmdbPersonResult => {
|
||||||
return (person as TmdbPersonResult).known_for !== undefined;
|
return (person as TmdbPersonResult).known_for !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isCollection = (
|
||||||
|
collection:
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
|
): collection is TmdbCollectionResult => {
|
||||||
|
return (collection as TmdbCollectionResult).media_type === 'collection';
|
||||||
|
};
|
||||||
|
|
||||||
export const isMovieDetails = (
|
export const isMovieDetails = (
|
||||||
movie: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails
|
movie: TmdbMovieDetails | TmdbTvDetails | TmdbPersonDetails
|
||||||
): movie is TmdbMovieDetails => {
|
): movie is TmdbMovieDetails => {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ parts:
|
|||||||
override-pull: |
|
override-pull: |
|
||||||
snapcraftctl pull
|
snapcraftctl pull
|
||||||
# Get information to determine snap grade and version
|
# Get information to determine snap grade and version
|
||||||
git config --global --add safe.directory /data/parts/jellyyseerr/src
|
git config --global --add safe.directory /data/parts/jellyseerr/src
|
||||||
#setup yarn.rc
|
#setup yarn.rc
|
||||||
echo "--install.frozen-lockfile\n--install.network-timeout 1000000" > .yarnrc
|
echo "--install.frozen-lockfile\n--install.network-timeout 1000000" > .yarnrc
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
|||||||
46
src/assets/services/emby.svg
Normal file
46
src/assets/services/emby.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="svg2"
|
||||||
|
viewBox="0 0 712.60077 712.5481"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<defs
|
||||||
|
id="defs4" />
|
||||||
|
<metadata
|
||||||
|
id="metadata7">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<rect
|
||||||
|
style="opacity:0;fill:#ffffff;stroke-width:4.12543"
|
||||||
|
id="rect249"
|
||||||
|
width="712.60077"
|
||||||
|
height="712.5481"
|
||||||
|
x="-0.00071160076"
|
||||||
|
y="2.0223413e-11" />
|
||||||
|
<rect
|
||||||
|
style="fill:#ffffff"
|
||||||
|
id="rect289"
|
||||||
|
width="230.18982"
|
||||||
|
height="229.82355"
|
||||||
|
x="241.20476"
|
||||||
|
y="241.36227" />
|
||||||
|
<g
|
||||||
|
id="layer1"
|
||||||
|
transform="matrix(0.70249853,0,0,0.70249853,88.770447,96.84571)">
|
||||||
|
<path
|
||||||
|
id="path3427"
|
||||||
|
d="m 327.06546,642.18589 c -45.39663,-45.86009 -82.73776,-83.3683 -82.98029,-83.3516 -0.24253,0.0167 -7.23324,6.65975 -15.53493,14.7623 l -15.09396,14.73193 -40.13624,-40.38805 C 151.24511,525.72706 108.73555,482.86504 78.854363,452.69158 l -54.329437,-54.86086 83.720394,-82.90796 83.72039,-82.90797 -15.19316,-15.20441 -15.19315,-15.20443 95.18008,-94.29313 c 52.34904,-51.86121 95.35849,-94.293118 95.57653,-94.293118 0.21805,0 37.39519,37.357576 82.61589,83.016832 45.22068,45.659256 82.53772,83.131956 82.92673,83.272666 0.38901,0.14071 7.46336,-6.49498 15.72077,-14.746 l 15.01348,-15.00184 7.14591,7.19902 c 73.95232,74.50189 181.50599,183.56427 181.36678,183.9109 -0.10065,0.25064 -37.54056,37.44106 -83.19981,82.64536 -45.65926,45.2043 -83.10802,82.41429 -83.21946,82.68884 -0.11145,0.27456 6.50478,7.34753 14.70272,15.71771 l 14.90534,15.21851 -15.3888,15.28883 c -21.09609,20.95904 -162.95155,161.27018 -169.79551,167.947 l -5.52526,5.39033 z m 89.8523,-204.1566 c 64.84836,-37.53366 117.81919,-68.54793 117.71294,-68.92058 -0.15927,-0.55862 -233.55022,-136.2489 -236.27084,-137.3646 -0.68441,-0.28068 -0.85761,27.45642 -0.85761,137.33982 0,99.83563 0.20749,137.62237 0.75471,137.43996 0.41509,-0.13837 53.81245,-30.96093 118.6608,-68.4946 z"
|
||||||
|
style="fill:#52b54b;fill-opacity:1;stroke:none" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -349,7 +349,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
<div className="pb-8" />
|
<div className="extra-bottom-space relative" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const Badge = (
|
|||||||
'bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100'
|
'bg-indigo-500 bg-opacity-80 border border-indigo-500 !text-indigo-100'
|
||||||
);
|
);
|
||||||
if (href) {
|
if (href) {
|
||||||
badgeStyle.push('hover:bg-indigo-500 bg-opacity-100');
|
badgeStyle.push('hover:bg-indigo-500 hover:bg-opacity-100');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import useVerticalScroll from '@app/hooks/useVerticalScroll';
|
|||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
|
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
|
||||||
import type {
|
import type {
|
||||||
|
CollectionResult,
|
||||||
MovieResult,
|
MovieResult,
|
||||||
PersonResult,
|
PersonResult,
|
||||||
TvResult,
|
TvResult,
|
||||||
@@ -12,7 +13,7 @@ import type {
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
type ListViewProps = {
|
type ListViewProps = {
|
||||||
items?: (TvResult | MovieResult | PersonResult)[];
|
items?: (TvResult | MovieResult | PersonResult | CollectionResult)[];
|
||||||
plexItems?: WatchlistItem[];
|
plexItems?: WatchlistItem[];
|
||||||
isEmpty?: boolean;
|
isEmpty?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@@ -94,6 +95,18 @@ const ListView = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'collection':
|
||||||
|
titleCard = (
|
||||||
|
<TitleCard
|
||||||
|
id={title.id}
|
||||||
|
image={title.posterPath}
|
||||||
|
summary={title.overview}
|
||||||
|
title={title.title}
|
||||||
|
mediaType={title.mediaType}
|
||||||
|
canExpand
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
case 'person':
|
case 'person':
|
||||||
titleCard = (
|
titleCard = (
|
||||||
<PersonCard
|
<PersonCard
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button';
|
|||||||
import Tooltip from '@app/components/Common/Tooltip';
|
import Tooltip from '@app/components/Common/Tooltip';
|
||||||
import { sliderTitles } from '@app/components/Discover/constants';
|
import { sliderTitles } from '@app/components/Discover/constants';
|
||||||
import MediaSlider from '@app/components/MediaSlider';
|
import MediaSlider from '@app/components/MediaSlider';
|
||||||
|
import { WatchProviderSelector } from '@app/components/Selector';
|
||||||
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
|
||||||
import type {
|
import type {
|
||||||
TmdbCompanySearchResponse,
|
TmdbCompanySearchResponse,
|
||||||
@@ -55,7 +56,7 @@ type CreateOption = {
|
|||||||
dataUrl: string;
|
dataUrl: string;
|
||||||
params?: string;
|
params?: string;
|
||||||
titlePlaceholderText: string;
|
titlePlaceholderText: string;
|
||||||
dataPlaceholderText: string;
|
dataPlaceholderText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
||||||
@@ -276,6 +277,20 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
|
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
|
||||||
dataPlaceholderText: intl.formatMessage(messages.providetmdbsearch),
|
dataPlaceholderText: intl.formatMessage(messages.providetmdbsearch),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES,
|
||||||
|
title: intl.formatMessage(sliderTitles.tmdbmoviestreamingservices),
|
||||||
|
dataUrl: '/api/v1/discover/movies',
|
||||||
|
params: 'watchRegion=$regionValue&watchProviders=$providersValue',
|
||||||
|
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.TMDB_TV_STREAMING_SERVICES,
|
||||||
|
title: intl.formatMessage(sliderTitles.tmdbtvstreamingservices),
|
||||||
|
dataUrl: '/api/v1/discover/tv',
|
||||||
|
params: 'watchRegion=$regionValue&watchProviders=$providersValue',
|
||||||
|
titlePlaceholderText: intl.formatMessage(messages.slidernameplaceholder),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -417,6 +432,40 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES:
|
||||||
|
dataInput = (
|
||||||
|
<WatchProviderSelector
|
||||||
|
type={'movie'}
|
||||||
|
region={slider?.data?.split(',')[0]}
|
||||||
|
activeProviders={
|
||||||
|
slider?.data
|
||||||
|
?.split(',')[1]
|
||||||
|
.split('|')
|
||||||
|
.map((v) => Number(v)) ?? []
|
||||||
|
}
|
||||||
|
onChange={(region, providers) => {
|
||||||
|
setFieldValue('data', `${region},${providers.join('|')}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case DiscoverSliderType.TMDB_TV_STREAMING_SERVICES:
|
||||||
|
dataInput = (
|
||||||
|
<WatchProviderSelector
|
||||||
|
type={'tv'}
|
||||||
|
region={slider?.data?.split(',')[0]}
|
||||||
|
activeProviders={
|
||||||
|
slider?.data
|
||||||
|
?.split(',')[1]
|
||||||
|
.split('|')
|
||||||
|
.map((v) => Number(v)) ?? []
|
||||||
|
}
|
||||||
|
onChange={(region, providers) => {
|
||||||
|
setFieldValue('data', `${region},${providers.join('|')}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
dataInput = (
|
dataInput = (
|
||||||
<Field
|
<Field
|
||||||
@@ -488,10 +537,25 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
|
|||||||
'$value',
|
'$value',
|
||||||
encodeURIExtraParams(values.data)
|
encodeURIExtraParams(values.data)
|
||||||
)}
|
)}
|
||||||
extraParams={activeOption.params?.replace(
|
extraParams={
|
||||||
'$value',
|
activeOption.type ===
|
||||||
encodeURIExtraParams(values.data)
|
DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES ||
|
||||||
)}
|
activeOption.type ===
|
||||||
|
DiscoverSliderType.TMDB_TV_STREAMING_SERVICES
|
||||||
|
? activeOption.params
|
||||||
|
?.replace(
|
||||||
|
'$regionValue',
|
||||||
|
encodeURIExtraParams(values?.data.split(',')[0])
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
'$providersValue',
|
||||||
|
encodeURIExtraParams(values?.data.split(',')[1])
|
||||||
|
)
|
||||||
|
: activeOption.params?.replace(
|
||||||
|
'$value',
|
||||||
|
encodeURIExtraParams(values.data)
|
||||||
|
)
|
||||||
|
}
|
||||||
onNewTitles={updateResultCount}
|
onNewTitles={updateResultCount}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ const DiscoverSliderEdit = ({
|
|||||||
return intl.formatMessage(sliderTitles.tmdbnetwork);
|
return intl.formatMessage(sliderTitles.tmdbnetwork);
|
||||||
case DiscoverSliderType.TMDB_SEARCH:
|
case DiscoverSliderType.TMDB_SEARCH:
|
||||||
return intl.formatMessage(sliderTitles.tmdbsearch);
|
return intl.formatMessage(sliderTitles.tmdbsearch);
|
||||||
|
case DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES:
|
||||||
|
return intl.formatMessage(sliderTitles.tmdbmoviestreamingservices);
|
||||||
|
case DiscoverSliderType.TMDB_TV_STREAMING_SERVICES:
|
||||||
|
return intl.formatMessage(sliderTitles.tmdbtvstreamingservices);
|
||||||
default:
|
default:
|
||||||
return 'Unknown Slider';
|
return 'Unknown Slider';
|
||||||
}
|
}
|
||||||
@@ -195,7 +199,9 @@ const DiscoverSliderEdit = ({
|
|||||||
className={`${slider.data ? 'mb-4' : 'mb-0'} flex space-x-2 md:mb-0`}
|
className={`${slider.data ? 'mb-4' : 'mb-0'} flex space-x-2 md:mb-0`}
|
||||||
>
|
>
|
||||||
<Bars3Icon className="h-6 w-6" />
|
<Bars3Icon className="h-6 w-6" />
|
||||||
<div>{getSliderTitle(slider)}</div>
|
<div className="w-7/12 truncate md:w-full">
|
||||||
|
{getSliderTitle(slider)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-none ${
|
className={`pointer-events-none ${
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ const messages = defineMessages({
|
|||||||
ratingText: 'Ratings between {minValue} and {maxValue}',
|
ratingText: 'Ratings between {minValue} and {maxValue}',
|
||||||
clearfilters: 'Clear Active Filters',
|
clearfilters: 'Clear Active Filters',
|
||||||
tmdbuserscore: 'TMDB User Score',
|
tmdbuserscore: 'TMDB User Score',
|
||||||
|
tmdbuservotecount: 'TMDB User Vote Count',
|
||||||
runtime: 'Runtime',
|
runtime: 'Runtime',
|
||||||
streamingservices: 'Streaming Services',
|
streamingservices: 'Streaming Services',
|
||||||
|
voteCount: 'Number of votes between {minValue} and {maxValue}',
|
||||||
});
|
});
|
||||||
|
|
||||||
type FilterSlideoverProps = {
|
type FilterSlideoverProps = {
|
||||||
@@ -246,6 +248,45 @@ const FilterSlideover = ({
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-lg font-semibold">
|
||||||
|
{intl.formatMessage(messages.tmdbuservotecount)}
|
||||||
|
</span>
|
||||||
|
<div className="relative z-0">
|
||||||
|
<MultiRangeSlider
|
||||||
|
min={0}
|
||||||
|
max={1000}
|
||||||
|
defaultMaxValue={
|
||||||
|
currentFilters.voteCountLte
|
||||||
|
? Number(currentFilters.voteCountLte)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
defaultMinValue={
|
||||||
|
currentFilters.voteCountGte
|
||||||
|
? Number(currentFilters.voteCountGte)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onUpdateMin={(min) => {
|
||||||
|
updateQueryParams(
|
||||||
|
'voteCountGte',
|
||||||
|
min !== 0 && Number(currentFilters.voteCountLte) !== 1000
|
||||||
|
? min.toString()
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onUpdateMax={(max) => {
|
||||||
|
updateQueryParams(
|
||||||
|
'voteCountLte',
|
||||||
|
max !== 1000 && Number(currentFilters.voteCountGte) !== 0
|
||||||
|
? max.toString()
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
subText={intl.formatMessage(messages.voteCount, {
|
||||||
|
minValue: currentFilters.voteCountGte ?? 0,
|
||||||
|
maxValue: currentFilters.voteCountLte ?? 1000,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<span className="text-lg font-semibold">
|
<span className="text-lg font-semibold">
|
||||||
{intl.formatMessage(messages.streamingservices)}
|
{intl.formatMessage(messages.streamingservices)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export const sliderTitles = defineMessages({
|
|||||||
tmdbnetwork: 'TMDB Network',
|
tmdbnetwork: 'TMDB Network',
|
||||||
tmdbstudio: 'TMDB Studio',
|
tmdbstudio: 'TMDB Studio',
|
||||||
tmdbsearch: 'TMDB Search',
|
tmdbsearch: 'TMDB Search',
|
||||||
|
tmdbmoviestreamingservices: 'TMDB Movie Streaming Services',
|
||||||
|
tmdbtvstreamingservices: 'TMDB TV Streaming Services',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const QueryFilterOptions = z.object({
|
export const QueryFilterOptions = z.object({
|
||||||
@@ -102,6 +104,8 @@ export const QueryFilterOptions = z.object({
|
|||||||
withRuntimeLte: z.string().optional(),
|
withRuntimeLte: z.string().optional(),
|
||||||
voteAverageGte: z.string().optional(),
|
voteAverageGte: z.string().optional(),
|
||||||
voteAverageLte: z.string().optional(),
|
voteAverageLte: z.string().optional(),
|
||||||
|
voteCountLte: z.string().optional(),
|
||||||
|
voteCountGte: z.string().optional(),
|
||||||
watchRegion: z.string().optional(),
|
watchRegion: z.string().optional(),
|
||||||
watchProviders: z.string().optional(),
|
watchProviders: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -167,6 +171,14 @@ export const prepareFilterValues = (
|
|||||||
filterValues.voteAverageLte = values.voteAverageLte;
|
filterValues.voteAverageLte = values.voteAverageLte;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (values.voteCountGte) {
|
||||||
|
filterValues.voteCountGte = values.voteCountGte;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.voteCountLte) {
|
||||||
|
filterValues.voteCountLte = values.voteCountLte;
|
||||||
|
}
|
||||||
|
|
||||||
if (values.watchProviders) {
|
if (values.watchProviders) {
|
||||||
filterValues.watchProviders = values.watchProviders;
|
filterValues.watchProviders = values.watchProviders;
|
||||||
}
|
}
|
||||||
@@ -188,6 +200,12 @@ export const countActiveFilters = (filterValues: FilterOptions): number => {
|
|||||||
delete clonedFilters.voteAverageLte;
|
delete clonedFilters.voteAverageLte;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clonedFilters.voteCountGte || filterValues.voteCountLte) {
|
||||||
|
totalCount += 1;
|
||||||
|
delete clonedFilters.voteCountGte;
|
||||||
|
delete clonedFilters.voteCountLte;
|
||||||
|
}
|
||||||
|
|
||||||
if (clonedFilters.withRuntimeGte || filterValues.withRuntimeLte) {
|
if (clonedFilters.withRuntimeGte || filterValues.withRuntimeLte) {
|
||||||
totalCount += 1;
|
totalCount += 1;
|
||||||
delete clonedFilters.withRuntimeGte;
|
delete clonedFilters.withRuntimeGte;
|
||||||
|
|||||||
@@ -365,6 +365,36 @@ const Discover = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case DiscoverSliderType.TMDB_MOVIE_STREAMING_SERVICES:
|
||||||
|
sliderComponent = (
|
||||||
|
<MediaSlider
|
||||||
|
sliderKey={`custom-slider-${slider.id}`}
|
||||||
|
title={slider.title ?? ''}
|
||||||
|
url="/api/v1/discover/movies"
|
||||||
|
extraParams={`watchRegion=${
|
||||||
|
slider.data?.split(',')[0]
|
||||||
|
}&watchProviders=${slider.data?.split(',')[1]}`}
|
||||||
|
linkUrl={`/discover/movies?watchRegion=${
|
||||||
|
slider.data?.split(',')[0]
|
||||||
|
}&watchProviders=${slider.data?.split(',')[1]}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case DiscoverSliderType.TMDB_TV_STREAMING_SERVICES:
|
||||||
|
sliderComponent = (
|
||||||
|
<MediaSlider
|
||||||
|
sliderKey={`custom-slider-${slider.id}`}
|
||||||
|
title={slider.title ?? ''}
|
||||||
|
url="/api/v1/discover/tv"
|
||||||
|
extraParams={`watchRegion=${
|
||||||
|
slider.data?.split(',')[0]
|
||||||
|
}&watchProviders=${slider.data?.split(',')[1]}`}
|
||||||
|
linkUrl={`/discover/tv?watchRegion=${
|
||||||
|
slider.data?.split(',')[0]
|
||||||
|
}&watchProviders=${slider.data?.split(',')[1]}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import EmbyLogo from '@app/assets/services/emby.svg';
|
||||||
import ImdbLogo from '@app/assets/services/imdb.svg';
|
import ImdbLogo from '@app/assets/services/imdb.svg';
|
||||||
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
|
import JellyfinLogo from '@app/assets/services/jellyfin.svg';
|
||||||
import PlexLogo from '@app/assets/services/plex.svg';
|
import PlexLogo from '@app/assets/services/plex.svg';
|
||||||
@@ -9,6 +10,7 @@ import useLocale from '@app/hooks/useLocale';
|
|||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
interface ExternalLinkBlockProps {
|
interface ExternalLinkBlockProps {
|
||||||
mediaType: 'movie' | 'tv';
|
mediaType: 'movie' | 'tv';
|
||||||
@@ -28,6 +30,7 @@ const ExternalLinkBlock = ({
|
|||||||
mediaUrl,
|
mediaUrl,
|
||||||
}: ExternalLinkBlockProps) => {
|
}: ExternalLinkBlockProps) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,6 +44,8 @@ const ExternalLinkBlock = ({
|
|||||||
>
|
>
|
||||||
{settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
|
{settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
|
||||||
<PlexLogo />
|
<PlexLogo />
|
||||||
|
) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? (
|
||||||
|
<EmbyLogo />
|
||||||
) : (
|
) : (
|
||||||
<JellyfinLogo />
|
<JellyfinLogo />
|
||||||
)}
|
)}
|
||||||
|
|||||||
118
src/components/Layout/PullToRefresh/index.tsx
Normal file
118
src/components/Layout/PullToRefresh/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
const PullToRefresh = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [pullStartPoint, setPullStartPoint] = useState(0);
|
||||||
|
const [pullChange, setPullChange] = useState(0);
|
||||||
|
const refreshDiv = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Various pull down thresholds that determine icon location
|
||||||
|
const pullDownInitThreshold = pullChange > 20;
|
||||||
|
const pullDownStopThreshold = 120;
|
||||||
|
const pullDownReloadThreshold = pullChange > 340;
|
||||||
|
const pullDownIconLocation = pullChange / 3;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reload function that is called when reload threshold has been hit
|
||||||
|
// Add loading class to determine when to add spin animation
|
||||||
|
const forceReload = () => {
|
||||||
|
refreshDiv.current?.classList.add('loading');
|
||||||
|
setTimeout(() => {
|
||||||
|
router.reload();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = document.querySelector('html');
|
||||||
|
|
||||||
|
// Determines if we are at the top of the page
|
||||||
|
// Locks or unlocks page when pulling down to refresh
|
||||||
|
const pullStart = (e: TouchEvent) => {
|
||||||
|
setPullStartPoint(e.targetTouches[0].screenY);
|
||||||
|
|
||||||
|
if (window.scrollY === 0 && window.scrollX === 0) {
|
||||||
|
refreshDiv.current?.classList.add('block');
|
||||||
|
refreshDiv.current?.classList.remove('hidden');
|
||||||
|
document.body.style.touchAction = 'none';
|
||||||
|
document.body.style.overscrollBehavior = 'none';
|
||||||
|
if (html) {
|
||||||
|
html.style.overscrollBehaviorY = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
refreshDiv.current?.classList.remove('block');
|
||||||
|
refreshDiv.current?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tracks how far we have pulled down the refresh icon
|
||||||
|
const pullDown = async (e: TouchEvent) => {
|
||||||
|
const screenY = e.targetTouches[0].screenY;
|
||||||
|
|
||||||
|
const pullLength =
|
||||||
|
pullStartPoint < screenY ? Math.abs(screenY - pullStartPoint) : 0;
|
||||||
|
|
||||||
|
setPullChange(pullLength);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Will reload the page if we are past the threshold
|
||||||
|
// Otherwise, we reset the pull
|
||||||
|
const pullFinish = () => {
|
||||||
|
setPullStartPoint(0);
|
||||||
|
|
||||||
|
if (pullDownReloadThreshold) {
|
||||||
|
forceReload();
|
||||||
|
} else {
|
||||||
|
setPullChange(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.style.touchAction = 'auto';
|
||||||
|
document.body.style.overscrollBehaviorY = 'auto';
|
||||||
|
if (html) {
|
||||||
|
html.style.overscrollBehaviorY = 'auto';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('touchstart', pullStart, { passive: false });
|
||||||
|
window.addEventListener('touchmove', pullDown, { passive: false });
|
||||||
|
window.addEventListener('touchend', pullFinish, { passive: false });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('touchstart', pullStart);
|
||||||
|
window.removeEventListener('touchmove', pullDown);
|
||||||
|
window.removeEventListener('touchend', pullFinish);
|
||||||
|
};
|
||||||
|
}, [pullDownInitThreshold, pullDownReloadThreshold, pullStartPoint, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={refreshDiv}
|
||||||
|
className="absolute left-0 right-0 top-0 z-50 m-auto w-fit transition-all ease-out"
|
||||||
|
id="refreshIcon"
|
||||||
|
style={{
|
||||||
|
top:
|
||||||
|
pullDownIconLocation < pullDownStopThreshold && pullDownInitThreshold
|
||||||
|
? pullDownIconLocation
|
||||||
|
: pullDownInitThreshold
|
||||||
|
? pullDownStopThreshold
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
refreshDiv.current?.classList.contains('loading') && 'animate-spin'
|
||||||
|
} relative -top-24 h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 shadow-md shadow-black ring-1 ring-gray-700`}
|
||||||
|
style={{ animationDirection: 'reverse' }}
|
||||||
|
>
|
||||||
|
<ArrowPathIcon
|
||||||
|
className={`rounded-full ${
|
||||||
|
pullDownReloadThreshold && 'rotate-180'
|
||||||
|
} text-indigo-500 transition-all duration-300`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PullToRefresh;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import MobileMenu from '@app/components/Layout/MobileMenu';
|
import MobileMenu from '@app/components/Layout/MobileMenu';
|
||||||
|
import PullToRefresh from '@app/components/Layout/PullToRefresh';
|
||||||
import SearchInput from '@app/components/Layout/SearchInput';
|
import SearchInput from '@app/components/Layout/SearchInput';
|
||||||
import Sidebar from '@app/components/Layout/Sidebar';
|
import Sidebar from '@app/components/Layout/Sidebar';
|
||||||
import UserDropdown from '@app/components/Layout/UserDropdown';
|
import UserDropdown from '@app/components/Layout/UserDropdown';
|
||||||
import PullToRefresh from '@app/components/PullToRefresh';
|
|
||||||
import type { AvailableLocale } from '@app/context/LanguageContext';
|
import type { AvailableLocale } from '@app/context/LanguageContext';
|
||||||
import useLocale from '@app/hooks/useLocale';
|
import useLocale from '@app/hooks/useLocale';
|
||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
|
import Tooltip from '@app/components/Common/Tooltip';
|
||||||
import useSettings from '@app/hooks/useSettings';
|
import useSettings from '@app/hooks/useSettings';
|
||||||
|
import { InformationCircleIcon } from '@heroicons/react/24/solid';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
@@ -13,6 +15,8 @@ const messages = defineMessages({
|
|||||||
password: 'Password',
|
password: 'Password',
|
||||||
host: '{mediaServerName} URL',
|
host: '{mediaServerName} URL',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
|
emailtooltip:
|
||||||
|
'Address does not need to be associated with your {mediaServerName} instance.',
|
||||||
validationhostrequired: '{mediaServerName} URL required',
|
validationhostrequired: '{mediaServerName} URL required',
|
||||||
validationhostformat: 'Valid URL required',
|
validationhostformat: 'Valid URL required',
|
||||||
validationemailrequired: 'Email required',
|
validationemailrequired: 'Email required',
|
||||||
@@ -63,6 +67,10 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
),
|
),
|
||||||
password: Yup.string(),
|
password: Yup.string(),
|
||||||
});
|
});
|
||||||
|
const mediaServerFormatValues = {
|
||||||
|
mediaServerName:
|
||||||
|
publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin',
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
@@ -101,12 +109,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
<Form>
|
<Form>
|
||||||
<div className="sm:border-t sm:border-gray-800">
|
<div className="sm:border-t sm:border-gray-800">
|
||||||
<label htmlFor="host" className="text-label">
|
<label htmlFor="host" className="text-label">
|
||||||
{intl.formatMessage(messages.host, {
|
{intl.formatMessage(messages.host, mediaServerFormatValues)}
|
||||||
mediaServerName:
|
|
||||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
|
||||||
? 'Emby'
|
|
||||||
: 'Jellyfin',
|
|
||||||
})}
|
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
||||||
<div className="flex rounded-md shadow-sm">
|
<div className="flex rounded-md shadow-sm">
|
||||||
@@ -114,20 +117,34 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
id="host"
|
id="host"
|
||||||
name="host"
|
name="host"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={intl.formatMessage(messages.host, {
|
placeholder={intl.formatMessage(
|
||||||
mediaServerName:
|
messages.host,
|
||||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
mediaServerFormatValues
|
||||||
? 'Emby'
|
)}
|
||||||
: 'Jellyfin',
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.host && touched.host && (
|
{errors.host && touched.host && (
|
||||||
<div className="error">{errors.host}</div>
|
<div className="error">{errors.host}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor="email" className="text-label">
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="text-label"
|
||||||
|
style={{ display: 'inline-flex' }}
|
||||||
|
>
|
||||||
{intl.formatMessage(messages.email)}
|
{intl.formatMessage(messages.email)}
|
||||||
|
<span className="label-tip">
|
||||||
|
<Tooltip
|
||||||
|
content={intl.formatMessage(
|
||||||
|
messages.emailtooltip,
|
||||||
|
mediaServerFormatValues
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="tooltip-trigger">
|
||||||
|
<InformationCircleIcon className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
<div className="mt-1 mb-2 sm:col-span-2 sm:mt-0">
|
||||||
<div className="flex rounded-md shadow-sm">
|
<div className="flex rounded-md shadow-sm">
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import PR from 'pulltorefreshjs';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import ReactDOMServer from 'react-dom/server';
|
|
||||||
|
|
||||||
const PullToRefresh = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
PR.init({
|
|
||||||
mainElement: '#pull-to-refresh',
|
|
||||||
onRefresh() {
|
|
||||||
router.reload();
|
|
||||||
},
|
|
||||||
iconArrow: ReactDOMServer.renderToString(
|
|
||||||
<div className="p-2">
|
|
||||||
<ArrowPathIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
iconRefreshing: ReactDOMServer.renderToString(
|
|
||||||
<div
|
|
||||||
className="animate-spin p-2"
|
|
||||||
style={{ animationDirection: 'reverse' }}
|
|
||||||
>
|
|
||||||
<ArrowPathIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
instructionsPullToRefresh: ReactDOMServer.renderToString(<div />),
|
|
||||||
instructionsReleaseToRefresh: ReactDOMServer.renderToString(<div />),
|
|
||||||
instructionsRefreshing: ReactDOMServer.renderToString(<div />),
|
|
||||||
distReload: 60,
|
|
||||||
distIgnore: 15,
|
|
||||||
shouldPullToRefresh: () =>
|
|
||||||
!window.scrollY && document.body.style.overflow !== 'hidden',
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
PR.destroyAll();
|
|
||||||
};
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
return <div id="pull-to-refresh"></div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PullToRefresh;
|
|
||||||
@@ -169,15 +169,19 @@ export const GenreSelector = ({
|
|||||||
loadDefaultGenre();
|
loadDefaultGenre();
|
||||||
}, [defaultValue, type]);
|
}, [defaultValue, type]);
|
||||||
|
|
||||||
const loadGenreOptions = async () => {
|
const loadGenreOptions = async (inputValue: string) => {
|
||||||
const results = await axios.get<GenreSliderItem[]>(
|
const results = await axios.get<GenreSliderItem[]>(
|
||||||
`/api/v1/discover/genreslider/${type}`
|
`/api/v1/discover/genreslider/${type}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return results.data.map((result) => ({
|
return results.data
|
||||||
label: result.name,
|
.map((result) => ({
|
||||||
value: result.id,
|
label: result.name,
|
||||||
}));
|
value: result.id,
|
||||||
|
}))
|
||||||
|
.filter(({ label }) =>
|
||||||
|
label.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -305,7 +309,9 @@ export const WatchProviderSelector = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange(watchRegion, activeProvider);
|
onChange(watchRegion, activeProvider);
|
||||||
}, [activeProvider, watchRegion, onChange]);
|
// removed onChange as a dependency as we only need to call it when the value(s) change
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [activeProvider, watchRegion]);
|
||||||
|
|
||||||
const orderedData = useMemo(() => {
|
const orderedData = useMemo(() => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -344,7 +350,7 @@ export const WatchProviderSelector = ({
|
|||||||
<SmallLoadingSpinner />
|
<SmallLoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<div className="grid grid-cols-6 gap-2">
|
<div className="provider-icons grid gap-2">
|
||||||
{initialProviders.map((provider) => {
|
{initialProviders.map((provider) => {
|
||||||
const isActive = activeProvider.includes(provider.id);
|
const isActive = activeProvider.includes(provider.id);
|
||||||
return (
|
return (
|
||||||
@@ -353,7 +359,7 @@ export const WatchProviderSelector = ({
|
|||||||
key={`prodiver-${provider.id}`}
|
key={`prodiver-${provider.id}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`provider-container relative h-full w-full cursor-pointer rounded-lg p-2 ring-1 ${
|
className={`provider-container relative w-full cursor-pointer rounded-lg p-2 ring-1 ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-gray-600 ring-indigo-500 hover:bg-gray-500'
|
? 'bg-gray-600 ring-indigo-500 hover:bg-gray-500'
|
||||||
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
|
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
|
||||||
@@ -386,7 +392,7 @@ export const WatchProviderSelector = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{showMore && otherProviders.length > 0 && (
|
{showMore && otherProviders.length > 0 && (
|
||||||
<div className="relative top-2 grid grid-cols-6 gap-2">
|
<div className="provider-icons relative top-2 grid gap-2">
|
||||||
{otherProviders.map((provider) => {
|
{otherProviders.map((provider) => {
|
||||||
const isActive = activeProvider.includes(provider.id);
|
const isActive = activeProvider.includes(provider.id);
|
||||||
return (
|
return (
|
||||||
@@ -395,7 +401,7 @@ export const WatchProviderSelector = ({
|
|||||||
key={`prodiver-${provider.id}`}
|
key={`prodiver-${provider.id}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`provider-container relative h-full w-full cursor-pointer rounded-lg p-2 ring-1 transition ${
|
className={`provider-container relative w-full cursor-pointer rounded-lg p-2 ring-1 transition ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-gray-600 ring-indigo-500 hover:bg-gray-500'
|
? 'bg-gray-600 ring-indigo-500 hover:bg-gray-500'
|
||||||
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
|
: 'bg-gray-700 ring-gray-500 hover:bg-gray-600'
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ const messages = defineMessages({
|
|||||||
testFirstTags: 'Test connection to load tags',
|
testFirstTags: 'Test connection to load tags',
|
||||||
tags: 'Tags',
|
tags: 'Tags',
|
||||||
enableSearch: 'Enable Automatic Search',
|
enableSearch: 'Enable Automatic Search',
|
||||||
|
tagRequests: 'Tag Requests',
|
||||||
|
tagRequestsInfo:
|
||||||
|
"Automatically add an additional tag with the requester's user ID & display name",
|
||||||
validationApplicationUrl: 'You must provide a valid URL',
|
validationApplicationUrl: 'You must provide a valid URL',
|
||||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||||
validationBaseUrlLeadingSlash: 'URL base must have a leading slash',
|
validationBaseUrlLeadingSlash: 'URL base must have a leading slash',
|
||||||
@@ -238,6 +241,7 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
|||||||
externalUrl: radarr?.externalUrl,
|
externalUrl: radarr?.externalUrl,
|
||||||
syncEnabled: radarr?.syncEnabled ?? false,
|
syncEnabled: radarr?.syncEnabled ?? false,
|
||||||
enableSearch: !radarr?.preventSearch,
|
enableSearch: !radarr?.preventSearch,
|
||||||
|
tagRequests: radarr?.tagRequests ?? false,
|
||||||
}}
|
}}
|
||||||
validationSchema={RadarrSettingsSchema}
|
validationSchema={RadarrSettingsSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
@@ -263,6 +267,7 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
|||||||
externalUrl: values.externalUrl,
|
externalUrl: values.externalUrl,
|
||||||
syncEnabled: values.syncEnabled,
|
syncEnabled: values.syncEnabled,
|
||||||
preventSearch: !values.enableSearch,
|
preventSearch: !values.enableSearch,
|
||||||
|
tagRequests: values.tagRequests,
|
||||||
};
|
};
|
||||||
if (!radarr) {
|
if (!radarr) {
|
||||||
await axios.post('/api/v1/settings/radarr', submission);
|
await axios.post('/api/v1/settings/radarr', submission);
|
||||||
@@ -713,6 +718,21 @@ const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="tagRequests" className="checkbox-label">
|
||||||
|
{intl.formatMessage(messages.tagRequests)}
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.tagRequestsInfo)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="tagRequests"
|
||||||
|
name="tagRequests"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ const messages = defineMessages({
|
|||||||
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
|
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
|
||||||
jellyfinSettings: '{mediaServerName} Settings',
|
jellyfinSettings: '{mediaServerName} Settings',
|
||||||
jellyfinSettingsDescription:
|
jellyfinSettingsDescription:
|
||||||
'Optionally configure an external player endpoint for your {mediaServerName} server that is different to the internal URL used during setup',
|
'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL.',
|
||||||
externalUrl: 'External URL',
|
externalUrl: 'External URL',
|
||||||
|
internalUrl: 'Internal URL',
|
||||||
validationUrl: 'You must provide a valid URL',
|
validationUrl: 'You must provide a valid URL',
|
||||||
syncing: 'Syncing',
|
syncing: 'Syncing',
|
||||||
syncJellyfin: 'Sync Libraries',
|
syncJellyfin: 'Sync Libraries',
|
||||||
@@ -86,7 +87,11 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
|
|
||||||
const JellyfinSettingsSchema = Yup.object().shape({
|
const JellyfinSettingsSchema = Yup.object().shape({
|
||||||
jellyfinExternalUrl: Yup.string().matches(
|
jellyfinExternalUrl: 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*)?$/,
|
/^(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)
|
intl.formatMessage(messages.validationUrl)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -346,12 +351,14 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
|
jellyfinInternalUrl: data?.hostname || '',
|
||||||
jellyfinExternalUrl: data?.externalHostname || '',
|
jellyfinExternalUrl: data?.externalHostname || '',
|
||||||
}}
|
}}
|
||||||
validationSchema={JellyfinSettingsSchema}
|
validationSchema={JellyfinSettingsSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/v1/settings/jellyfin', {
|
await axios.post('/api/v1/settings/jellyfin', {
|
||||||
|
hostname: values.jellyfinInternalUrl,
|
||||||
externalHostname: values.jellyfinExternalUrl,
|
externalHostname: values.jellyfinExternalUrl,
|
||||||
} as JellyfinSettings);
|
} as JellyfinSettings);
|
||||||
|
|
||||||
@@ -388,6 +395,27 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
|
|||||||
{({ errors, touched, handleSubmit, isSubmitting, isValid }) => {
|
{({ errors, touched, handleSubmit, isSubmitting, isValid }) => {
|
||||||
return (
|
return (
|
||||||
<form className="section" onSubmit={handleSubmit}>
|
<form className="section" onSubmit={handleSubmit}>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="jellyfinInternalUrl" className="text-label">
|
||||||
|
{intl.formatMessage(messages.internalUrl)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<div className="form-input-field">
|
||||||
|
<Field
|
||||||
|
type="text"
|
||||||
|
inputMode="url"
|
||||||
|
id="jellyfinInternalUrl"
|
||||||
|
name="jellyfinInternalUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.jellyfinInternalUrl &&
|
||||||
|
touched.jellyfinInternalUrl && (
|
||||||
|
<div className="error">
|
||||||
|
{errors.jellyfinInternalUrl}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="jellyfinExternalUrl" className="text-label">
|
<label htmlFor="jellyfinExternalUrl" className="text-label">
|
||||||
{intl.formatMessage(messages.externalUrl)}
|
{intl.formatMessage(messages.externalUrl)}
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ const messages = defineMessages({
|
|||||||
syncEnabled: 'Enable Scan',
|
syncEnabled: 'Enable Scan',
|
||||||
externalUrl: 'External URL',
|
externalUrl: 'External URL',
|
||||||
enableSearch: 'Enable Automatic Search',
|
enableSearch: 'Enable Automatic Search',
|
||||||
|
tagRequests: 'Tag Requests',
|
||||||
|
tagRequestsInfo:
|
||||||
|
"Automatically add an additional tag with the requester's user ID & display name",
|
||||||
validationApplicationUrl: 'You must provide a valid URL',
|
validationApplicationUrl: 'You must provide a valid URL',
|
||||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||||
validationBaseUrlLeadingSlash: 'Base URL must have a leading slash',
|
validationBaseUrlLeadingSlash: 'Base URL must have a leading slash',
|
||||||
@@ -252,6 +255,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
externalUrl: sonarr?.externalUrl,
|
externalUrl: sonarr?.externalUrl,
|
||||||
syncEnabled: sonarr?.syncEnabled ?? false,
|
syncEnabled: sonarr?.syncEnabled ?? false,
|
||||||
enableSearch: !sonarr?.preventSearch,
|
enableSearch: !sonarr?.preventSearch,
|
||||||
|
tagRequests: sonarr?.tagRequests ?? false,
|
||||||
}}
|
}}
|
||||||
validationSchema={SonarrSettingsSchema}
|
validationSchema={SonarrSettingsSchema}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
@@ -292,6 +296,7 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
externalUrl: values.externalUrl,
|
externalUrl: values.externalUrl,
|
||||||
syncEnabled: values.syncEnabled,
|
syncEnabled: values.syncEnabled,
|
||||||
preventSearch: !values.enableSearch,
|
preventSearch: !values.enableSearch,
|
||||||
|
tagRequests: values.tagRequests,
|
||||||
};
|
};
|
||||||
if (!sonarr) {
|
if (!sonarr) {
|
||||||
await axios.post('/api/v1/settings/sonarr', submission);
|
await axios.post('/api/v1/settings/sonarr', submission);
|
||||||
@@ -960,6 +965,21 @@ const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label htmlFor="tagRequests" className="checkbox-label">
|
||||||
|
{intl.formatMessage(messages.tagRequests)}
|
||||||
|
<span className="label-tip">
|
||||||
|
{intl.formatMessage(messages.tagRequestsInfo)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="tagRequests"
|
||||||
|
name="tagRequests"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ interface TitleCardProps {
|
|||||||
summary?: string;
|
summary?: string;
|
||||||
year?: string;
|
year?: string;
|
||||||
title: string;
|
title: string;
|
||||||
userScore: number;
|
userScore?: number;
|
||||||
mediaType: MediaType;
|
mediaType: MediaType;
|
||||||
status?: MediaStatus;
|
status?: MediaStatus;
|
||||||
canExpand?: boolean;
|
canExpand?: boolean;
|
||||||
@@ -157,7 +157,9 @@ const TitleCard = ({
|
|||||||
const showRequestButton = hasPermission(
|
const showRequestButton = hasPermission(
|
||||||
[
|
[
|
||||||
Permission.REQUEST,
|
Permission.REQUEST,
|
||||||
mediaType === 'movie' ? Permission.REQUEST_MOVIE : Permission.REQUEST_TV,
|
mediaType === 'movie' || mediaType === 'collection'
|
||||||
|
? Permission.REQUEST_MOVIE
|
||||||
|
: Permission.REQUEST_TV,
|
||||||
],
|
],
|
||||||
{ type: 'or' }
|
{ type: 'or' }
|
||||||
);
|
);
|
||||||
@@ -170,7 +172,13 @@ const TitleCard = ({
|
|||||||
<RequestModal
|
<RequestModal
|
||||||
tmdbId={id}
|
tmdbId={id}
|
||||||
show={showRequestModal}
|
show={showRequestModal}
|
||||||
type={mediaType === 'movie' ? 'movie' : 'tv'}
|
type={
|
||||||
|
mediaType === 'movie'
|
||||||
|
? 'movie'
|
||||||
|
: mediaType === 'collection'
|
||||||
|
? 'collection'
|
||||||
|
: 'tv'
|
||||||
|
}
|
||||||
onComplete={requestComplete}
|
onComplete={requestComplete}
|
||||||
onUpdating={requestUpdating}
|
onUpdating={requestUpdating}
|
||||||
onCancel={closeModal}
|
onCancel={closeModal}
|
||||||
@@ -214,7 +222,7 @@ const TitleCard = ({
|
|||||||
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
|
<div className="absolute left-0 right-0 flex items-center justify-between p-2">
|
||||||
<div
|
<div
|
||||||
className={`pointer-events-none z-40 rounded-full border bg-opacity-80 shadow-md ${
|
className={`pointer-events-none z-40 rounded-full border bg-opacity-80 shadow-md ${
|
||||||
mediaType === 'movie'
|
mediaType === 'movie' || mediaType === 'collection'
|
||||||
? 'border-blue-500 bg-blue-600'
|
? 'border-blue-500 bg-blue-600'
|
||||||
: 'border-purple-600 bg-purple-600'
|
: 'border-purple-600 bg-purple-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -222,6 +230,8 @@ const TitleCard = ({
|
|||||||
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
|
<div className="flex h-4 items-center px-2 py-2 text-center text-xs font-medium uppercase tracking-wider text-white sm:h-5">
|
||||||
{mediaType === 'movie'
|
{mediaType === 'movie'
|
||||||
? intl.formatMessage(globalMessages.movie)
|
? intl.formatMessage(globalMessages.movie)
|
||||||
|
: mediaType === 'collection'
|
||||||
|
? intl.formatMessage(globalMessages.collection)
|
||||||
: intl.formatMessage(globalMessages.tvshow)}
|
: intl.formatMessage(globalMessages.tvshow)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,7 +293,15 @@ const TitleCard = ({
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 overflow-hidden rounded-xl">
|
<div className="absolute inset-0 overflow-hidden rounded-xl">
|
||||||
<Link href={mediaType === 'movie' ? `/movie/${id}` : `/tv/${id}`}>
|
<Link
|
||||||
|
href={
|
||||||
|
mediaType === 'movie'
|
||||||
|
? `/movie/${id}`
|
||||||
|
: mediaType === 'collection'
|
||||||
|
? `/collection/${id}`
|
||||||
|
: `/tv/${id}`
|
||||||
|
}
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
className="absolute inset-0 h-full w-full cursor-pointer overflow-hidden text-left"
|
className="absolute inset-0 h-full w-full cursor-pointer overflow-hidden text-left"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -15,13 +15,20 @@ export const useLockBodyScroll = (
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
): void => {
|
): void => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const originalStyle = window.getComputedStyle(document.body).overflow;
|
const originalOverflowStyle = window.getComputedStyle(
|
||||||
|
document.body
|
||||||
|
).overflow;
|
||||||
|
const originalTouchActionStyle = window.getComputedStyle(
|
||||||
|
document.body
|
||||||
|
).touchAction;
|
||||||
if (isLocked && !disabled) {
|
if (isLocked && !disabled) {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
document.body.style.touchAction = 'none';
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
document.body.style.overflow = originalStyle;
|
document.body.style.overflow = originalOverflowStyle;
|
||||||
|
document.body.style.touchAction = originalTouchActionStyle;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isLocked, disabled]);
|
}, [isLocked, disabled]);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const globalMessages = defineMessages({
|
|||||||
approved: 'Approved',
|
approved: 'Approved',
|
||||||
movie: 'Movie',
|
movie: 'Movie',
|
||||||
movies: 'Movies',
|
movies: 'Movies',
|
||||||
|
collection: 'Collection',
|
||||||
tvshow: 'Series',
|
tvshow: 'Series',
|
||||||
tvshows: 'Series',
|
tvshows: 'Series',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
|
|||||||
@@ -457,7 +457,7 @@
|
|||||||
"components.Settings.partialRequestsEnabled": "Permet sol·licituds parcials de Sèries",
|
"components.Settings.partialRequestsEnabled": "Permet sol·licituds parcials de Sèries",
|
||||||
"components.Settings.originallanguageTip": "Filtra el contingut per l'idioma original",
|
"components.Settings.originallanguageTip": "Filtra el contingut per l'idioma original",
|
||||||
"components.Settings.originallanguage": "Idioma per a la secció \"Descobriu\"",
|
"components.Settings.originallanguage": "Idioma per a la secció \"Descobriu\"",
|
||||||
"components.Settings.manualscanDescription": "Normalment, només s’executarà una vegada cada 24 hores. Overseerr comprovarà de forma més agressiva el contingut afegit recentment del seu servidor Plex. Si és la primera vegada que configureu Plex, es recomana fer una exploració manual completa de la biblioteca!",
|
"components.Settings.manualscanDescription": "Normalment, només s’executarà una vegada cada 24 hores. Jellyseerr comprovarà de forma més agressiva el contingut afegit recentment del seu servidor Plex. Si és la primera vegada que configureu Plex, es recomana fer una exploració manual completa de la biblioteca!",
|
||||||
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Exploració d'elements de Plex afegits recentment",
|
"components.Settings.SettingsJobsCache.plex-recently-added-scan": "Exploració d'elements de Plex afegits recentment",
|
||||||
"components.Settings.notrunning": "No s'està executant",
|
"components.Settings.notrunning": "No s'està executant",
|
||||||
"components.Settings.notificationsettings": "Configuració de les notificacions",
|
"components.Settings.notificationsettings": "Configuració de les notificacions",
|
||||||
@@ -1110,5 +1110,5 @@
|
|||||||
"components.Settings.SettingsJobsCache.imagecache": "Memòria cau d'imatges",
|
"components.Settings.SettingsJobsCache.imagecache": "Memòria cau d'imatges",
|
||||||
"components.Settings.SettingsJobsCache.imagecachecount": "Imatges a la memòria cau",
|
"components.Settings.SettingsJobsCache.imagecachecount": "Imatges a la memòria cau",
|
||||||
"components.Settings.SettingsJobsCache.imagecachesize": "Mida total de la memòria cau",
|
"components.Settings.SettingsJobsCache.imagecachesize": "Mida total de la memòria cau",
|
||||||
"components.Settings.SettingsJobsCache.imagecacheDescription": "Quan està activat a la configuració, Overseerr enviarà les imatges a la memòria cau de fonts externes preconfigurades. Les imatges emmagatzemades a la memòria cau es desen a la vostra carpeta de configuració. Podeu trobar els fitxers a <code>{appDataPath}/cache/images</code>."
|
"components.Settings.SettingsJobsCache.imagecacheDescription": "Quan està activat a la configuració, Jellyseerr enviarà les imatges a la memòria cau de fonts externes preconfigurades. Les imatges emmagatzemades a la memòria cau es desen a la vostra carpeta de configuració. Podeu trobar els fitxers a <code>{appDataPath}/cache/images</code>."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"components.Settings.SettingsAbout.totalrequests": "Celkový počet žádostí",
|
"components.Settings.SettingsAbout.totalrequests": "Celkový počet žádostí",
|
||||||
"components.Settings.SettingsAbout.totalmedia": "Celkový počet médií",
|
"components.Settings.SettingsAbout.totalmedia": "Celkový počet médií",
|
||||||
"components.Settings.SettingsAbout.timezone": "Časové pásmo",
|
"components.Settings.SettingsAbout.timezone": "Časové pásmo",
|
||||||
"components.Settings.SettingsAbout.supportoverseerr": "Podpořte Overseerr",
|
"components.Settings.SettingsAbout.supportoverseerr": "Podpořte Jellyseerr",
|
||||||
"components.Settings.SettingsAbout.overseerrinformation": "O Jellyseerr",
|
"components.Settings.SettingsAbout.overseerrinformation": "O Jellyseerr",
|
||||||
"components.Settings.SettingsAbout.githubdiscussions": "Diskuze na GitHubu",
|
"components.Settings.SettingsAbout.githubdiscussions": "Diskuze na GitHubu",
|
||||||
"components.Settings.SettingsAbout.Releases.viewchangelog": "Zobrazit seznam změn",
|
"components.Settings.SettingsAbout.Releases.viewchangelog": "Zobrazit seznam změn",
|
||||||
|
|||||||
@@ -6,34 +6,122 @@
|
|||||||
"components.CollectionDetails.overview": "Overview",
|
"components.CollectionDetails.overview": "Overview",
|
||||||
"components.CollectionDetails.requestcollection": "Request Collection",
|
"components.CollectionDetails.requestcollection": "Request Collection",
|
||||||
"components.CollectionDetails.requestcollection4k": "Request Collection in 4K",
|
"components.CollectionDetails.requestcollection4k": "Request Collection in 4K",
|
||||||
|
"components.Discover.CreateSlider.addSlider": "Add Slider",
|
||||||
|
"components.Discover.CreateSlider.addcustomslider": "Create Custom Slider",
|
||||||
|
"components.Discover.CreateSlider.addfail": "Failed to create new slider.",
|
||||||
|
"components.Discover.CreateSlider.addsuccess": "Created new slider and saved discover customization settings.",
|
||||||
|
"components.Discover.CreateSlider.editSlider": "Edit Slider",
|
||||||
|
"components.Discover.CreateSlider.editfail": "Failed to edit slider.",
|
||||||
|
"components.Discover.CreateSlider.editsuccess": "Edited slider and saved discover customization settings.",
|
||||||
|
"components.Discover.CreateSlider.needresults": "You need to have at least 1 result.",
|
||||||
|
"components.Discover.CreateSlider.nooptions": "No results.",
|
||||||
|
"components.Discover.CreateSlider.providetmdbgenreid": "Provide a TMDB Genre ID",
|
||||||
|
"components.Discover.CreateSlider.providetmdbkeywordid": "Provide a TMDB Keyword ID",
|
||||||
|
"components.Discover.CreateSlider.providetmdbnetwork": "Provide TMDB Network ID",
|
||||||
|
"components.Discover.CreateSlider.providetmdbsearch": "Provide a search query",
|
||||||
|
"components.Discover.CreateSlider.providetmdbstudio": "Provide TMDB Studio ID",
|
||||||
|
"components.Discover.CreateSlider.searchGenres": "Search genres…",
|
||||||
|
"components.Discover.CreateSlider.searchKeywords": "Search keywords…",
|
||||||
|
"components.Discover.CreateSlider.searchStudios": "Search studios…",
|
||||||
|
"components.Discover.CreateSlider.slidernameplaceholder": "Slider Name",
|
||||||
|
"components.Discover.CreateSlider.starttyping": "Starting typing to search.",
|
||||||
|
"components.Discover.CreateSlider.validationDatarequired": "You must provide a data value.",
|
||||||
|
"components.Discover.CreateSlider.validationTitlerequired": "You must provide a title.",
|
||||||
"components.Discover.DiscoverMovieGenre.genreMovies": "{genre} Movies",
|
"components.Discover.DiscoverMovieGenre.genreMovies": "{genre} Movies",
|
||||||
|
"components.Discover.DiscoverMovieKeyword.keywordMovies": "{keywordTitle} Movies",
|
||||||
"components.Discover.DiscoverMovieLanguage.languageMovies": "{language} Movies",
|
"components.Discover.DiscoverMovieLanguage.languageMovies": "{language} Movies",
|
||||||
|
"components.Discover.DiscoverMovies.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
|
||||||
|
"components.Discover.DiscoverMovies.discovermovies": "Movies",
|
||||||
|
"components.Discover.DiscoverMovies.sortPopularityAsc": "Popularity Ascending",
|
||||||
|
"components.Discover.DiscoverMovies.sortPopularityDesc": "Popularity Descending",
|
||||||
|
"components.Discover.DiscoverMovies.sortReleaseDateAsc": "Release Date Ascending",
|
||||||
|
"components.Discover.DiscoverMovies.sortReleaseDateDesc": "Release Date Descending",
|
||||||
|
"components.Discover.DiscoverMovies.sortTitleAsc": "Title (A-Z) Ascending",
|
||||||
|
"components.Discover.DiscoverMovies.sortTitleDesc": "Title (Z-A) Descending",
|
||||||
|
"components.Discover.DiscoverMovies.sortTmdbRatingAsc": "TMDB Rating Ascending",
|
||||||
|
"components.Discover.DiscoverMovies.sortTmdbRatingDesc": "TMDB Rating Descending",
|
||||||
"components.Discover.DiscoverNetwork.networkSeries": "{network} Series",
|
"components.Discover.DiscoverNetwork.networkSeries": "{network} Series",
|
||||||
|
"components.Discover.DiscoverSliderEdit.deletefail": "Failed to delete slider.",
|
||||||
|
"components.Discover.DiscoverSliderEdit.deletesuccess": "Sucessfully deleted slider.",
|
||||||
|
"components.Discover.DiscoverSliderEdit.enable": "Toggle Visibility",
|
||||||
|
"components.Discover.DiscoverSliderEdit.remove": "Remove",
|
||||||
"components.Discover.DiscoverStudio.studioMovies": "{studio} Movies",
|
"components.Discover.DiscoverStudio.studioMovies": "{studio} Movies",
|
||||||
|
"components.Discover.DiscoverTv.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
|
||||||
|
"components.Discover.DiscoverTv.discovertv": "Series",
|
||||||
|
"components.Discover.DiscoverTv.sortFirstAirDateAsc": "First Air Date Ascending",
|
||||||
|
"components.Discover.DiscoverTv.sortFirstAirDateDesc": "First Air Date Descending",
|
||||||
|
"components.Discover.DiscoverTv.sortPopularityAsc": "Popularity Ascending",
|
||||||
|
"components.Discover.DiscoverTv.sortPopularityDesc": "Popularity Descending",
|
||||||
|
"components.Discover.DiscoverTv.sortTitleAsc": "Title (A-Z) Ascending",
|
||||||
|
"components.Discover.DiscoverTv.sortTitleDesc": "Title (Z-A) Descending",
|
||||||
|
"components.Discover.DiscoverTv.sortTmdbRatingAsc": "TMDB Rating Ascending",
|
||||||
|
"components.Discover.DiscoverTv.sortTmdbRatingDesc": "TMDB Rating Descending",
|
||||||
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series",
|
"components.Discover.DiscoverTvGenre.genreSeries": "{genre} Series",
|
||||||
|
"components.Discover.DiscoverTvKeyword.keywordSeries": "{keywordTitle} Series",
|
||||||
"components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series",
|
"components.Discover.DiscoverTvLanguage.languageSeries": "{language} Series",
|
||||||
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Watchlist",
|
"components.Discover.DiscoverWatchlist.discoverwatchlist": "Your Watchlist",
|
||||||
"components.Discover.DiscoverWatchlist.watchlist": "Plex Watchlist",
|
"components.Discover.DiscoverWatchlist.watchlist": "Plex Watchlist",
|
||||||
|
"components.Discover.FilterSlideover.activefilters": "{count, plural, one {# Active Filter} other {# Active Filters}}",
|
||||||
|
"components.Discover.FilterSlideover.clearfilters": "Clear Active Filters",
|
||||||
|
"components.Discover.FilterSlideover.filters": "Filters",
|
||||||
|
"components.Discover.FilterSlideover.firstAirDate": "First Air Date",
|
||||||
|
"components.Discover.FilterSlideover.from": "From",
|
||||||
|
"components.Discover.FilterSlideover.genres": "Genres",
|
||||||
|
"components.Discover.FilterSlideover.keywords": "Keywords",
|
||||||
|
"components.Discover.FilterSlideover.originalLanguage": "Original Language",
|
||||||
|
"components.Discover.FilterSlideover.ratingText": "Ratings between {minValue} and {maxValue}",
|
||||||
|
"components.Discover.FilterSlideover.releaseDate": "Release Date",
|
||||||
|
"components.Discover.FilterSlideover.runtime": "Runtime",
|
||||||
|
"components.Discover.FilterSlideover.runtimeText": "{minValue}-{maxValue} minute runtime",
|
||||||
|
"components.Discover.FilterSlideover.streamingservices": "Streaming Services",
|
||||||
|
"components.Discover.FilterSlideover.studio": "Studio",
|
||||||
|
"components.Discover.FilterSlideover.tmdbuserscore": "TMDB User Score",
|
||||||
|
"components.Discover.FilterSlideover.tmdbuservotecount": "TMDB User Vote Count",
|
||||||
|
"components.Discover.FilterSlideover.to": "To",
|
||||||
|
"components.Discover.FilterSlideover.voteCount": "Number of votes between {minValue} and {maxValue}",
|
||||||
"components.Discover.MovieGenreList.moviegenres": "Movie Genres",
|
"components.Discover.MovieGenreList.moviegenres": "Movie Genres",
|
||||||
"components.Discover.MovieGenreSlider.moviegenres": "Movie Genres",
|
"components.Discover.MovieGenreSlider.moviegenres": "Movie Genres",
|
||||||
"components.Discover.NetworkSlider.networks": "Networks",
|
"components.Discover.NetworkSlider.networks": "Networks",
|
||||||
|
"components.Discover.PlexWatchlistSlider.emptywatchlist": "Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.",
|
||||||
|
"components.Discover.PlexWatchlistSlider.plexwatchlist": "Your Plex Watchlist",
|
||||||
|
"components.Discover.RecentlyAddedSlider.recentlyAdded": "Recently Added",
|
||||||
"components.Discover.StudioSlider.studios": "Studios",
|
"components.Discover.StudioSlider.studios": "Studios",
|
||||||
"components.Discover.TvGenreList.seriesgenres": "Series Genres",
|
"components.Discover.TvGenreList.seriesgenres": "Series Genres",
|
||||||
"components.Discover.TvGenreSlider.tvgenres": "Series Genres",
|
"components.Discover.TvGenreSlider.tvgenres": "Series Genres",
|
||||||
|
"components.Discover.createnewslider": "Create New Slider",
|
||||||
|
"components.Discover.customizediscover": "Customize Discover",
|
||||||
"components.Discover.discover": "Discover",
|
"components.Discover.discover": "Discover",
|
||||||
"components.Discover.discovermovies": "Popular Movies",
|
|
||||||
"components.Discover.discovertv": "Popular Series",
|
|
||||||
"components.Discover.emptywatchlist": "Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.",
|
"components.Discover.emptywatchlist": "Media added to your <PlexWatchlistSupportLink>Plex Watchlist</PlexWatchlistSupportLink> will appear here.",
|
||||||
|
"components.Discover.moviegenres": "Movie Genres",
|
||||||
|
"components.Discover.networks": "Networks",
|
||||||
"components.Discover.noRequests": "No requests.",
|
"components.Discover.noRequests": "No requests.",
|
||||||
"components.Discover.plexwatchlist": "Your Watchlist",
|
"components.Discover.plexwatchlist": "Your Watchlist",
|
||||||
"components.Discover.popularmovies": "Popular Movies",
|
"components.Discover.popularmovies": "Popular Movies",
|
||||||
"components.Discover.populartv": "Popular Series",
|
"components.Discover.populartv": "Popular Series",
|
||||||
"components.Discover.recentlyAdded": "Recently Added",
|
"components.Discover.recentlyAdded": "Recently Added",
|
||||||
"components.Discover.recentrequests": "Recent Requests",
|
"components.Discover.recentrequests": "Recent Requests",
|
||||||
|
"components.Discover.resetfailed": "Something went wrong resetting the discover customization settings.",
|
||||||
|
"components.Discover.resetsuccess": "Sucessfully reset discover customization settings.",
|
||||||
|
"components.Discover.resettodefault": "Reset to Default",
|
||||||
|
"components.Discover.resetwarning": "Reset all sliders to default. This will also delete any custom sliders!",
|
||||||
|
"components.Discover.stopediting": "Stop Editing",
|
||||||
|
"components.Discover.studios": "Studios",
|
||||||
|
"components.Discover.tmdbmoviegenre": "TMDB Movie Genre",
|
||||||
|
"components.Discover.tmdbmoviekeyword": "TMDB Movie Keyword",
|
||||||
|
"components.Discover.tmdbmoviestreamingservices": "TMDB Movie Streaming Services",
|
||||||
|
"components.Discover.tmdbnetwork": "TMDB Network",
|
||||||
|
"components.Discover.tmdbsearch": "TMDB Search",
|
||||||
|
"components.Discover.tmdbstudio": "TMDB Studio",
|
||||||
|
"components.Discover.tmdbtvgenre": "TMDB Series Genre",
|
||||||
|
"components.Discover.tmdbtvkeyword": "TMDB Series Keyword",
|
||||||
|
"components.Discover.tmdbtvstreamingservices": "TMDB TV Streaming Services",
|
||||||
"components.Discover.trending": "Trending",
|
"components.Discover.trending": "Trending",
|
||||||
|
"components.Discover.tvgenres": "Series Genres",
|
||||||
"components.Discover.upcoming": "Upcoming Movies",
|
"components.Discover.upcoming": "Upcoming Movies",
|
||||||
"components.Discover.upcomingmovies": "Upcoming Movies",
|
"components.Discover.upcomingmovies": "Upcoming Movies",
|
||||||
"components.Discover.upcomingtv": "Upcoming Series",
|
"components.Discover.upcomingtv": "Upcoming Series",
|
||||||
|
"components.Discover.updatefailed": "Something went wrong updating the discover customization settings.",
|
||||||
|
"components.Discover.updatesuccess": "Updated discover customization settings.",
|
||||||
"components.DownloadBlock.estimatedtime": "Estimated {time}",
|
"components.DownloadBlock.estimatedtime": "Estimated {time}",
|
||||||
"components.DownloadBlock.formattedTitle": "{title}: Season {seasonNumber} Episode {episodeNumber}",
|
"components.DownloadBlock.formattedTitle": "{title}: Season {seasonNumber} Episode {episodeNumber}",
|
||||||
"components.IssueDetails.IssueComment.areyousuredelete": "Are you sure you want to delete this comment?",
|
"components.IssueDetails.IssueComment.areyousuredelete": "Are you sure you want to delete this comment?",
|
||||||
@@ -112,7 +200,11 @@
|
|||||||
"components.LanguageSelector.originalLanguageDefault": "All Languages",
|
"components.LanguageSelector.originalLanguageDefault": "All Languages",
|
||||||
"components.Layout.LanguagePicker.displaylanguage": "Display Language",
|
"components.Layout.LanguagePicker.displaylanguage": "Display Language",
|
||||||
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
|
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
|
||||||
|
"components.Layout.Sidebar.browsemovies": "Movies",
|
||||||
|
"components.Layout.Sidebar.browsetv": "Series",
|
||||||
"components.Layout.Sidebar.dashboard": "Discover",
|
"components.Layout.Sidebar.dashboard": "Discover",
|
||||||
|
"components.Layout.Sidebar.browsemovies": "Movies",
|
||||||
|
"components.Layout.Sidebar.browsetv": "Series",
|
||||||
"components.Layout.Sidebar.issues": "Issues",
|
"components.Layout.Sidebar.issues": "Issues",
|
||||||
"components.Layout.Sidebar.requests": "Requests",
|
"components.Layout.Sidebar.requests": "Requests",
|
||||||
"components.Layout.Sidebar.settings": "Settings",
|
"components.Layout.Sidebar.settings": "Settings",
|
||||||
@@ -123,21 +215,40 @@
|
|||||||
"components.Layout.UserDropdown.requests": "Requests",
|
"components.Layout.UserDropdown.requests": "Requests",
|
||||||
"components.Layout.UserDropdown.settings": "Settings",
|
"components.Layout.UserDropdown.settings": "Settings",
|
||||||
"components.Layout.UserDropdown.signout": "Sign Out",
|
"components.Layout.UserDropdown.signout": "Sign Out",
|
||||||
|
"components.Layout.UserWarnings.emailInvalid": "Email address is invalid.",
|
||||||
|
"components.Layout.UserWarnings.emailRequired": "An email address is required.",
|
||||||
|
"components.Layout.UserWarnings.passwordRequired": "A password is required.",
|
||||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind",
|
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {commit} other {commits}} behind",
|
||||||
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
"components.Layout.VersionStatus.outofdate": "Out of Date",
|
||||||
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop",
|
||||||
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stable",
|
||||||
|
"components.Login.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.email": "Email Address",
|
||||||
"components.Login.forgotpassword": "Forgot Password?",
|
"components.Login.forgotpassword": "Forgot Password?",
|
||||||
|
"components.Login.host": "{mediaServerName} URL",
|
||||||
|
"components.Login.initialsignin": "Connect",
|
||||||
|
"components.Login.initialsigningin": "Connecting…",
|
||||||
"components.Login.loginerror": "Something went wrong while trying to sign in.",
|
"components.Login.loginerror": "Something went wrong while trying to sign in.",
|
||||||
"components.Login.password": "Password",
|
"components.Login.password": "Password",
|
||||||
|
"components.Login.save": "Add",
|
||||||
|
"components.Login.saving": "Adding…",
|
||||||
"components.Login.signin": "Sign In",
|
"components.Login.signin": "Sign In",
|
||||||
"components.Login.signingin": "Signing In…",
|
"components.Login.signingin": "Signing In…",
|
||||||
"components.Login.signinheader": "Sign in to continue",
|
"components.Login.signinheader": "Sign in to continue",
|
||||||
|
"components.Login.signinwithjellyfin": "Use your {mediaServerName} account",
|
||||||
"components.Login.signinwithoverseerr": "Use your {applicationTitle} account",
|
"components.Login.signinwithoverseerr": "Use your {applicationTitle} account",
|
||||||
"components.Login.signinwithplex": "Use your Plex account",
|
"components.Login.signinwithplex": "Use your Plex account",
|
||||||
|
"components.Login.title": "Add Email",
|
||||||
|
"components.Login.username": "Username",
|
||||||
|
"components.Login.validationEmailFormat": "Invalid email",
|
||||||
|
"components.Login.validationEmailRequired": "You must provide an email",
|
||||||
|
"components.Login.validationemailformat": "Valid email required",
|
||||||
"components.Login.validationemailrequired": "You must provide a valid email address",
|
"components.Login.validationemailrequired": "You must provide a valid email address",
|
||||||
|
"components.Login.validationhostformat": "Valid URL required",
|
||||||
|
"components.Login.validationhostrequired": "{mediaServerName} URL required",
|
||||||
"components.Login.validationpasswordrequired": "You must provide a password",
|
"components.Login.validationpasswordrequired": "You must provide a password",
|
||||||
|
"components.Login.validationusernamerequired": "Username required",
|
||||||
"components.ManageSlideOver.alltime": "All Time",
|
"components.ManageSlideOver.alltime": "All Time",
|
||||||
"components.ManageSlideOver.downloadstatus": "Downloads",
|
"components.ManageSlideOver.downloadstatus": "Downloads",
|
||||||
"components.ManageSlideOver.manageModalAdvanced": "Advanced",
|
"components.ManageSlideOver.manageModalAdvanced": "Advanced",
|
||||||
@@ -147,6 +258,7 @@
|
|||||||
"components.ManageSlideOver.manageModalMedia": "Media",
|
"components.ManageSlideOver.manageModalMedia": "Media",
|
||||||
"components.ManageSlideOver.manageModalMedia4k": "4K Media",
|
"components.ManageSlideOver.manageModalMedia4k": "4K Media",
|
||||||
"components.ManageSlideOver.manageModalNoRequests": "No requests.",
|
"components.ManageSlideOver.manageModalNoRequests": "No requests.",
|
||||||
|
"components.ManageSlideOver.manageModalRemoveMediaWarning": "* This will irreversibly remove this {mediaType} from {arr}, including all files.",
|
||||||
"components.ManageSlideOver.manageModalRequests": "Requests",
|
"components.ManageSlideOver.manageModalRequests": "Requests",
|
||||||
"components.ManageSlideOver.manageModalTitle": "Manage {mediaType}",
|
"components.ManageSlideOver.manageModalTitle": "Manage {mediaType}",
|
||||||
"components.ManageSlideOver.mark4kavailable": "Mark as Available in 4K",
|
"components.ManageSlideOver.mark4kavailable": "Mark as Available in 4K",
|
||||||
@@ -160,6 +272,8 @@
|
|||||||
"components.ManageSlideOver.pastdays": "Past {days, number} Days",
|
"components.ManageSlideOver.pastdays": "Past {days, number} Days",
|
||||||
"components.ManageSlideOver.playedby": "Played By",
|
"components.ManageSlideOver.playedby": "Played By",
|
||||||
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {play} other {plays}}",
|
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {play} other {plays}}",
|
||||||
|
"components.ManageSlideOver.removearr": "Remove from {arr}",
|
||||||
|
"components.ManageSlideOver.removearr4k": "Remove from 4K {arr}",
|
||||||
"components.ManageSlideOver.tvshow": "series",
|
"components.ManageSlideOver.tvshow": "series",
|
||||||
"components.MediaSlider.ShowMoreCard.seemore": "See More",
|
"components.MediaSlider.ShowMoreCard.seemore": "See More",
|
||||||
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
|
"components.MovieDetails.MovieCast.fullcast": "Full Cast",
|
||||||
@@ -167,16 +281,19 @@
|
|||||||
"components.MovieDetails.budget": "Budget",
|
"components.MovieDetails.budget": "Budget",
|
||||||
"components.MovieDetails.cast": "Cast",
|
"components.MovieDetails.cast": "Cast",
|
||||||
"components.MovieDetails.digitalrelease": "Digital Release",
|
"components.MovieDetails.digitalrelease": "Digital Release",
|
||||||
|
"components.MovieDetails.downloadstatus": "Download Status",
|
||||||
"components.MovieDetails.managemovie": "Manage Movie",
|
"components.MovieDetails.managemovie": "Manage Movie",
|
||||||
"components.MovieDetails.mark4kavailable": "Mark as Available in 4K",
|
"components.MovieDetails.mark4kavailable": "Mark as Available in 4K",
|
||||||
"components.MovieDetails.markavailable": "Mark as Available",
|
"components.MovieDetails.markavailable": "Mark as Available",
|
||||||
|
"components.MovieDetails.openradarr": "Open Movie in Radarr",
|
||||||
|
"components.MovieDetails.openradarr4k": "Open Movie in 4K Radarr",
|
||||||
"components.MovieDetails.originallanguage": "Original Language",
|
"components.MovieDetails.originallanguage": "Original Language",
|
||||||
"components.MovieDetails.originaltitle": "Original Title",
|
"components.MovieDetails.originaltitle": "Original Title",
|
||||||
"components.MovieDetails.overview": "Overview",
|
"components.MovieDetails.overview": "Overview",
|
||||||
"components.MovieDetails.overviewunavailable": "Overview unavailable.",
|
"components.MovieDetails.overviewunavailable": "Overview unavailable.",
|
||||||
"components.MovieDetails.physicalrelease": "Physical Release",
|
"components.MovieDetails.physicalrelease": "Physical Release",
|
||||||
"components.MovieDetails.play4konplex": "Play in 4K on Plex",
|
"components.MovieDetails.play": "Play on {mediaServerName}",
|
||||||
"components.MovieDetails.playonplex": "Play on Plex",
|
"components.MovieDetails.play4k": "Play 4K on {mediaServerName}",
|
||||||
"components.MovieDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
"components.MovieDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
||||||
"components.MovieDetails.recommendations": "Recommendations",
|
"components.MovieDetails.recommendations": "Recommendations",
|
||||||
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Release Date} other {Release Dates}}",
|
"components.MovieDetails.releasedate": "{releaseCount, plural, one {Release Date} other {Release Dates}}",
|
||||||
@@ -270,8 +387,6 @@
|
|||||||
"components.PermissionEdit.requestMoviesDescription": "Grant permission to submit requests for non-4K movies.",
|
"components.PermissionEdit.requestMoviesDescription": "Grant permission to submit requests for non-4K movies.",
|
||||||
"components.PermissionEdit.requestTv": "Request Series",
|
"components.PermissionEdit.requestTv": "Request Series",
|
||||||
"components.PermissionEdit.requestTvDescription": "Grant permission to submit requests for non-4K series.",
|
"components.PermissionEdit.requestTvDescription": "Grant permission to submit requests for non-4K series.",
|
||||||
"components.PermissionEdit.settings": "Manage Settings",
|
|
||||||
"components.PermissionEdit.settingsDescription": "Grant permission to modify global settings. A user must have this permission to grant it to others.",
|
|
||||||
"components.PermissionEdit.users": "Manage Users",
|
"components.PermissionEdit.users": "Manage Users",
|
||||||
"components.PermissionEdit.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.",
|
"components.PermissionEdit.usersDescription": "Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.",
|
||||||
"components.PermissionEdit.viewissues": "View Issues",
|
"components.PermissionEdit.viewissues": "View Issues",
|
||||||
@@ -425,6 +540,13 @@
|
|||||||
"components.ResetPassword.validationpasswordrequired": "You must provide a password",
|
"components.ResetPassword.validationpasswordrequired": "You must provide a password",
|
||||||
"components.Search.search": "Search",
|
"components.Search.search": "Search",
|
||||||
"components.Search.searchresults": "Search Results",
|
"components.Search.searchresults": "Search Results",
|
||||||
|
"components.Selector.nooptions": "No results.",
|
||||||
|
"components.Selector.searchGenres": "Select genres…",
|
||||||
|
"components.Selector.searchKeywords": "Search keywords…",
|
||||||
|
"components.Selector.searchStudios": "Search studios…",
|
||||||
|
"components.Selector.showless": "Show Less",
|
||||||
|
"components.Selector.showmore": "Show More",
|
||||||
|
"components.Selector.starttyping": "Starting typing to search.",
|
||||||
"components.Settings.Notifications.NotificationsGotify.agentenabled": "Enable Agent",
|
"components.Settings.Notifications.NotificationsGotify.agentenabled": "Enable Agent",
|
||||||
"components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "Gotify notification settings failed to save.",
|
"components.Settings.Notifications.NotificationsGotify.gotifysettingsfailed": "Gotify notification settings failed to save.",
|
||||||
"components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Gotify notification settings saved successfully!",
|
"components.Settings.Notifications.NotificationsGotify.gotifysettingssaved": "Gotify notification settings saved successfully!",
|
||||||
@@ -548,6 +670,7 @@
|
|||||||
"components.Settings.Notifications.toastTelegramTestFailed": "Telegram test notification failed to send.",
|
"components.Settings.Notifications.toastTelegramTestFailed": "Telegram test notification failed to send.",
|
||||||
"components.Settings.Notifications.toastTelegramTestSending": "Sending Telegram test notification…",
|
"components.Settings.Notifications.toastTelegramTestSending": "Sending Telegram test notification…",
|
||||||
"components.Settings.Notifications.toastTelegramTestSuccess": "Telegram test notification sent!",
|
"components.Settings.Notifications.toastTelegramTestSuccess": "Telegram test notification sent!",
|
||||||
|
"components.Settings.Notifications.userEmailRequired": "Require user email",
|
||||||
"components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authorization token",
|
"components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authorization token",
|
||||||
"components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID",
|
"components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID",
|
||||||
"components.Settings.Notifications.validationEmail": "You must provide a valid email address",
|
"components.Settings.Notifications.validationEmail": "You must provide a valid email address",
|
||||||
@@ -590,6 +713,8 @@
|
|||||||
"components.Settings.RadarrModal.servername": "Server Name",
|
"components.Settings.RadarrModal.servername": "Server Name",
|
||||||
"components.Settings.RadarrModal.ssl": "Use SSL",
|
"components.Settings.RadarrModal.ssl": "Use SSL",
|
||||||
"components.Settings.RadarrModal.syncEnabled": "Enable Scan",
|
"components.Settings.RadarrModal.syncEnabled": "Enable Scan",
|
||||||
|
"components.Settings.RadarrModal.tagRequests": "Tag Requests",
|
||||||
|
"components.Settings.RadarrModal.tagRequestsInfo": "Automatically add an additional tag with the requester's user ID & display name",
|
||||||
"components.Settings.RadarrModal.tags": "Tags",
|
"components.Settings.RadarrModal.tags": "Tags",
|
||||||
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
|
"components.Settings.RadarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
|
||||||
"components.Settings.RadarrModal.testFirstRootFolders": "Test connection to load root folders",
|
"components.Settings.RadarrModal.testFirstRootFolders": "Test connection to load root folders",
|
||||||
@@ -652,13 +777,13 @@
|
|||||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
|
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
|
||||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
|
"components.Settings.SettingsJobsCache.editJobScheduleSelectorSeconds": "Every {jobScheduleSeconds, plural, one {second} other {{jobScheduleSeconds} seconds}}",
|
||||||
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
|
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
|
||||||
"components.Settings.SettingsJobsCache.jelly-recently-added-scan": "Jellyfin Recently Added Scan",
|
|
||||||
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Jellyfin Full Library Scan",
|
|
||||||
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup",
|
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Image Cache Cleanup",
|
||||||
"components.Settings.SettingsJobsCache.imagecache": "Image Cache",
|
"components.Settings.SettingsJobsCache.imagecache": "Image Cache",
|
||||||
"components.Settings.SettingsJobsCache.imagecacheDescription": "When enabled in settings, Overseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.",
|
"components.Settings.SettingsJobsCache.imagecacheDescription": "When enabled in settings, Jellyseerr will proxy and cache images from pre-configured external sources. Cached images are saved into your config folder. You can find the files in <code>{appDataPath}/cache/images</code>.",
|
||||||
"components.Settings.SettingsJobsCache.imagecachecount": "Images Cached",
|
"components.Settings.SettingsJobsCache.imagecachecount": "Images Cached",
|
||||||
"components.Settings.SettingsJobsCache.imagecachesize": "Total Cache Size",
|
"components.Settings.SettingsJobsCache.imagecachesize": "Total Cache Size",
|
||||||
|
"components.Settings.SettingsJobsCache.jellyfin-full-sync": "Jellyfin Full Library Scan",
|
||||||
|
"components.Settings.SettingsJobsCache.jellyfin-recently-added-sync": "Jellyfin Recently Added Scan",
|
||||||
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Something went wrong while saving the job.",
|
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Something went wrong while saving the job.",
|
||||||
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Job edited successfully!",
|
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Job edited successfully!",
|
||||||
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
|
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
|
||||||
@@ -695,6 +820,33 @@
|
|||||||
"components.Settings.SettingsLogs.showall": "Show All Logs",
|
"components.Settings.SettingsLogs.showall": "Show All Logs",
|
||||||
"components.Settings.SettingsLogs.time": "Timestamp",
|
"components.Settings.SettingsLogs.time": "Timestamp",
|
||||||
"components.Settings.SettingsLogs.viewdetails": "View Details",
|
"components.Settings.SettingsLogs.viewdetails": "View Details",
|
||||||
|
"components.Settings.SettingsMain.apikey": "API Key",
|
||||||
|
"components.Settings.SettingsMain.applicationTitle": "Application Title",
|
||||||
|
"components.Settings.SettingsMain.applicationurl": "Application URL",
|
||||||
|
"components.Settings.SettingsMain.cacheImages": "Enable Image Caching",
|
||||||
|
"components.Settings.SettingsMain.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)",
|
||||||
|
"components.Settings.SettingsMain.csrfProtection": "Enable CSRF Protection",
|
||||||
|
"components.Settings.SettingsMain.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
|
||||||
|
"components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
|
||||||
|
"components.Settings.SettingsMain.general": "General",
|
||||||
|
"components.Settings.SettingsMain.generalsettings": "General Settings",
|
||||||
|
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
|
||||||
|
"components.Settings.SettingsMain.hideAvailable": "Hide Available Media",
|
||||||
|
"components.Settings.SettingsMain.locale": "Display Language",
|
||||||
|
"components.Settings.SettingsMain.originallanguage": "Discover Language",
|
||||||
|
"components.Settings.SettingsMain.originallanguageTip": "Filter content by original language",
|
||||||
|
"components.Settings.SettingsMain.partialRequestsEnabled": "Allow Partial Series Requests",
|
||||||
|
"components.Settings.SettingsMain.region": "Discover Region",
|
||||||
|
"components.Settings.SettingsMain.regionTip": "Filter content by regional availability",
|
||||||
|
"components.Settings.SettingsMain.toastApiKeyFailure": "Something went wrong while generating a new API key.",
|
||||||
|
"components.Settings.SettingsMain.toastApiKeySuccess": "New API key generated successfully!",
|
||||||
|
"components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.",
|
||||||
|
"components.Settings.SettingsMain.toastSettingsSuccess": "Settings saved successfully!",
|
||||||
|
"components.Settings.SettingsMain.trustProxy": "Enable Proxy Support",
|
||||||
|
"components.Settings.SettingsMain.trustProxyTip": "Allow Jellyseerr to correctly register client IP addresses behind a proxy",
|
||||||
|
"components.Settings.SettingsMain.validationApplicationTitle": "You must provide an application title",
|
||||||
|
"components.Settings.SettingsMain.validationApplicationUrl": "You must provide a valid URL",
|
||||||
|
"components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||||
"components.Settings.SettingsUsers.defaultPermissions": "Default Permissions",
|
"components.Settings.SettingsUsers.defaultPermissions": "Default Permissions",
|
||||||
"components.Settings.SettingsUsers.defaultPermissionsTip": "Initial permissions assigned to new users",
|
"components.Settings.SettingsUsers.defaultPermissionsTip": "Initial permissions assigned to new users",
|
||||||
"components.Settings.SettingsUsers.localLogin": "Enable Local Sign-In",
|
"components.Settings.SettingsUsers.localLogin": "Enable Local Sign-In",
|
||||||
@@ -742,6 +894,8 @@
|
|||||||
"components.Settings.SonarrModal.servername": "Server Name",
|
"components.Settings.SonarrModal.servername": "Server Name",
|
||||||
"components.Settings.SonarrModal.ssl": "Use SSL",
|
"components.Settings.SonarrModal.ssl": "Use SSL",
|
||||||
"components.Settings.SonarrModal.syncEnabled": "Enable Scan",
|
"components.Settings.SonarrModal.syncEnabled": "Enable Scan",
|
||||||
|
"components.Settings.SonarrModal.tagRequests": "Tag Requests",
|
||||||
|
"components.Settings.SonarrModal.tagRequestsInfo": "Automatically add an additional tag with the requester's user ID & display name",
|
||||||
"components.Settings.SonarrModal.tags": "Tags",
|
"components.Settings.SonarrModal.tags": "Tags",
|
||||||
"components.Settings.SonarrModal.testFirstLanguageProfiles": "Test connection to load language profiles",
|
"components.Settings.SonarrModal.testFirstLanguageProfiles": "Test connection to load language profiles",
|
||||||
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
|
"components.Settings.SonarrModal.testFirstQualityProfiles": "Test connection to load quality profiles",
|
||||||
@@ -765,16 +919,8 @@
|
|||||||
"components.Settings.address": "Address",
|
"components.Settings.address": "Address",
|
||||||
"components.Settings.addsonarr": "Add Sonarr Server",
|
"components.Settings.addsonarr": "Add Sonarr Server",
|
||||||
"components.Settings.advancedTooltip": "Incorrectly configuring this setting may result in broken functionality",
|
"components.Settings.advancedTooltip": "Incorrectly configuring this setting may result in broken functionality",
|
||||||
"components.Settings.apikey": "API Key",
|
|
||||||
"components.Settings.applicationTitle": "Application Title",
|
|
||||||
"components.Settings.applicationurl": "Application URL",
|
|
||||||
"components.Settings.cacheImages": "Enable Image Caching",
|
|
||||||
"components.Settings.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)",
|
|
||||||
"components.Settings.cancelscan": "Cancel Scan",
|
"components.Settings.cancelscan": "Cancel Scan",
|
||||||
"components.Settings.copied": "Copied API key to clipboard.",
|
"components.Settings.copied": "Copied API key to clipboard.",
|
||||||
"components.Settings.csrfProtection": "Enable CSRF Protection",
|
|
||||||
"components.Settings.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
|
|
||||||
"components.Settings.csrfProtectionTip": "Set external API access to read-only (requires HTTPS, and Jellyseerr must be reloaded for changes to take effect)",
|
|
||||||
"components.Settings.currentlibrary": "Current Library: {name}",
|
"components.Settings.currentlibrary": "Current Library: {name}",
|
||||||
"components.Settings.default": "Default",
|
"components.Settings.default": "Default",
|
||||||
"components.Settings.default4k": "Default 4K",
|
"components.Settings.default4k": "Default 4K",
|
||||||
@@ -784,20 +930,27 @@
|
|||||||
"components.Settings.enablessl": "Use SSL",
|
"components.Settings.enablessl": "Use SSL",
|
||||||
"components.Settings.experimentalTooltip": "Enabling this setting may result in unexpected application behavior",
|
"components.Settings.experimentalTooltip": "Enabling this setting may result in unexpected application behavior",
|
||||||
"components.Settings.externalUrl": "External URL",
|
"components.Settings.externalUrl": "External URL",
|
||||||
"components.Settings.general": "General",
|
|
||||||
"components.Settings.generalsettings": "General Settings",
|
|
||||||
"components.Settings.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
|
|
||||||
"components.Settings.hideAvailable": "Hide Available Media",
|
|
||||||
"components.Settings.hostname": "Hostname or IP Address",
|
"components.Settings.hostname": "Hostname or IP Address",
|
||||||
|
"components.Settings.internalUrl": "Internal URL",
|
||||||
"components.Settings.is4k": "4K",
|
"components.Settings.is4k": "4K",
|
||||||
|
"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.",
|
||||||
|
"components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
|
||||||
|
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
|
||||||
|
"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.librariesRemaining": "Libraries Remaining: {count}",
|
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
|
||||||
"components.Settings.locale": "Display Language",
|
|
||||||
"components.Settings.manualscan": "Manual Library Scan",
|
"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!",
|
"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!",
|
||||||
|
"components.Settings.manualscanDescriptionJellyfin": "Normally, this will only be run once every 24 hours. Jellyseerr will check your {mediaServerName} server's recently added more aggressively. If this is your first time configuring Jellyseerr, a one-time full manual library scan is recommended!",
|
||||||
|
"components.Settings.manualscanJellyfin": "Manual Library Scan",
|
||||||
"components.Settings.mediaTypeMovie": "movie",
|
"components.Settings.mediaTypeMovie": "movie",
|
||||||
"components.Settings.mediaTypeSeries": "series",
|
"components.Settings.mediaTypeSeries": "series",
|
||||||
"components.Settings.menuAbout": "About",
|
"components.Settings.menuAbout": "About",
|
||||||
"components.Settings.menuGeneralSettings": "General",
|
"components.Settings.menuGeneralSettings": "General",
|
||||||
|
"components.Settings.menuJellyfinSettings": "{mediaServerName}",
|
||||||
"components.Settings.menuJobs": "Jobs & Cache",
|
"components.Settings.menuJobs": "Jobs & Cache",
|
||||||
"components.Settings.menuLogs": "Logs",
|
"components.Settings.menuLogs": "Logs",
|
||||||
"components.Settings.menuNotifications": "Notifications",
|
"components.Settings.menuNotifications": "Notifications",
|
||||||
@@ -811,9 +964,6 @@
|
|||||||
"components.Settings.notifications": "Notifications",
|
"components.Settings.notifications": "Notifications",
|
||||||
"components.Settings.notificationsettings": "Notification Settings",
|
"components.Settings.notificationsettings": "Notification Settings",
|
||||||
"components.Settings.notrunning": "Not Running",
|
"components.Settings.notrunning": "Not Running",
|
||||||
"components.Settings.originallanguage": "Discover Language",
|
|
||||||
"components.Settings.originallanguageTip": "Filter content by original language",
|
|
||||||
"components.Settings.partialRequestsEnabled": "Allow Partial Series Requests",
|
|
||||||
"components.Settings.plex": "Plex",
|
"components.Settings.plex": "Plex",
|
||||||
"components.Settings.plexlibraries": "Plex Libraries",
|
"components.Settings.plexlibraries": "Plex Libraries",
|
||||||
"components.Settings.plexlibrariesDescription": "The libraries Jellyseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.",
|
"components.Settings.plexlibrariesDescription": "The libraries Jellyseerr scans for titles. Set up and save your Plex connection settings, then click the button below if no libraries are listed.",
|
||||||
@@ -821,9 +971,9 @@
|
|||||||
"components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Jellyseerr scans your Plex libraries to determine content availability.",
|
"components.Settings.plexsettingsDescription": "Configure the settings for your Plex server. Jellyseerr scans your Plex libraries to determine content availability.",
|
||||||
"components.Settings.port": "Port",
|
"components.Settings.port": "Port",
|
||||||
"components.Settings.radarrsettings": "Radarr Settings",
|
"components.Settings.radarrsettings": "Radarr Settings",
|
||||||
"components.Settings.region": "Discover Region",
|
|
||||||
"components.Settings.regionTip": "Filter content by regional availability",
|
|
||||||
"components.Settings.restartrequiredTooltip": "Jellyseerr must be restarted for changes to this setting to take effect",
|
"components.Settings.restartrequiredTooltip": "Jellyseerr must be restarted for changes to this setting to take effect",
|
||||||
|
"components.Settings.save": "Save Changes",
|
||||||
|
"components.Settings.saving": "Saving…",
|
||||||
"components.Settings.scan": "Sync Libraries",
|
"components.Settings.scan": "Sync Libraries",
|
||||||
"components.Settings.scanning": "Syncing…",
|
"components.Settings.scanning": "Syncing…",
|
||||||
"components.Settings.serverLocal": "local",
|
"components.Settings.serverLocal": "local",
|
||||||
@@ -839,28 +989,22 @@
|
|||||||
"components.Settings.sonarrsettings": "Sonarr Settings",
|
"components.Settings.sonarrsettings": "Sonarr Settings",
|
||||||
"components.Settings.ssl": "SSL",
|
"components.Settings.ssl": "SSL",
|
||||||
"components.Settings.startscan": "Start Scan",
|
"components.Settings.startscan": "Start Scan",
|
||||||
|
"components.Settings.syncJellyfin": "Sync Libraries",
|
||||||
|
"components.Settings.syncing": "Syncing",
|
||||||
"components.Settings.tautulliApiKey": "API Key",
|
"components.Settings.tautulliApiKey": "API Key",
|
||||||
"components.Settings.tautulliSettings": "Tautulli Settings",
|
"components.Settings.tautulliSettings": "Tautulli Settings",
|
||||||
"components.Settings.tautulliSettingsDescription": "Optionally configure the settings for your Tautulli server. Jellyseerr fetches watch history data for your Plex media from Tautulli.",
|
"components.Settings.tautulliSettingsDescription": "Optionally configure the settings for your Tautulli server. Jellyseerr fetches watch history data for your Plex media from Tautulli.",
|
||||||
"components.Settings.toastApiKeyFailure": "Something went wrong while generating a new API key.",
|
"components.Settings.timeout": "Timeout",
|
||||||
"components.Settings.toastApiKeySuccess": "New API key generated successfully!",
|
|
||||||
"components.Settings.toastPlexConnecting": "Attempting to connect to Plex…",
|
"components.Settings.toastPlexConnecting": "Attempting to connect to Plex…",
|
||||||
"components.Settings.toastPlexConnectingFailure": "Failed to connect to Plex.",
|
"components.Settings.toastPlexConnectingFailure": "Failed to connect to Plex.",
|
||||||
"components.Settings.toastPlexConnectingSuccess": "Plex connection established successfully!",
|
"components.Settings.toastPlexConnectingSuccess": "Plex connection established successfully!",
|
||||||
"components.Settings.toastPlexRefresh": "Retrieving server list from Plex…",
|
"components.Settings.toastPlexRefresh": "Retrieving server list from Plex…",
|
||||||
"components.Settings.toastPlexRefreshFailure": "Failed to retrieve Plex server list.",
|
"components.Settings.toastPlexRefreshFailure": "Failed to retrieve Plex server list.",
|
||||||
"components.Settings.toastPlexRefreshSuccess": "Plex server list retrieved successfully!",
|
"components.Settings.toastPlexRefreshSuccess": "Plex server list retrieved successfully!",
|
||||||
"components.Settings.toastSettingsFailure": "Something went wrong while saving settings.",
|
|
||||||
"components.Settings.toastSettingsSuccess": "Settings saved successfully!",
|
|
||||||
"components.Settings.toastTautulliSettingsFailure": "Something went wrong while saving Tautulli settings.",
|
"components.Settings.toastTautulliSettingsFailure": "Something went wrong while saving Tautulli settings.",
|
||||||
"components.Settings.toastTautulliSettingsSuccess": "Tautulli settings saved successfully!",
|
"components.Settings.toastTautulliSettingsSuccess": "Tautulli settings saved successfully!",
|
||||||
"components.Settings.trustProxy": "Enable Proxy Support",
|
|
||||||
"components.Settings.trustProxyTip": "Allow Jellyseerr to correctly register client IP addresses behind a proxy (Jellyseerr must be reloaded for changes to take effect)",
|
|
||||||
"components.Settings.urlBase": "URL Base",
|
"components.Settings.urlBase": "URL Base",
|
||||||
"components.Settings.validationApiKey": "You must provide an API key",
|
"components.Settings.validationApiKey": "You must provide an API key",
|
||||||
"components.Settings.validationApplicationTitle": "You must provide an application title",
|
|
||||||
"components.Settings.validationApplicationUrl": "You must provide a valid URL",
|
|
||||||
"components.Settings.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash",
|
|
||||||
"components.Settings.validationHostnameRequired": "You must provide a valid hostname or IP address",
|
"components.Settings.validationHostnameRequired": "You must provide a valid hostname or IP address",
|
||||||
"components.Settings.validationPortRequired": "You must provide a valid port number",
|
"components.Settings.validationPortRequired": "You must provide a valid port number",
|
||||||
"components.Settings.validationUrl": "You must provide a valid URL",
|
"components.Settings.validationUrl": "You must provide a valid URL",
|
||||||
@@ -871,15 +1015,17 @@
|
|||||||
"components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app",
|
"components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app",
|
||||||
"components.Settings.webhook": "Webhook",
|
"components.Settings.webhook": "Webhook",
|
||||||
"components.Settings.webpush": "Web Push",
|
"components.Settings.webpush": "Web Push",
|
||||||
"components.Setup.configureplex": "Configure Plex",
|
"components.Setup.configuremediaserver": "Configure Media Server",
|
||||||
"components.Setup.configureservices": "Configure Services",
|
"components.Setup.configureservices": "Configure Services",
|
||||||
"components.Setup.continue": "Continue",
|
"components.Setup.continue": "Continue",
|
||||||
"components.Setup.finish": "Finish Setup",
|
"components.Setup.finish": "Finish Setup",
|
||||||
"components.Setup.finishing": "Finishing…",
|
"components.Setup.finishing": "Finishing…",
|
||||||
"components.Setup.loginwithplex": "Sign in with Plex",
|
|
||||||
"components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
|
"components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.",
|
||||||
"components.Setup.setup": "Setup",
|
"components.Setup.setup": "Setup",
|
||||||
"components.Setup.signinMessage": "Get started by signing in with your Plex account",
|
"components.Setup.signin": "Sign In",
|
||||||
|
"components.Setup.signinMessage": "Get started by signing in",
|
||||||
|
"components.Setup.signinWithJellyfin": "Use your {mediaServerName} account",
|
||||||
|
"components.Setup.signinWithPlex": "Use your Plex account",
|
||||||
"components.Setup.tip": "Tip",
|
"components.Setup.tip": "Tip",
|
||||||
"components.Setup.welcome": "Welcome to Jellyseerr",
|
"components.Setup.welcome": "Welcome to Jellyseerr",
|
||||||
"components.StatusBadge.managemedia": "Manage {mediaType}",
|
"components.StatusBadge.managemedia": "Manage {mediaType}",
|
||||||
@@ -888,9 +1034,6 @@
|
|||||||
"components.StatusBadge.seasonepisodenumber": "S{seasonNumber}E{episodeNumber}",
|
"components.StatusBadge.seasonepisodenumber": "S{seasonNumber}E{episodeNumber}",
|
||||||
"components.StatusBadge.status": "{status}",
|
"components.StatusBadge.status": "{status}",
|
||||||
"components.StatusBadge.status4k": "4K {status}",
|
"components.StatusBadge.status4k": "4K {status}",
|
||||||
"components.StatusChacker.newversionDescription": "Jellyseerr has been updated! Please click the button below to reload the page.",
|
|
||||||
"components.StatusChacker.newversionavailable": "Application Update",
|
|
||||||
"components.StatusChacker.reloadOverseerr": "Reload",
|
|
||||||
"components.StatusChecker.appUpdated": "{applicationTitle} Updated",
|
"components.StatusChecker.appUpdated": "{applicationTitle} Updated",
|
||||||
"components.StatusChecker.appUpdatedDescription": "Please click the button below to reload the application.",
|
"components.StatusChecker.appUpdatedDescription": "Please click the button below to reload the application.",
|
||||||
"components.StatusChecker.reloadApp": "Reload {applicationTitle}",
|
"components.StatusChecker.reloadApp": "Reload {applicationTitle}",
|
||||||
@@ -917,8 +1060,8 @@
|
|||||||
"components.TvDetails.originaltitle": "Original Title",
|
"components.TvDetails.originaltitle": "Original Title",
|
||||||
"components.TvDetails.overview": "Overview",
|
"components.TvDetails.overview": "Overview",
|
||||||
"components.TvDetails.overviewunavailable": "Overview unavailable.",
|
"components.TvDetails.overviewunavailable": "Overview unavailable.",
|
||||||
"components.TvDetails.play4konplex": "Play in 4K on Plex",
|
"components.TvDetails.play": "Play on {mediaServerName}",
|
||||||
"components.TvDetails.playonplex": "Play on Plex",
|
"components.TvDetails.play4k": "Play 4K on {mediaServerName}",
|
||||||
"components.TvDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
"components.TvDetails.productioncountries": "Production {countryCount, plural, one {Country} other {Countries}}",
|
||||||
"components.TvDetails.recommendations": "Recommendations",
|
"components.TvDetails.recommendations": "Recommendations",
|
||||||
"components.TvDetails.reportissue": "Report an Issue",
|
"components.TvDetails.reportissue": "Report an Issue",
|
||||||
@@ -948,13 +1091,19 @@
|
|||||||
"components.UserList.displayName": "Display Name",
|
"components.UserList.displayName": "Display Name",
|
||||||
"components.UserList.edituser": "Edit User Permissions",
|
"components.UserList.edituser": "Edit User Permissions",
|
||||||
"components.UserList.email": "Email Address",
|
"components.UserList.email": "Email Address",
|
||||||
|
"components.UserList.importedfromJellyfin": "<strong>{userCount}</strong> {mediaServerName} {userCount, plural, one {user} other {users}} imported successfully!",
|
||||||
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} imported successfully!",
|
"components.UserList.importedfromplex": "<strong>{userCount}</strong> Plex {userCount, plural, one {user} other {users}} imported successfully!",
|
||||||
|
"components.UserList.importfromJellyfin": "Import {mediaServerName} Users",
|
||||||
|
"components.UserList.importfromJellyfinerror": "Something went wrong while importing {mediaServerName} users.",
|
||||||
"components.UserList.importfrommediaserver": "Import {mediaServerName} Users",
|
"components.UserList.importfrommediaserver": "Import {mediaServerName} Users",
|
||||||
"components.UserList.importfromplex": "Import Plex Users",
|
"components.UserList.importfromplex": "Import Plex Users",
|
||||||
"components.UserList.importfromplexerror": "Something went wrong while importing Plex users.",
|
"components.UserList.importfromplexerror": "Something went wrong while importing Plex users.",
|
||||||
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
|
"components.UserList.localLoginDisabled": "The <strong>Enable Local Sign-In</strong> setting is currently disabled.",
|
||||||
"components.UserList.localuser": "Local User",
|
"components.UserList.localuser": "Local User",
|
||||||
|
"components.UserList.mediaServerUser": "{mediaServerName} User",
|
||||||
|
"components.UserList.newJellyfinsigninenabled": "The <strong>Enable New {mediaServerName} Sign-In</strong> setting is currently enabled. {mediaServerName} users with library access do not need to be imported in order to sign in.",
|
||||||
"components.UserList.newplexsigninenabled": "The <strong>Enable New Plex Sign-In</strong> setting is currently enabled. Plex users with library access do not need to be imported in order to sign in.",
|
"components.UserList.newplexsigninenabled": "The <strong>Enable New Plex Sign-In</strong> setting is currently enabled. Plex users with library access do not need to be imported in order to sign in.",
|
||||||
|
"components.UserList.noJellyfinuserstoimport": "There are no {mediaServerName} users to import.",
|
||||||
"components.UserList.nouserstoimport": "There are no Plex users to import.",
|
"components.UserList.nouserstoimport": "There are no Plex users to import.",
|
||||||
"components.UserList.owner": "Owner",
|
"components.UserList.owner": "Owner",
|
||||||
"components.UserList.password": "Password",
|
"components.UserList.password": "Password",
|
||||||
@@ -987,11 +1136,13 @@
|
|||||||
"components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord User ID",
|
"components.UserProfile.UserSettings.UserGeneralSettings.discordId": "Discord User ID",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your Discord user account",
|
"components.UserProfile.UserSettings.UserGeneralSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your Discord user account",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Display Name",
|
"components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Display Name",
|
||||||
|
"components.UserProfile.UserSettings.UserGeneralSettings.email": "Email",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Override Global Limit",
|
"components.UserProfile.UserSettings.UserGeneralSettings.enableOverride": "Override Global Limit",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.general": "General",
|
"components.UserProfile.UserSettings.UserGeneralSettings.general": "General",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "General Settings",
|
"components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "General Settings",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "Default ({language})",
|
"components.UserProfile.UserSettings.UserGeneralSettings.languageDefault": "Default ({language})",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.localuser": "Local User",
|
"components.UserProfile.UserSettings.UserGeneralSettings.localuser": "Local User",
|
||||||
|
"components.UserProfile.UserSettings.UserGeneralSettings.mediaServerUser": "{mediaServerName} User",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Movie Request Limit",
|
"components.UserProfile.UserSettings.UserGeneralSettings.movierequestlimit": "Movie Request Limit",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "Discover Language",
|
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "Discover Language",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "Filter content by original language",
|
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguageTip": "Filter content by original language",
|
||||||
@@ -1004,6 +1155,8 @@
|
|||||||
"components.UserProfile.UserSettings.UserGeneralSettings.region": "Discover Region",
|
"components.UserProfile.UserSettings.UserGeneralSettings.region": "Discover Region",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Filter content by regional availability",
|
"components.UserProfile.UserSettings.UserGeneralSettings.regionTip": "Filter content by regional availability",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.role": "Role",
|
"components.UserProfile.UserSettings.UserGeneralSettings.role": "Role",
|
||||||
|
"components.UserProfile.UserSettings.UserGeneralSettings.save": "Save Changes",
|
||||||
|
"components.UserProfile.UserSettings.UserGeneralSettings.saving": "Saving…",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Series Request Limit",
|
"components.UserProfile.UserSettings.UserGeneralSettings.seriesrequestlimit": "Series Request Limit",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.",
|
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
|
"components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!",
|
||||||
@@ -1095,6 +1248,7 @@
|
|||||||
"i18n.cancel": "Cancel",
|
"i18n.cancel": "Cancel",
|
||||||
"i18n.canceling": "Canceling…",
|
"i18n.canceling": "Canceling…",
|
||||||
"i18n.close": "Close",
|
"i18n.close": "Close",
|
||||||
|
"i18n.collection": "Collection",
|
||||||
"i18n.decline": "Decline",
|
"i18n.decline": "Decline",
|
||||||
"i18n.declined": "Declined",
|
"i18n.declined": "Declined",
|
||||||
"i18n.delete": "Delete",
|
"i18n.delete": "Delete",
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
"components.Layout.Sidebar.users": "משתמשים",
|
"components.Layout.Sidebar.users": "משתמשים",
|
||||||
"components.Layout.UserDropdown.myprofile": "פרופיל",
|
"components.Layout.UserDropdown.myprofile": "פרופיל",
|
||||||
"components.Layout.UserDropdown.settings": "הגדרות",
|
"components.Layout.UserDropdown.settings": "הגדרות",
|
||||||
"components.Layout.VersionStatus.streamdevelop": "Overseerr פיתוח",
|
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr פיתוח",
|
||||||
"components.AirDateBadge.airedrelative": "שודר ב-{relativeTime}",
|
"components.AirDateBadge.airedrelative": "שודר ב-{relativeTime}",
|
||||||
"components.Discover.NetworkSlider.networks": "רשתות שידור",
|
"components.Discover.NetworkSlider.networks": "רשתות שידור",
|
||||||
"components.Discover.discover": "לגלות",
|
"components.Discover.discover": "לגלות",
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "בקשות סרטים",
|
"components.Layout.UserDropdown.MiniQuotaDisplay.movierequests": "בקשות סרטים",
|
||||||
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "בקשות סדרות",
|
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "בקשות סדרות",
|
||||||
"components.Login.forgotpassword": "שכחת סיסמה?",
|
"components.Login.forgotpassword": "שכחת סיסמה?",
|
||||||
"components.Layout.VersionStatus.streamstable": "Overseerr יציבה",
|
"components.Layout.VersionStatus.streamstable": "Jellyseerr יציבה",
|
||||||
"components.Login.email": "כתובת אימייל",
|
"components.Login.email": "כתובת אימייל",
|
||||||
"components.ManageSlideOver.manageModalAdvanced": "מתקדם",
|
"components.ManageSlideOver.manageModalAdvanced": "מתקדם",
|
||||||
"components.ManageSlideOver.manageModalClearMedia": "מחק מידע",
|
"components.ManageSlideOver.manageModalClearMedia": "מחק מידע",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {Sezona} other {Sezone}}",
|
"components.IssueList.IssueItem.seasons": "{seasonCount, plural, one {Sezona} other {Sezone}}",
|
||||||
"components.Layout.UserDropdown.myprofile": "Profil",
|
"components.Layout.UserDropdown.myprofile": "Profil",
|
||||||
"components.Layout.UserDropdown.requests": "Zahtjevi",
|
"components.Layout.UserDropdown.requests": "Zahtjevi",
|
||||||
"components.Layout.VersionStatus.streamstable": "Overseerr Stabilan",
|
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stabilan",
|
||||||
"components.Login.password": "Zaporka",
|
"components.Login.password": "Zaporka",
|
||||||
"components.ManageSlideOver.openarr4k": "Otvori 4K u {arr}-u",
|
"components.ManageSlideOver.openarr4k": "Otvori 4K u {arr}-u",
|
||||||
"components.ManageSlideOver.pastdays": "Proteklih {days, number} dana",
|
"components.ManageSlideOver.pastdays": "Proteklih {days, number} dana",
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
"components.Layout.UserDropdown.settings": "Postavke",
|
"components.Layout.UserDropdown.settings": "Postavke",
|
||||||
"components.Layout.UserDropdown.signout": "Odjavi se",
|
"components.Layout.UserDropdown.signout": "Odjavi se",
|
||||||
"components.Layout.VersionStatus.outofdate": "Zastarjelo",
|
"components.Layout.VersionStatus.outofdate": "Zastarjelo",
|
||||||
"components.Layout.VersionStatus.streamdevelop": "Overseerr Razvoj",
|
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Razvoj",
|
||||||
"components.Login.email": "Adresa e-pošte",
|
"components.Login.email": "Adresa e-pošte",
|
||||||
"components.Login.forgotpassword": "Zaboravljena lozinka?",
|
"components.Login.forgotpassword": "Zaboravljena lozinka?",
|
||||||
"components.Login.loginerror": "Nešto nije u redu prilikom pokušaja prijave.",
|
"components.Login.loginerror": "Nešto nije u redu prilikom pokušaja prijave.",
|
||||||
|
|||||||
@@ -1103,7 +1103,7 @@
|
|||||||
"components.PermissionEdit.autorequestSeries": "Series automatisch aanvragen",
|
"components.PermissionEdit.autorequestSeries": "Series automatisch aanvragen",
|
||||||
"components.PermissionEdit.autorequestMovies": "Films automatisch aanvragen",
|
"components.PermissionEdit.autorequestMovies": "Films automatisch aanvragen",
|
||||||
"components.Settings.experimentalTooltip": "Deze instelling inschakelen, kan leiden tot onverwacht gedrag van de toepassing",
|
"components.Settings.experimentalTooltip": "Deze instelling inschakelen, kan leiden tot onverwacht gedrag van de toepassing",
|
||||||
"components.Settings.restartrequiredTooltip": "Overseerr moet opnieuw worden gestart om wijzigingen in deze instelling door te voeren",
|
"components.Settings.restartrequiredTooltip": "Jellyseerr moet opnieuw worden gestart om wijzigingen in deze instelling door te voeren",
|
||||||
"components.AirDateBadge.airedrelative": "{relativeTime} uitgezonden",
|
"components.AirDateBadge.airedrelative": "{relativeTime} uitgezonden",
|
||||||
"components.AirDateBadge.airsrelative": "Uitzending {relativeTime}",
|
"components.AirDateBadge.airsrelative": "Uitzending {relativeTime}",
|
||||||
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Serieverzoeken",
|
"components.Layout.UserDropdown.MiniQuotaDisplay.seriesrequests": "Serieverzoeken",
|
||||||
|
|||||||
@@ -246,7 +246,7 @@
|
|||||||
"components.PermissionEdit.managerequestsDescription": "Acordați permisiunea de a gestiona solicitările media. Toate solicitările făcute de un utilizator cu această permisiune vor fi aprobate automat.",
|
"components.PermissionEdit.managerequestsDescription": "Acordați permisiunea de a gestiona solicitările media. Toate solicitările făcute de un utilizator cu această permisiune vor fi aprobate automat.",
|
||||||
"components.PermissionEdit.request4k": "Solicitați 4K",
|
"components.PermissionEdit.request4k": "Solicitați 4K",
|
||||||
"components.PermissionEdit.request4kTv": "Solicitați Seria TV în 4K",
|
"components.PermissionEdit.request4kTv": "Solicitați Seria TV în 4K",
|
||||||
"components.Layout.VersionStatus.streamstable": "Overseerr Stabil",
|
"components.Layout.VersionStatus.streamstable": "Jellyseerr Stabil",
|
||||||
"components.PermissionEdit.request4kMoviesDescription": "Acordați permisiunea de a trimite solicitări pentru filme 4K.",
|
"components.PermissionEdit.request4kMoviesDescription": "Acordați permisiunea de a trimite solicitări pentru filme 4K.",
|
||||||
"components.PermissionEdit.createissuesDescription": "Acordați permisiunea de a raporta probleme media.",
|
"components.PermissionEdit.createissuesDescription": "Acordați permisiunea de a raporta probleme media.",
|
||||||
"components.PermissionEdit.autorequestSeries": "Solicită Automat Seriale TV",
|
"components.PermissionEdit.autorequestSeries": "Solicită Automat Seriale TV",
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
"components.PermissionEdit.manageissuesDescription": "Acordați permisiunea de a gestiona problemele media.",
|
"components.PermissionEdit.manageissuesDescription": "Acordați permisiunea de a gestiona problemele media.",
|
||||||
"components.PermissionEdit.managerequests": "Gestionați Solicitările",
|
"components.PermissionEdit.managerequests": "Gestionați Solicitările",
|
||||||
"components.PermissionEdit.request4kMovies": "Solicitați Filme 4K",
|
"components.PermissionEdit.request4kMovies": "Solicitați Filme 4K",
|
||||||
"components.Layout.VersionStatus.streamdevelop": "Overseerr Dezvoltat",
|
"components.Layout.VersionStatus.streamdevelop": "Jellyseerr Dezvoltat",
|
||||||
"components.PermissionEdit.manageissues": "Gestionarea Problemelor",
|
"components.PermissionEdit.manageissues": "Gestionarea Problemelor",
|
||||||
"components.ManageSlideOver.playedby": "Rulat De",
|
"components.ManageSlideOver.playedby": "Rulat De",
|
||||||
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {rulare} other {rulări}}",
|
"components.ManageSlideOver.plays": "<strong>{playCount, number}</strong> {playCount, plural, one {rulare} other {rulări}}",
|
||||||
|
|||||||
@@ -654,7 +654,7 @@
|
|||||||
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Сканування повної бібліотеки Jellyfin",
|
"components.Settings.SettingsJobsCache.jellyfin-full-scan": "Сканування повної бібліотеки Jellyfin",
|
||||||
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Очищення кешу зображень",
|
"components.Settings.SettingsJobsCache.image-cache-cleanup": "Очищення кешу зображень",
|
||||||
"components.Settings.SettingsJobsCache.imagecache": "Кеш зображень",
|
"components.Settings.SettingsJobsCache.imagecache": "Кеш зображень",
|
||||||
"components.Settings.SettingsJobsCache.imagecacheDescription": "Якщо в налаштуваннях увімкнено, Overseerr буде проксі-сервером і кешувати зображення з попередньо налаштованих зовнішніх джерел. Кешовані зображення зберігаються у папці конфігурації. Ви можете знайти файли в <code>{appDataPath}/cache/images</code>.",
|
"components.Settings.SettingsJobsCache.imagecacheDescription": "Якщо в налаштуваннях увімкнено, Jellyseerr буде проксі-сервером і кешувати зображення з попередньо налаштованих зовнішніх джерел. Кешовані зображення зберігаються у папці конфігурації. Ви можете знайти файли в <code>{appDataPath}/cache/images</code>.",
|
||||||
"components.Settings.SettingsJobsCache.imagecachecount": "Зображення кешовані",
|
"components.Settings.SettingsJobsCache.imagecachecount": "Зображення кешовані",
|
||||||
"components.Settings.SettingsJobsCache.imagecachesize": "Загальний розмір кешу",
|
"components.Settings.SettingsJobsCache.imagecachesize": "Загальний розмір кешу",
|
||||||
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Щось пішло не так при збереженні завдання.",
|
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Щось пішло не так при збереженні завдання.",
|
||||||
|
|||||||
@@ -274,6 +274,8 @@
|
|||||||
"components.Layout.UserDropdown.signout": "登出",
|
"components.Layout.UserDropdown.signout": "登出",
|
||||||
"components.Layout.UserDropdown.settings": "用户设定",
|
"components.Layout.UserDropdown.settings": "用户设定",
|
||||||
"components.Layout.UserDropdown.myprofile": "个人档案",
|
"components.Layout.UserDropdown.myprofile": "个人档案",
|
||||||
|
"components.Layout.Sidebar.browsemovies": "电影",
|
||||||
|
"components.Layout.Sidebar.browsetv": "电视节目",
|
||||||
"components.Layout.Sidebar.users": "用户",
|
"components.Layout.Sidebar.users": "用户",
|
||||||
"components.Layout.Sidebar.settings": "设定",
|
"components.Layout.Sidebar.settings": "设定",
|
||||||
"components.Layout.Sidebar.requests": "请求",
|
"components.Layout.Sidebar.requests": "请求",
|
||||||
@@ -289,6 +291,7 @@
|
|||||||
"components.Discover.trending": "趋势",
|
"components.Discover.trending": "趋势",
|
||||||
"components.Discover.recentrequests": "最新请求",
|
"components.Discover.recentrequests": "最新请求",
|
||||||
"components.Discover.recentlyAdded": "最新添加",
|
"components.Discover.recentlyAdded": "最新添加",
|
||||||
|
"components.Discover.RecentlyAddedSlider.recentlyAdded": "最近添加",
|
||||||
"components.Discover.populartv": "热门电视节目",
|
"components.Discover.populartv": "热门电视节目",
|
||||||
"components.Discover.popularmovies": "热门电影",
|
"components.Discover.popularmovies": "热门电影",
|
||||||
"components.Discover.discovertv": "热门电视节目",
|
"components.Discover.discovertv": "热门电视节目",
|
||||||
@@ -1057,7 +1060,7 @@
|
|||||||
"components.RequestCard.tmdbid": "TMDB ID",
|
"components.RequestCard.tmdbid": "TMDB ID",
|
||||||
"components.Settings.SettingsLogs.viewdetails": "查看详情",
|
"components.Settings.SettingsLogs.viewdetails": "查看详情",
|
||||||
"components.Layout.UserDropdown.requests": "请求",
|
"components.Layout.UserDropdown.requests": "请求",
|
||||||
"components.Settings.restartrequiredTooltip": "必须重新启动 Overseerr 才能使更改的设置生效",
|
"components.Settings.restartrequiredTooltip": "必须重新启动 Jellyseerr 才能使更改的设置生效",
|
||||||
"components.TvDetails.manageseries": "管理电视节目",
|
"components.TvDetails.manageseries": "管理电视节目",
|
||||||
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "自动请求您的 <PlexWatchlistSupportLink>Plex 关注列表</PlexWatchlistSupportLink>的媒体",
|
"components.UserProfile.UserSettings.UserGeneralSettings.plexwatchlistsyncseriestip": "自动请求您的 <PlexWatchlistSupportLink>Plex 关注列表</PlexWatchlistSupportLink>的媒体",
|
||||||
"components.AirDateBadge.airedrelative": "播出{relativeTime}",
|
"components.AirDateBadge.airedrelative": "播出{relativeTime}",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import MovieDetails from '@app/components/MovieDetails';
|
import MovieDetails from '@app/components/MovieDetails';
|
||||||
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
import type { MovieDetails as MovieDetailsType } from '@server/models/Movie';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { NextPage } from 'next';
|
import type { GetServerSideProps, NextPage } from 'next';
|
||||||
|
|
||||||
interface MoviePageProps {
|
interface MoviePageProps {
|
||||||
movie?: MovieDetailsType;
|
movie?: MovieDetailsType;
|
||||||
@@ -11,25 +11,25 @@ const MoviePage: NextPage<MoviePageProps> = ({ movie }) => {
|
|||||||
return <MovieDetails movie={movie} />;
|
return <MovieDetails movie={movie} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
MoviePage.getInitialProps = async (ctx) => {
|
export const getServerSideProps: GetServerSideProps<MoviePageProps> = async (
|
||||||
if (ctx.req) {
|
ctx
|
||||||
const response = await axios.get<MovieDetailsType>(
|
) => {
|
||||||
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
|
const response = await axios.get<MovieDetailsType>(
|
||||||
ctx.query.movieId
|
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
|
||||||
}`,
|
ctx.query.movieId
|
||||||
{
|
}`,
|
||||||
headers: ctx.req?.headers?.cookie
|
{
|
||||||
? { cookie: ctx.req.headers.cookie }
|
headers: ctx.req?.headers?.cookie
|
||||||
: undefined,
|
? { cookie: ctx.req.headers.cookie }
|
||||||
}
|
: undefined,
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
props: {
|
||||||
movie: response.data,
|
movie: response.data,
|
||||||
};
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MoviePage;
|
export default MoviePage;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import TvDetails from '@app/components/TvDetails';
|
import TvDetails from '@app/components/TvDetails';
|
||||||
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
|
import type { TvDetails as TvDetailsType } from '@server/models/Tv';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { NextPage } from 'next';
|
import type { GetServerSideProps, NextPage } from 'next';
|
||||||
|
|
||||||
interface TvPageProps {
|
interface TvPageProps {
|
||||||
tv?: TvDetailsType;
|
tv?: TvDetailsType;
|
||||||
@@ -11,25 +11,23 @@ const TvPage: NextPage<TvPageProps> = ({ tv }) => {
|
|||||||
return <TvDetails tv={tv} />;
|
return <TvDetails tv={tv} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
TvPage.getInitialProps = async (ctx) => {
|
export const getServerSideProps: GetServerSideProps<TvPageProps> = async (
|
||||||
if (ctx.req) {
|
ctx
|
||||||
const response = await axios.get<TvDetailsType>(
|
) => {
|
||||||
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${
|
const response = await axios.get<TvDetailsType>(
|
||||||
ctx.query.tvId
|
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`,
|
||||||
}`,
|
{
|
||||||
{
|
headers: ctx.req?.headers?.cookie
|
||||||
headers: ctx.req?.headers?.cookie
|
? { cookie: ctx.req.headers.cookie }
|
||||||
? { cookie: ctx.req.headers.cookie }
|
: undefined,
|
||||||
: undefined,
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
props: {
|
||||||
tv: response.data,
|
tv: response.data,
|
||||||
};
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TvPage;
|
export default TvPage;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-gray-900;
|
@apply bg-gray-900;
|
||||||
overscroll-behavior-y: contain;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@@ -73,6 +73,10 @@
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(16.5rem, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provider-icons {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(3.5rem, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.slider-header {
|
.slider-header {
|
||||||
@apply relative mt-6 mb-4 flex;
|
@apply relative mt-6 mb-4 flex;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user