mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 18:59:28 -05:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
299f65c597 | ||
|
|
6021d1e336 | ||
|
|
3ce1ef350e | ||
|
|
06c91744f3 | ||
|
|
d18e3d185f | ||
|
|
e222463a63 | ||
|
|
03b9bda287 | ||
|
|
7e20c7cb78 | ||
|
|
d0cdce9e90 | ||
|
|
113b09bf2b | ||
|
|
b16f192b92 | ||
|
|
d9ca3c6e52 | ||
|
|
ba82ecec5c | ||
|
|
c052a2455c | ||
|
|
2d99a8b03c | ||
|
|
7434c0cf2f | ||
|
|
afcb096f49 | ||
|
|
9dc11cedbf | ||
|
|
22aab783d4 | ||
|
|
a2babb83ad | ||
|
|
76a7ceb758 | ||
|
|
9688acaa87 | ||
|
|
64339e5f03 | ||
|
|
1ceea3dcca | ||
|
|
e3c3283603 | ||
|
|
4ac02d3aac | ||
|
|
8eacfe045f | ||
|
|
15e246929b | ||
|
|
c1424634fb | ||
|
|
07ec3efbca | ||
|
|
9b07b10901 | ||
|
|
b1e9cdbea2 | ||
|
|
9aee630392 | ||
|
|
6b50f77624 | ||
|
|
16f1c286c4 | ||
|
|
64aab6dd82 | ||
|
|
144bb84bdc | ||
|
|
76260f9b22 | ||
|
|
500cd1f872 | ||
|
|
bfe56c3470 | ||
|
|
1dfa9431a9 |
@@ -737,6 +737,24 @@
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Eclipseop",
|
||||
"name": "Mackenzie",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5846213?v=4",
|
||||
"profile": "https://github.com/Eclipseop",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "s0up4200",
|
||||
"name": "soup",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18177310?v=4",
|
||||
"profile": "https://github.com/s0up4200",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
@@ -745,5 +763,6 @@
|
||||
"projectOwner": "sct",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": false
|
||||
"skipCi": false,
|
||||
"commitConvention": "angular"
|
||||
}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Upload Snap Package
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: overseerr-snap-package-${{ matrix.architecture }}
|
||||
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
- name: Review Snap Package
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
|
||||
2
.github/workflows/snap.yaml
vendored
2
.github/workflows/snap.yaml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Upload Snap Package
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: overseerr-snap-package-${{ matrix.architecture }}
|
||||
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
- name: Review Snap Package
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -67,3 +67,6 @@ tsconfig.tsbuildinfo
|
||||
|
||||
# Webstorm
|
||||
.idea
|
||||
|
||||
# Config Cache Directory
|
||||
config/cache
|
||||
|
||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,95 +1,25 @@
|
||||
## [1.2.1](https://github.com/fallenbagel/jellyseerr/compare/v1.2.0...v1.2.1) (2022-10-18)
|
||||
# [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** fix jellyfinHost to not be undefined ([ab09664](https://github.com/fallenbagel/jellyseerr/commit/ab09664d418e07198ac72e7f342b53ba441ab7c3)), closes [#237](https://github.com/fallenbagel/jellyseerr/issues/237)
|
||||
|
||||
# [1.2.0](https://github.com/fallenbagel/jellyseerr/compare/v1.1.1...v1.2.0) (2022-10-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** add rate limiter to TMDb requests to hopefully deal with 429s ([#2881](https://github.com/fallenbagel/jellyseerr/issues/2881)) ([aed1409](https://github.com/fallenbagel/jellyseerr/commit/aed1409f29d5b6360e87381d78dfeb4cc86d6fc6)), closes [#2853](https://github.com/fallenbagel/jellyseerr/issues/2853)
|
||||
* **api:** ignore filter if unset in media route ([#2647](https://github.com/fallenbagel/jellyseerr/issues/2647)) ([a6c1f3f](https://github.com/fallenbagel/jellyseerr/commit/a6c1f3f7ce498e32817cc8c74d439e8d99d6fbf4))
|
||||
* **api:** lookup shows using english title only ([#2911](https://github.com/fallenbagel/jellyseerr/issues/2911)) ([004e1bb](https://github.com/fallenbagel/jellyseerr/commit/004e1bb17e14bef1697b6b993f6ff92b77cdaeb4)), closes [#2801](https://github.com/fallenbagel/jellyseerr/issues/2801)
|
||||
* **api:** use correct path param type in openapi spec ([#2834](https://github.com/fallenbagel/jellyseerr/issues/2834)) ([6cd0c9b](https://github.com/fallenbagel/jellyseerr/commit/6cd0c9b2c81db2728ce09a9bc273a9aee366acbc))
|
||||
* **backend:** fixes Jellyfin/Emby links if server is initially setup with a trailing / ([6635701](https://github.com/fallenbagel/jellyseerr/commit/66357019f034ffac342ebd49a32bd75c95d4dec9)), closes [#168](https://github.com/fallenbagel/jellyseerr/issues/168) [#220](https://github.com/fallenbagel/jellyseerr/issues/220)
|
||||
* better ordering of RequestButton options & properly handle failed requests ([#2944](https://github.com/fallenbagel/jellyseerr/issues/2944)) ([c143c0b](https://github.com/fallenbagel/jellyseerr/commit/c143c0b8d285896a7ad5f42f2a7dc1e38b55cc3a))
|
||||
* check perms to view watchlist slider on user profile ([#2980](https://github.com/fallenbagel/jellyseerr/issues/2980)) ([5d73bc2](https://github.com/fallenbagel/jellyseerr/commit/5d73bc22389a2606ecc630f88b6ec2328f690975))
|
||||
* clicking outside modal closes modal again ([#2984](https://github.com/fallenbagel/jellyseerr/issues/2984)) ([1a00532](https://github.com/fallenbagel/jellyseerr/commit/1a0053221b58d198bdcca0249fcd76226122c6a2))
|
||||
* compatibility issue with safari ([#3019](https://github.com/fallenbagel/jellyseerr/issues/3019)) ([e486623](https://github.com/fallenbagel/jellyseerr/commit/e486623310b1c0de12a5b38bf934543fde250c37))
|
||||
* correct safe margin for slideover ([#2977](https://github.com/fallenbagel/jellyseerr/issues/2977)) ([23409e6](https://github.com/fallenbagel/jellyseerr/commit/23409e6f2ffb060e7914db28375e141a6087ddfa))
|
||||
* correct spacing on season header badges ([#2983](https://github.com/fallenbagel/jellyseerr/issues/2983)) ([c4aa08f](https://github.com/fallenbagel/jellyseerr/commit/c4aa08f5f0bcc856e993416936cc74e4db6aed5d))
|
||||
* **deps:** do not list email-validator as a devDependency ([9518cb3](https://github.com/fallenbagel/jellyseerr/commit/9518cb36357cd36b4b75439ac74ba910faa9a217))
|
||||
* **deps:** pin dependencies ([#2946](https://github.com/fallenbagel/jellyseerr/issues/2946)) [skip ci] ([103c4ca](https://github.com/fallenbagel/jellyseerr/commit/103c4ca49ca6f7a8101a18a540d3955a1ac6eae7))
|
||||
* **deps:** pin dependency @formatjs/intl-utils to 3.8.4 ([#2975](https://github.com/fallenbagel/jellyseerr/issues/2975)) [skip ci] ([baf1ea9](https://github.com/fallenbagel/jellyseerr/commit/baf1ea95a38537fab56cc324aa9359f40b615fe6))
|
||||
* **deps:** pin dependency @headlessui/react to v0.0.0-insiders.b301f04 ([#2993](https://github.com/fallenbagel/jellyseerr/issues/2993)) [skip ci] ([833f52d](https://github.com/fallenbagel/jellyseerr/commit/833f52de56cc4715673c5d2593f05528d260ae15))
|
||||
* **deps:** pin dependency cronstrue to 2.11.0 ([#3018](https://github.com/fallenbagel/jellyseerr/issues/3018)) [skip ci] ([f20ba3f](https://github.com/fallenbagel/jellyseerr/commit/f20ba3fc2e6cd42c2c73ba47812d9d177e75a8c2))
|
||||
* **deps:** pin dependency react-popper-tooltip to 4.4.2 ([#2952](https://github.com/fallenbagel/jellyseerr/issues/2952)) [skip ci] ([5247f14](https://github.com/fallenbagel/jellyseerr/commit/5247f14968c1f5a1a2efdc26f850cf81c0c04f0e))
|
||||
* do not display 'Request More' button if no requestable seasons ([#2998](https://github.com/fallenbagel/jellyseerr/issues/2998)) ([23f93e3](https://github.com/fallenbagel/jellyseerr/commit/23f93e311d3c4ed413b2fabca520b5c97869c9b8))
|
||||
* failure to load SearchByNameModal ([#3000](https://github.com/fallenbagel/jellyseerr/issues/3000)) ([410ad0d](https://github.com/fallenbagel/jellyseerr/commit/410ad0d4b40e2ae3ca315a9c808a886ab2c7bf2d))
|
||||
* fix play on Jellyfin/Emby button after previous merge ([3b0c091](https://github.com/fallenbagel/jellyseerr/commit/3b0c0915fb8e1ad671227f60cc181d52f226ad6b))
|
||||
* **frontend:** better request/media cards for items without valid TMDb IDs ([#2181](https://github.com/fallenbagel/jellyseerr/issues/2181)) ([9bc1f89](https://github.com/fallenbagel/jellyseerr/commit/9bc1f8977707f2d3383a7af8efa0521ced096777))
|
||||
* **frontend:** only allow 'request as' users w/ request perms ([#2991](https://github.com/fallenbagel/jellyseerr/issues/2991)) ([dbdecb1](https://github.com/fallenbagel/jellyseerr/commit/dbdecb1e0afb450e0cda6965a6d89408a831db0e))
|
||||
* **import statement:** import statement ([8724058](https://github.com/fallenbagel/jellyseerr/commit/8724058aa568b5eb96420c28f9082ca8a9ed714f))
|
||||
* issues and login page still had incorrect animations ([#2979](https://github.com/fallenbagel/jellyseerr/issues/2979)) ([095048d](https://github.com/fallenbagel/jellyseerr/commit/095048d94af28f25f88f956aba8e01d1199082f3))
|
||||
* **lang:** correct capitalization of 'TMDB' ([#2953](https://github.com/fallenbagel/jellyseerr/issues/2953)) ([9021696](https://github.com/fallenbagel/jellyseerr/commit/9021696cf085bd6387701811e64466e14893d1db))
|
||||
* **lang:** manage movie -> manage series ([#2963](https://github.com/fallenbagel/jellyseerr/issues/2963)) ([f5e6b62](https://github.com/fallenbagel/jellyseerr/commit/f5e6b620c133e38cf18978a619d3d4757bbab755))
|
||||
* log level value should not be case sensitive ([#2913](https://github.com/fallenbagel/jellyseerr/issues/2913)) ([6428b8d](https://github.com/fallenbagel/jellyseerr/commit/6428b8d4195c736a384441a761a746a35a3ad009))
|
||||
* new status indicators added to series list on mobile ([#3024](https://github.com/fallenbagel/jellyseerr/issues/3024)) ([407af32](https://github.com/fallenbagel/jellyseerr/commit/407af32d32a30d23ee14a9ff763cb4aa582d3ede))
|
||||
* only request Tautulli watch data for Plex media servers (to avoid error messages in logs) ([6dbb99e](https://github.com/fallenbagel/jellyseerr/commit/6dbb99e0b685c6a0e7c9cb62c300ceee85b43bf4))
|
||||
* **plex:** add container-size header to recently added api call ([#3023](https://github.com/fallenbagel/jellyseerr/issues/3023)) ([d8da5cb](https://github.com/fallenbagel/jellyseerr/commit/d8da5cbe9d0700e02cbece70caf9103bc5376505))
|
||||
* remove backdrop-blur class from warning buttons ([#3037](https://github.com/fallenbagel/jellyseerr/issues/3037)) ([430b1ab](https://github.com/fallenbagel/jellyseerr/commit/430b1ab871f8e3eefbebb37c74aa1ce3f0862efe))
|
||||
* remove failing ci job that builds a test copy to a private repo ([5f7e7ee](https://github.com/fallenbagel/jellyseerr/commit/5f7e7eef119c1ba2fb7b1cb8c5e06f372b92ac7f))
|
||||
* scroll restoration ([#3005](https://github.com/fallenbagel/jellyseerr/issues/3005)) ([14280c5](https://github.com/fallenbagel/jellyseerr/commit/14280c54370fd9a3e73120e4208e36183d39f9a3))
|
||||
* settings log modal when closing ([#2985](https://github.com/fallenbagel/jellyseerr/issues/2985)) ([4d56320](https://github.com/fallenbagel/jellyseerr/commit/4d563208709fe581ef6bf475e969197d586d907b))
|
||||
* sidebar close button placement when using PWA ([#3045](https://github.com/fallenbagel/jellyseerr/issues/3045)) ([21d20fd](https://github.com/fallenbagel/jellyseerr/commit/21d20fdfd61b7a5a2ec265d420aec103b1430a06))
|
||||
* start scheduled jobs on initial admin account setup ([b080251](https://github.com/fallenbagel/jellyseerr/commit/b08025195e1e1fd07e274b6846728770ec1d3e18)), closes [#170](https://github.com/fallenbagel/jellyseerr/issues/170)
|
||||
* transition animation ([#2974](https://github.com/fallenbagel/jellyseerr/issues/2974)) ([98028bf](https://github.com/fallenbagel/jellyseerr/commit/98028bf2f4d29175fa0922e744fee168849d653f))
|
||||
* **ui:** hide 'Recently Added' & 'Recent Requests' sliders when empty ([#2190](https://github.com/fallenbagel/jellyseerr/issues/2190)) ([03d5e56](https://github.com/fallenbagel/jellyseerr/commit/03d5e56678c3a372114a0256ee3431deee42cb57))
|
||||
* **ui:** hide null dates in episodes list ([#3035](https://github.com/fallenbagel/jellyseerr/issues/3035)) ([7404d68](https://github.com/fallenbagel/jellyseerr/commit/7404d68143e830df73b9d2779a6d7ea65bc9fd4f))
|
||||
* **ui:** minor fixes ([#3036](https://github.com/fallenbagel/jellyseerr/issues/3036)) ([f5c2fc1](https://github.com/fallenbagel/jellyseerr/commit/f5c2fc1c209b2d04f0e39a97d8b65bcac00667dc))
|
||||
* **ui:** remove 'all' badge from request cards ([#2992](https://github.com/fallenbagel/jellyseerr/issues/2992)) ([5c01313](https://github.com/fallenbagel/jellyseerr/commit/5c01313cc4c8277de398124848b4481a2b0080b3))
|
||||
* update Discord ID regex to include 19 digit IDs ([#2860](https://github.com/fallenbagel/jellyseerr/issues/2860)) ([9da8461](https://github.com/fallenbagel/jellyseerr/commit/9da84612254fe0c36b38bc49184cc1fc52ff6212))
|
||||
* use fallbackData to prepare user data during SSR ([#2968](https://github.com/fallenbagel/jellyseerr/issues/2968)) ([6e60a27](https://github.com/fallenbagel/jellyseerr/commit/6e60a275c7c2451beb1922d66eaa6c5411d36c3d))
|
||||
* use image.tmdb.org for setup/login backdrop images ([#2966](https://github.com/fallenbagel/jellyseerr/issues/2966)) ([3b26338](https://github.com/fallenbagel/jellyseerr/commit/3b2633812b5ca8cde010b3f8c2ea9a8646b33e1d))
|
||||
* username will not show undefined on cancel or delete ([#2982](https://github.com/fallenbagel/jellyseerr/issues/2982)) ([b925857](https://github.com/fallenbagel/jellyseerr/commit/b925857dfa27483558eab24bc5f20f654e294a08))
|
||||
* watch data not required to show Tautulli button ([#2976](https://github.com/fallenbagel/jellyseerr/issues/2976)) ([dd28200](https://github.com/fallenbagel/jellyseerr/commit/dd282000407ae9656640f38561a104916d12571a))
|
||||
* added deep links to issues and status badges ([#3065](https://github.com/fallenbagel/jellyseerr/issues/3065)) ([bfe56c3](https://github.com/fallenbagel/jellyseerr/commit/bfe56c347073001795b1c3e917eb7a5afcc4462c))
|
||||
* **api:** handle auth for accounts where the plex id may have been set to null ([#3125](https://github.com/fallenbagel/jellyseerr/issues/3125)) ([15e2469](https://github.com/fallenbagel/jellyseerr/commit/15e246929bdbc2b7b5bdab7a84bd7882b79d5cb1))
|
||||
* **api:** ignore Music,Books,Photos,MusicVideo libraries ([d9ca3c6](https://github.com/fallenbagel/jellyseerr/commit/d9ca3c6e52c118698ca71021217f6ca409e71974))
|
||||
* count combined episodes ([64339e5](https://github.com/fallenbagel/jellyseerr/commit/64339e5f0374f8490e685e5c086e088bb7fd737e))
|
||||
* improved PTR scrolling performance ([#3095](https://github.com/fallenbagel/jellyseerr/issues/3095)) ([07ec3ef](https://github.com/fallenbagel/jellyseerr/commit/07ec3efbcaf669de7ccde4421c1112bfd23675d6))
|
||||
* **locale:** fix the duplicated wording in the Clear Media Warning message ([7e20c7c](https://github.com/fallenbagel/jellyseerr/commit/7e20c7cb78a44c32ab8a5f21203e285f23f402ab))
|
||||
* **ui:** adds mediaServerName to statusBadge and manageSlideOver ([d0cdce9](https://github.com/fallenbagel/jellyseerr/commit/d0cdce9e90fba642d2bf934a4266e1421424bc73)), closes [#254](https://github.com/fallenbagel/jellyseerr/issues/254)
|
||||
* update API docs to allow 'all' seasons value ([#3073](https://github.com/fallenbagel/jellyseerr/issues/3073)) ([1dfa943](https://github.com/fallenbagel/jellyseerr/commit/1dfa9431a95e7e2a1843746c2473d8a06f03e184))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add 20th Century Studios to Studio Slider ([#2288](https://github.com/fallenbagel/jellyseerr/issues/2288)) ([b33956e](https://github.com/fallenbagel/jellyseerr/commit/b33956e6b85b2c2fc12e23951c06f36e009fb627))
|
||||
* **frontend:** a few more tooltips ([#2972](https://github.com/fallenbagel/jellyseerr/issues/2972)) ([815d709](https://github.com/fallenbagel/jellyseerr/commit/815d709bcfa4cca1528cc697defe9cd773ea0089))
|
||||
* **frontend:** add more tooltips ([#2961](https://github.com/fallenbagel/jellyseerr/issues/2961)) ([950b171](https://github.com/fallenbagel/jellyseerr/commit/950b1712b7449dc48a9c1589950907dd581f069a))
|
||||
* improved user dropdown ([#2969](https://github.com/fallenbagel/jellyseerr/issues/2969)) ([67f3a38](https://github.com/fallenbagel/jellyseerr/commit/67f3a3829e2f629e72ec3c32b042be2ef08c38e9))
|
||||
* **jobs:** show current job frequency in edit modal ([#3008](https://github.com/fallenbagel/jellyseerr/issues/3008)) ([99fc9a2](https://github.com/fallenbagel/jellyseerr/commit/99fc9a2da01b1628d5f849ce56f016c0ab26c3db))
|
||||
* **lang:** add Arabic and Lithuanian display languages ([#2916](https://github.com/fallenbagel/jellyseerr/issues/2916)) ([3db3044](https://github.com/fallenbagel/jellyseerr/commit/3db3044210316817144ecacd81be97f159d0df2b))
|
||||
* **lang:** translations update from Hosted Weblate ([#2659](https://github.com/fallenbagel/jellyseerr/issues/2659)) ([e939dc6](https://github.com/fallenbagel/jellyseerr/commit/e939dc678e124bd1f630c98d1d4927dba6372af5))
|
||||
* **lang:** translations update from Hosted Weblate ([#2915](https://github.com/fallenbagel/jellyseerr/issues/2915)) ([a0301e2](https://github.com/fallenbagel/jellyseerr/commit/a0301e2d83b2fca07f69a2c274d4745edef6d0cd))
|
||||
* **lang:** translations update from Hosted Weblate ([#2958](https://github.com/fallenbagel/jellyseerr/issues/2958)) ([29ab178](https://github.com/fallenbagel/jellyseerr/commit/29ab178fb0048c055a86bc40f7b47117903a3b2c))
|
||||
* **lang:** translations update from Hosted Weblate ([#2971](https://github.com/fallenbagel/jellyseerr/issues/2971)) ([2950cf4](https://github.com/fallenbagel/jellyseerr/commit/2950cf4438e2d10a9c0543a848519a113527fe91))
|
||||
* **lang:** translations update from Hosted Weblate ([#2999](https://github.com/fallenbagel/jellyseerr/issues/2999)) ([8672869](https://github.com/fallenbagel/jellyseerr/commit/867286996b805fefd3c16f2a1a05d5f2c10daced))
|
||||
* **lang:** translations update from Hosted Weblate ([#3006](https://github.com/fallenbagel/jellyseerr/issues/3006)) ([611ceeb](https://github.com/fallenbagel/jellyseerr/commit/611ceeb5f49ec4760f157eafb03b2dae465d476e))
|
||||
* **lang:** translations update from Hosted Weblate ([#3014](https://github.com/fallenbagel/jellyseerr/issues/3014)) ([3d458dd](https://github.com/fallenbagel/jellyseerr/commit/3d458dd2fdc8807f09af8b4c29bed794b3886f0f))
|
||||
* **lang:** translations update from Hosted Weblate ([#3026](https://github.com/fallenbagel/jellyseerr/issues/3026)) ([16cb53f](https://github.com/fallenbagel/jellyseerr/commit/16cb53f703a6c42b99aa776515f90b3ed153b382))
|
||||
* **language:** update czech language ([8619724](https://github.com/fallenbagel/jellyseerr/commit/8619724c652bfb0604373bcdb8c32a8e87a73fe7))
|
||||
* **logs:** add search filter ([#2505](https://github.com/fallenbagel/jellyseerr/issues/2505)) ([30141f7](https://github.com/fallenbagel/jellyseerr/commit/30141f76e025763bf79fd3c8fb344d45519d5d8d))
|
||||
* **notif:** auto-request notif type ([#2956](https://github.com/fallenbagel/jellyseerr/issues/2956)) ([6c0fd40](https://github.com/fallenbagel/jellyseerr/commit/6c0fd408779bc084698499bca861d042cd735d77))
|
||||
* **perms:** add new permission for viewing recently added media ([#2129](https://github.com/fallenbagel/jellyseerr/issues/2129)) ([a12697b](https://github.com/fallenbagel/jellyseerr/commit/a12697b06143e9a9c5c240c104faabdf2096ffd3))
|
||||
* plex deep links for iOS devices ([#2680](https://github.com/fallenbagel/jellyseerr/issues/2680)) ([575da30](https://github.com/fallenbagel/jellyseerr/commit/575da306b03eea3561de8d7dbe1b4b69674c7b2b))
|
||||
* plex watchlist sync integration ([#2885](https://github.com/fallenbagel/jellyseerr/issues/2885)) ([301f2bf](https://github.com/fallenbagel/jellyseerr/commit/301f2bf7ab0c5e7c5aef9d78a58d6449df0f55b8))
|
||||
* pull down to refresh ([#2908](https://github.com/fallenbagel/jellyseerr/issues/2908)) ([87825a0](https://github.com/fallenbagel/jellyseerr/commit/87825a0e058162e82634503c81deeff1a59634e5))
|
||||
* restore option to cache and optimize images locally ([#2964](https://github.com/fallenbagel/jellyseerr/issues/2964)) ([507227a](https://github.com/fallenbagel/jellyseerr/commit/507227aa496a60d1f46ce1a34c13bb162edc4bb6))
|
||||
* season/episode list on series details ([#2967](https://github.com/fallenbagel/jellyseerr/issues/2967)) ([8a2acb7](https://github.com/fallenbagel/jellyseerr/commit/8a2acb7f2bbe91feb3c6ee45c2066e3a036b83f9))
|
||||
* show alert/prompt when settings changes require restart ([#2401](https://github.com/fallenbagel/jellyseerr/issues/2401)) ([f3e56da](https://github.com/fallenbagel/jellyseerr/commit/f3e56da3b719285095a59d5a0e822087e095b709))
|
||||
* tooltip foundation ([#2950](https://github.com/fallenbagel/jellyseerr/issues/2950)) ([16545ee](https://github.com/fallenbagel/jellyseerr/commit/16545eec225ce942b55935019185a94e471fb93b))
|
||||
* **ui:** revalidate requests slider on discover page ([#2818](https://github.com/fallenbagel/jellyseerr/issues/2818)) ([91e0928](https://github.com/fallenbagel/jellyseerr/commit/91e0928aa0e1353431754750095cb64b93348c21))
|
||||
* user delete modal shows username and requires confirmation ([#2779](https://github.com/fallenbagel/jellyseerr/issues/2779)) ([36d17fe](https://github.com/fallenbagel/jellyseerr/commit/36d17fed6e4e1ca651a4e29f087b2abb53f794cf))
|
||||
* view other users' watchlists ([#2959](https://github.com/fallenbagel/jellyseerr/issues/2959)) ([0839718](https://github.com/fallenbagel/jellyseerr/commit/0839718806a04ee094445dd7276bba7f49424ab7))
|
||||
* **api:** adds support for Mixed Libraries ([ba82ece](https://github.com/fallenbagel/jellyseerr/commit/ba82ecec5c994e79d7c9b658372041522b58a120)), closes [#95](https://github.com/fallenbagel/jellyseerr/issues/95)
|
||||
* custom image proxy ([#3056](https://github.com/fallenbagel/jellyseerr/issues/3056)) ([500cd1f](https://github.com/fallenbagel/jellyseerr/commit/500cd1f872942923d2b9c3b835e6329e335d4a3f))
|
||||
* **lang:** add Croatian display language ([#3041](https://github.com/fallenbagel/jellyseerr/issues/3041)) ([64aab6d](https://github.com/fallenbagel/jellyseerr/commit/64aab6dd8240e191026512733b34cc046b6e508a))
|
||||
|
||||
# [1.1.1](https://github.com/fallenbagel/jellyseerr/compare/v1.1.0...v1.1.1) (2022-06-20)
|
||||
## [1.29.1](https://github.com/sct/overseerr/compare/v1.29.0...v1.29.1) (2022-04-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
||||
86
README.md
86
README.md
@@ -13,37 +13,105 @@ _The original Overseerr team have been busy and Jellyfin/Emby support aren't on
|
||||
|
||||
## Current Features
|
||||
|
||||
- Jellyfin Support
|
||||
- Emby Support
|
||||
|
||||
(Upcoming Features include: Multiple Server Instances, Music Support, Ability to change email address and much more!)
|
||||
|
||||
Along with all the existing Overseerr features:
|
||||
|
||||
- Full Plex integration. Authenticate and manage user access with Plex!
|
||||
- Full Jellyfin/Emby/Plex integration. Authenticate and manage user access with Jellyfin/Emby/Plex!
|
||||
- Supports Movies, Shows, Mixed Libraries!
|
||||
- Ability to change email addresses for smtp purposes
|
||||
- Ability to import all jellyfin/emby users
|
||||
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
|
||||
- Plex library scan, to keep track of the titles which are already available.
|
||||
- Jellyfin/Emby/Plex library scan, to keep track of the titles which are already available.
|
||||
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
|
||||
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
|
||||
- Granular permission system.
|
||||
- Support for various notification agents.
|
||||
- Mobile-friendly design, for when you need to approve requests on the go!
|
||||
|
||||
(Upcoming Features include: Multiple Server Instances, Music Support, and much more!)
|
||||
|
||||
With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested.
|
||||
|
||||
## Getting Started
|
||||
|
||||
#### Pre-requisite (Important)
|
||||
|
||||
_*On Jellyfin/Emby, ensure the `settings > Home > Automatically group content from the following folders into views such as 'Movies', 'Music' and 'TV'` is turned off*_
|
||||
|
||||
### Launching Jellyseerr using Docker
|
||||
|
||||
Check out our dockerhub for instructions on how to install and run Jellyseerr:
|
||||
https://hub.docker.com/r/fallenbagel/jellyseerr
|
||||
|
||||
### Launching Jellyseerr manually:
|
||||
|
||||
#### Windows
|
||||
|
||||
Pre-requisites:
|
||||
|
||||
- Nodejs (atleast LTS version)
|
||||
- Yarn
|
||||
- Download the source code from the github (Either develop branch or main for stable)
|
||||
|
||||
```bash
|
||||
npm i -g win-node-env
|
||||
yarn install
|
||||
yarn run build
|
||||
yarn start
|
||||
```
|
||||
|
||||
#### Linux
|
||||
|
||||
Pre-requisites:
|
||||
|
||||
- Nodejs (atleast LTS version)
|
||||
- Yarn
|
||||
- Git
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Fallenbagel/jellyseerr.git && cd jellyseerr
|
||||
git checkout main #if you want to run stable instead of develop
|
||||
yarn install
|
||||
yarn run build
|
||||
yarn start
|
||||
```
|
||||
|
||||
_Systemd-service:_
|
||||
|
||||
- assuming jellyseerr was cloned to `/opt/`
|
||||
and the environmentfile is located at `/etc/jellyseerr`
|
||||
|
||||
service:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Jellyseerr Service
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/jellyseerr/jellyseerr.conf
|
||||
Environment=NODE_ENV=production
|
||||
Type=exec
|
||||
Restart=on-failure
|
||||
WorkingDirectory=/opt/jellyseerr
|
||||
ExecStart=/root/.nvm/versions/node/v18.7.0/bin/node dist/index.js
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Environmentfile:
|
||||
|
||||
```
|
||||
# Jellyseerr's default port is 5055, if you want to use both, change this.
|
||||
# specify on which port to listen
|
||||
PORT=5055
|
||||
|
||||
# specify on which interface to listen, by default jellyseerr listens on all interfaces
|
||||
#HOST=127.0.0.1
|
||||
|
||||
# Uncomment if your media server is emby instead of jellyfin.
|
||||
# JELLYFIN_TYPE=emby
|
||||
```
|
||||
|
||||
### Packages:
|
||||
|
||||
Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'onnqy3',
|
||||
projectId: 'xkm1b4',
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:5055',
|
||||
experimentalSessionAndOrigin: true,
|
||||
|
||||
@@ -11,4 +11,4 @@ To use Fail2ban with Overseerr, create a new file named `overseerr.local` in you
|
||||
failregex = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"<HOST>"
|
||||
```
|
||||
|
||||
You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.
|
||||
You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documentation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.
|
||||
|
||||
@@ -40,6 +40,14 @@ If you enable this setting and find yourself unable to access Overseerr, you can
|
||||
|
||||
This setting is **disabled** by default.
|
||||
|
||||
### Enable Image Caching
|
||||
|
||||
When enabled, Overseerr will proxy and cache images from pre-configured sources (such as TMDB). This can use a significant amount of disk space.
|
||||
|
||||
Images are saved in the `config/cache/images` and stale images are cleared out every 24 hours.
|
||||
|
||||
You should enable this if you are having issues with loading images directly from TMDB in your browser.
|
||||
|
||||
### Display Language
|
||||
|
||||
Set the default display language for Overseerr. Users can override this setting in their user settings.
|
||||
|
||||
@@ -2667,29 +2667,44 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: cache-id
|
||||
name:
|
||||
type: string
|
||||
example: cache name
|
||||
stats:
|
||||
type: object
|
||||
properties:
|
||||
imageCache:
|
||||
type: object
|
||||
properties:
|
||||
tmdb:
|
||||
type: object
|
||||
properties:
|
||||
size:
|
||||
type: number
|
||||
example: 123456
|
||||
imageCount:
|
||||
type: number
|
||||
example: 123
|
||||
apiCaches:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
hits:
|
||||
type: number
|
||||
misses:
|
||||
type: number
|
||||
keys:
|
||||
type: number
|
||||
ksize:
|
||||
type: number
|
||||
vsize:
|
||||
type: number
|
||||
id:
|
||||
type: string
|
||||
example: cache-id
|
||||
name:
|
||||
type: string
|
||||
example: cache name
|
||||
stats:
|
||||
type: object
|
||||
properties:
|
||||
hits:
|
||||
type: number
|
||||
misses:
|
||||
type: number
|
||||
keys:
|
||||
type: number
|
||||
ksize:
|
||||
type: number
|
||||
vsize:
|
||||
type: number
|
||||
/settings/cache/{cacheId}/flush:
|
||||
post:
|
||||
summary: Flush a specific cache
|
||||
@@ -4838,9 +4853,13 @@ paths:
|
||||
type: number
|
||||
example: 123
|
||||
seasons:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
oneOf:
|
||||
- type: array
|
||||
items:
|
||||
type: number
|
||||
minimum: 1
|
||||
- type: string
|
||||
enum: [all]
|
||||
is4k:
|
||||
type: boolean
|
||||
example: false
|
||||
@@ -4919,7 +4938,7 @@ paths:
|
||||
$ref: '#/components/schemas/MediaRequest'
|
||||
put:
|
||||
summary: Update MediaRequest
|
||||
description: Updates a specific media request and returns the request in a JSON object.. Requires the `MANAGE_REQUESTS` permission.
|
||||
description: Updates a specific media request and returns the request in a JSON object. Requires the `MANAGE_REQUESTS` permission.
|
||||
tags:
|
||||
- request
|
||||
parameters:
|
||||
@@ -4930,6 +4949,37 @@ paths:
|
||||
example: '1'
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
mediaType:
|
||||
type: string
|
||||
enum: [movie, tv]
|
||||
seasons:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
minimum: 1
|
||||
is4k:
|
||||
type: boolean
|
||||
example: false
|
||||
serverId:
|
||||
type: number
|
||||
profileId:
|
||||
type: number
|
||||
rootFolder:
|
||||
type: string
|
||||
languageProfileId:
|
||||
type: number
|
||||
userId:
|
||||
type: number
|
||||
nullable: true
|
||||
required:
|
||||
- mediaType
|
||||
responses:
|
||||
'200':
|
||||
description: Succesfully updated request
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellyseerr",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
|
||||
@@ -225,7 +225,7 @@
|
||||
{
|
||||
"path": "semantic-release-docker-buildx",
|
||||
"buildArgs": {
|
||||
"COMMIT_TAG": "$GITHUB_SHA"
|
||||
"COMMIT_TAG": "$GIT_SHA"
|
||||
},
|
||||
"imageNames": [
|
||||
"fallenbagel/jellyseerr"
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface JellyfinLibraryItem {
|
||||
SeasonId?: string;
|
||||
SeasonName?: string;
|
||||
IndexNumber?: number;
|
||||
IndexNumberEnd?: number;
|
||||
ParentIndexNumber?: number;
|
||||
MediaType: string;
|
||||
}
|
||||
@@ -178,8 +179,10 @@ class JellyfinAPI {
|
||||
(Item: any) => {
|
||||
return (
|
||||
Item.Type === 'CollectionFolder' &&
|
||||
(Item.CollectionType === 'tvshows' ||
|
||||
Item.CollectionType === 'movies')
|
||||
Item.CollectionType !== 'music' &&
|
||||
Item.CollectionType !== 'books' &&
|
||||
Item.CollectionType !== 'musicvideos' &&
|
||||
Item.CollectionType !== 'homevideos'
|
||||
);
|
||||
}
|
||||
).map((Item: any) => {
|
||||
@@ -204,7 +207,7 @@ class JellyfinAPI {
|
||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||
try {
|
||||
const contents = await this.axios.get<any>(
|
||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}`
|
||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}`
|
||||
);
|
||||
|
||||
return contents.data.Items.filter(
|
||||
|
||||
@@ -205,8 +205,8 @@ class Media {
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost!.endsWith('/')
|
||||
? jellyfinHost!.slice(0, -1)
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
|
||||
if (this.jellyfinMediaId) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export class User {
|
||||
return users.map((u) => u.filter(showFiltered));
|
||||
}
|
||||
|
||||
static readonly filteredFields: string[] = ['email'];
|
||||
static readonly filteredFields: string[] = ['email', 'plexId'];
|
||||
|
||||
public displayName: string;
|
||||
|
||||
@@ -76,7 +76,7 @@ export class User {
|
||||
@Column({ type: 'integer', default: UserType.PLEX })
|
||||
public userType: UserType;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({ nullable: true, select: true })
|
||||
public plexId?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
|
||||
@@ -17,6 +17,7 @@ import WebPushAgent from '@server/lib/notifications/agents/webpush';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import routes from '@server/routes';
|
||||
import imageproxy from '@server/routes/imageproxy';
|
||||
import { getAppVersion } from '@server/utils/appVersion';
|
||||
import restartFlag from '@server/utils/restartFlag';
|
||||
import { getClientIp } from '@supercharge/request-ip';
|
||||
@@ -186,6 +187,9 @@ app
|
||||
next();
|
||||
});
|
||||
server.use('/api/v1', routes);
|
||||
|
||||
server.use('/imageproxy', imageproxy);
|
||||
|
||||
server.get('*', (req, res) => handle(req, res));
|
||||
server.use(
|
||||
(
|
||||
|
||||
@@ -54,6 +54,11 @@ export interface CacheItem {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CacheResponse {
|
||||
apiCaches: CacheItem[];
|
||||
imageCache: Record<'tmdb', { size: number; imageCount: number }>;
|
||||
}
|
||||
|
||||
export interface StatusResponse {
|
||||
version: string;
|
||||
commitTag: string;
|
||||
|
||||
@@ -257,8 +257,19 @@ class JobJellyfinSync {
|
||||
//use for loop to make sure this loop _completes_ in full
|
||||
//before the next section
|
||||
for (const episode of episodes) {
|
||||
let episodeCount = 1;
|
||||
|
||||
// count number of combined episodes
|
||||
if (
|
||||
episode.IndexNumber !== undefined &&
|
||||
episode.IndexNumberEnd !== undefined
|
||||
) {
|
||||
episodeCount =
|
||||
episode.IndexNumberEnd - episode.IndexNumber + 1;
|
||||
}
|
||||
|
||||
if (!this.enable4kShow) {
|
||||
totalStandard++;
|
||||
totalStandard += episodeCount;
|
||||
} else {
|
||||
const ExtendedEpisodeData = await this.jfClient.getItemData(
|
||||
episode.Id
|
||||
@@ -268,10 +279,10 @@ class JobJellyfinSync {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
if (MediaStream.Type === 'Video') {
|
||||
if (MediaStream.Width ?? 0 < 2000) {
|
||||
totalStandard++;
|
||||
totalStandard += episodeCount;
|
||||
}
|
||||
} else {
|
||||
total4k++;
|
||||
total4k += episodeCount;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import { plexFullScanner, plexRecentScanner } from '@server/lib/scanners/plex';
|
||||
import { radarrScanner } from '@server/lib/scanners/radarr';
|
||||
import { sonarrScanner } from '@server/lib/scanners/sonarr';
|
||||
@@ -181,5 +182,21 @@ export const startJobs = (): void => {
|
||||
}),
|
||||
});
|
||||
|
||||
// Run image cache cleanup every 5 minutes
|
||||
scheduledJobs.push({
|
||||
id: 'image-cache-cleanup',
|
||||
name: 'Image Cache Cleanup',
|
||||
type: 'process',
|
||||
interval: 'long',
|
||||
cronSchedule: jobs['image-cache-cleanup'].schedule,
|
||||
job: schedule.scheduleJob(jobs['image-cache-cleanup'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Image Cache Cleanup', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
// Clean TMDB image cache
|
||||
ImageProxy.clearCache('tmdb');
|
||||
}),
|
||||
});
|
||||
|
||||
logger.info('Scheduled jobs loaded', { label: 'Jobs' });
|
||||
};
|
||||
|
||||
268
server/lib/imageproxy.ts
Normal file
268
server/lib/imageproxy.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import logger from '@server/logger';
|
||||
import axios from 'axios';
|
||||
import rateLimit, { type rateLimitOptions } from 'axios-rate-limit';
|
||||
import { createHash } from 'crypto';
|
||||
import { promises } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
|
||||
type ImageResponse = {
|
||||
meta: {
|
||||
revalidateAfter: number;
|
||||
curRevalidate: number;
|
||||
isStale: boolean;
|
||||
etag: string;
|
||||
extension: string;
|
||||
cacheKey: string;
|
||||
cacheMiss: boolean;
|
||||
};
|
||||
imageBuffer: Buffer;
|
||||
};
|
||||
|
||||
class ImageProxy {
|
||||
public static async clearCache(key: string) {
|
||||
let deletedImages = 0;
|
||||
const cacheDirectory = path.join(
|
||||
__dirname,
|
||||
'../../config/cache/images/',
|
||||
key
|
||||
);
|
||||
|
||||
const files = await promises.readdir(cacheDirectory);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(cacheDirectory, file);
|
||||
const stat = await promises.lstat(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const imageFiles = await promises.readdir(filePath);
|
||||
|
||||
for (const imageFile of imageFiles) {
|
||||
const [, expireAtSt] = imageFile.split('.');
|
||||
const expireAt = Number(expireAtSt);
|
||||
const now = Date.now();
|
||||
|
||||
if (now > expireAt) {
|
||||
await promises.rm(path.join(filePath, imageFile));
|
||||
deletedImages += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Cleared ${deletedImages} stale image(s) from cache`, {
|
||||
label: 'Image Cache',
|
||||
});
|
||||
}
|
||||
|
||||
public static async getImageStats(
|
||||
key: string
|
||||
): Promise<{ size: number; imageCount: number }> {
|
||||
const cacheDirectory = path.join(
|
||||
__dirname,
|
||||
'../../config/cache/images/',
|
||||
key
|
||||
);
|
||||
|
||||
const imageTotalSize = await ImageProxy.getDirectorySize(cacheDirectory);
|
||||
const imageCount = await ImageProxy.getImageCount(cacheDirectory);
|
||||
|
||||
return {
|
||||
size: imageTotalSize,
|
||||
imageCount,
|
||||
};
|
||||
}
|
||||
|
||||
private static async getDirectorySize(dir: string): Promise<number> {
|
||||
const files = await promises.readdir(dir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
const paths = files.map(async (file) => {
|
||||
const path = join(dir, file.name);
|
||||
|
||||
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
|
||||
|
||||
if (file.isFile()) {
|
||||
const { size } = await promises.stat(path);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (await Promise.all(paths))
|
||||
.flat(Infinity)
|
||||
.reduce((i, size) => i + size, 0);
|
||||
}
|
||||
|
||||
private static async getImageCount(dir: string) {
|
||||
const files = await promises.readdir(dir);
|
||||
|
||||
return files.length;
|
||||
}
|
||||
|
||||
private axios;
|
||||
private cacheVersion;
|
||||
private key;
|
||||
|
||||
constructor(
|
||||
key: string,
|
||||
baseUrl: string,
|
||||
options: {
|
||||
cacheVersion?: number;
|
||||
rateLimitOptions?: rateLimitOptions;
|
||||
} = {}
|
||||
) {
|
||||
this.cacheVersion = options.cacheVersion ?? 1;
|
||||
this.key = key;
|
||||
this.axios = axios.create({
|
||||
baseURL: baseUrl,
|
||||
});
|
||||
|
||||
if (options.rateLimitOptions) {
|
||||
this.axios = rateLimit(this.axios, options.rateLimitOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public async getImage(path: string): Promise<ImageResponse> {
|
||||
const cacheKey = this.getCacheKey(path);
|
||||
|
||||
const imageResponse = await this.get(cacheKey);
|
||||
|
||||
if (!imageResponse) {
|
||||
const newImage = await this.set(path, cacheKey);
|
||||
|
||||
if (!newImage) {
|
||||
throw new Error('Failed to load image');
|
||||
}
|
||||
|
||||
return newImage;
|
||||
}
|
||||
|
||||
// If the image is stale, we will revalidate it in the background.
|
||||
if (imageResponse.meta.isStale) {
|
||||
this.set(path, cacheKey);
|
||||
}
|
||||
|
||||
return imageResponse;
|
||||
}
|
||||
|
||||
private async get(cacheKey: string): Promise<ImageResponse | null> {
|
||||
try {
|
||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||
const files = await promises.readdir(directory);
|
||||
const now = Date.now();
|
||||
|
||||
for (const file of files) {
|
||||
const [maxAgeSt, expireAtSt, etag, extension] = file.split('.');
|
||||
const buffer = await promises.readFile(join(directory, file));
|
||||
const expireAt = Number(expireAtSt);
|
||||
const maxAge = Number(maxAgeSt);
|
||||
|
||||
return {
|
||||
meta: {
|
||||
curRevalidate: maxAge,
|
||||
revalidateAfter: maxAge * 1000 + now,
|
||||
isStale: now > expireAt,
|
||||
etag,
|
||||
extension,
|
||||
cacheKey,
|
||||
cacheMiss: false,
|
||||
},
|
||||
imageBuffer: buffer,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// No files. Treat as empty cache.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async set(
|
||||
path: string,
|
||||
cacheKey: string
|
||||
): Promise<ImageResponse | null> {
|
||||
try {
|
||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||
const response = await this.axios.get(path, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
const buffer = Buffer.from(response.data, 'binary');
|
||||
const extension = path.split('.').pop() ?? '';
|
||||
const maxAge = Number(response.headers['cache-control'].split('=')[1]);
|
||||
const expireAt = Date.now() + maxAge * 1000;
|
||||
const etag = response.headers.etag.replace(/"/g, '');
|
||||
|
||||
await this.writeToCacheDir(
|
||||
directory,
|
||||
extension,
|
||||
maxAge,
|
||||
expireAt,
|
||||
buffer,
|
||||
etag
|
||||
);
|
||||
|
||||
return {
|
||||
meta: {
|
||||
curRevalidate: maxAge,
|
||||
revalidateAfter: expireAt,
|
||||
isStale: false,
|
||||
etag,
|
||||
extension,
|
||||
cacheKey,
|
||||
cacheMiss: true,
|
||||
},
|
||||
imageBuffer: buffer,
|
||||
};
|
||||
} catch (e) {
|
||||
logger.debug('Something went wrong caching image.', {
|
||||
label: 'Image Cache',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async writeToCacheDir(
|
||||
dir: string,
|
||||
extension: string,
|
||||
maxAge: number,
|
||||
expireAt: number,
|
||||
buffer: Buffer,
|
||||
etag: string
|
||||
) {
|
||||
const filename = join(dir, `${maxAge}.${expireAt}.${etag}.${extension}`);
|
||||
|
||||
await promises.rm(dir, { force: true, recursive: true }).catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
|
||||
await promises.mkdir(dir, { recursive: true });
|
||||
await promises.writeFile(filename, buffer);
|
||||
}
|
||||
|
||||
private getCacheKey(path: string) {
|
||||
return this.getHash([this.key, this.cacheVersion, path]);
|
||||
}
|
||||
|
||||
private getHash(items: (string | number | Buffer)[]) {
|
||||
const hash = createHash('sha256');
|
||||
for (const item of items) {
|
||||
if (typeof item === 'number') hash.update(String(item));
|
||||
else {
|
||||
hash.update(item);
|
||||
}
|
||||
}
|
||||
// See https://en.wikipedia.org/wiki/Base64#Filenames
|
||||
return hash.digest('base64').replace(/\//g, '-');
|
||||
}
|
||||
|
||||
private getCacheDirectory() {
|
||||
return path.join(__dirname, '../../config/cache/images/', this.key);
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageProxy;
|
||||
@@ -38,7 +38,7 @@ export interface PlexSettings {
|
||||
|
||||
export interface JellyfinSettings {
|
||||
name: string;
|
||||
hostname?: string;
|
||||
hostname: string;
|
||||
externalHostname?: string;
|
||||
libraries: Library[];
|
||||
serverId: string;
|
||||
@@ -263,7 +263,8 @@ export type JobId =
|
||||
| 'download-sync'
|
||||
| 'download-sync-reset'
|
||||
| 'jellyfin-recently-added-sync'
|
||||
| 'jellyfin-full-sync';
|
||||
| 'jellyfin-full-sync'
|
||||
| 'image-cache-cleanup';
|
||||
|
||||
interface AllSettings {
|
||||
clientId: string;
|
||||
@@ -446,6 +447,9 @@ class Settings {
|
||||
'jellyfin-full-sync': {
|
||||
schedule: '0 0 3 * * *',
|
||||
},
|
||||
'image-cache-cleanup': {
|
||||
schedule: '0 0 5 * * *',
|
||||
},
|
||||
},
|
||||
};
|
||||
if (initialSettings) {
|
||||
|
||||
@@ -89,13 +89,28 @@ authRoutes.post('/plex', async (req, res, next) => {
|
||||
await userRepository.save(user);
|
||||
} else {
|
||||
const mainUser = await userRepository.findOneOrFail({
|
||||
select: { id: true, plexToken: true, plexId: true },
|
||||
select: { id: true, plexToken: true, plexId: true, email: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
||||
|
||||
if (!account.id) {
|
||||
logger.error('Plex ID was missing from Plex.tv response', {
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
email: account.email,
|
||||
plexUsername: account.username,
|
||||
});
|
||||
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong. Try again.',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
account.id === mainUser.plexId ||
|
||||
(account.email === mainUser.email && !mainUser.plexId) ||
|
||||
(await mainPlexTv.checkUserAccess(account.id))
|
||||
) {
|
||||
if (user) {
|
||||
@@ -226,7 +241,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
const hostname =
|
||||
settings.jellyfin.hostname !== ''
|
||||
? settings.jellyfin.hostname
|
||||
: body.hostname;
|
||||
: body.hostname ?? '';
|
||||
const { externalHostname } = getSettings().jellyfin;
|
||||
|
||||
// Try to find deviceId that corresponds to jellyfin user, else generate a new one
|
||||
@@ -249,8 +264,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost!.endsWith('/')
|
||||
? jellyfinHost!.slice(0, -1)
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
|
||||
const account = await jellyfinserver.login(body.username, body.password);
|
||||
|
||||
39
server/routes/imageproxy.ts
Normal file
39
server/routes/imageproxy.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import logger from '@server/logger';
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
const tmdbImageProxy = new ImageProxy('tmdb', 'https://image.tmdb.org', {
|
||||
rateLimitOptions: {
|
||||
maxRequests: 20,
|
||||
maxRPS: 50,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Image Proxy
|
||||
*/
|
||||
router.get('/*', async (req, res) => {
|
||||
const imagePath = req.path.replace('/image', '');
|
||||
try {
|
||||
const imageData = await tmdbImageProxy.getImage(imagePath);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': `image/${imageData.meta.extension}`,
|
||||
'Content-Length': imageData.imageBuffer.length,
|
||||
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
|
||||
'OS-Cache-Key': imageData.meta.cacheKey,
|
||||
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
|
||||
});
|
||||
|
||||
res.end(imageData.imageBuffer);
|
||||
} catch (e) {
|
||||
logger.error('Failed to proxy image', {
|
||||
imagePath,
|
||||
errorMessage: e.message,
|
||||
});
|
||||
res.status(500).send();
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -16,9 +16,10 @@ import { jobJellyfinFullSync } from '@server/job/jellyfinsync';
|
||||
import { scheduledJobs } from '@server/job/schedule';
|
||||
import type { AvailableCacheIds } from '@server/lib/cache';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
import { plexFullScanner } from '@server/lib/scanners/plex';
|
||||
import type { Library, MainSettings } from '@server/lib/settings';
|
||||
import type { JobId, Library, MainSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { isAuthenticated } from '@server/middleware/auth';
|
||||
@@ -312,8 +313,8 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost!.endsWith('/')
|
||||
? jellyfinHost!.slice(0, -1)
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
@@ -604,7 +605,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => {
|
||||
});
|
||||
});
|
||||
|
||||
settingsRoutes.post<{ jobId: string }>(
|
||||
settingsRoutes.post<{ jobId: JobId }>(
|
||||
'/jobs/:jobId/cancel',
|
||||
(req, res, next) => {
|
||||
const scheduledJob = scheduledJobs.find(
|
||||
@@ -631,7 +632,7 @@ settingsRoutes.post<{ jobId: string }>(
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.post<{ jobId: string }>(
|
||||
settingsRoutes.post<{ jobId: JobId }>(
|
||||
'/jobs/:jobId/schedule',
|
||||
(req, res, next) => {
|
||||
const scheduledJob = scheduledJobs.find(
|
||||
@@ -666,16 +667,23 @@ settingsRoutes.post<{ jobId: string }>(
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.get('/cache', (req, res) => {
|
||||
const caches = cacheManager.getAllCaches();
|
||||
settingsRoutes.get('/cache', async (_req, res) => {
|
||||
const cacheManagerCaches = cacheManager.getAllCaches();
|
||||
|
||||
return res.status(200).json(
|
||||
Object.values(caches).map((cache) => ({
|
||||
id: cache.id,
|
||||
name: cache.name,
|
||||
stats: cache.getStats(),
|
||||
}))
|
||||
);
|
||||
const apiCaches = Object.values(cacheManagerCaches).map((cache) => ({
|
||||
id: cache.id,
|
||||
name: cache.name,
|
||||
stats: cache.getStats(),
|
||||
}));
|
||||
|
||||
const tmdbImageCache = await ImageProxy.getImageStats('tmdb');
|
||||
|
||||
return res.status(200).json({
|
||||
apiCaches,
|
||||
imageCache: {
|
||||
tmdb: tmdbImageCache,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
settingsRoutes.post<{ cacheId: AvailableCacheIds }>(
|
||||
|
||||
@@ -502,8 +502,8 @@ router.post(
|
||||
? externalHostname
|
||||
: hostname;
|
||||
|
||||
jellyfinHost = jellyfinHost!.endsWith('/')
|
||||
? jellyfinHost!.slice(0, -1)
|
||||
jellyfinHost = jellyfinHost.endsWith('/')
|
||||
? jellyfinHost.slice(0, -1)
|
||||
: jellyfinHost;
|
||||
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||
const jellyfinUsers = await jellyfinClient.getUsers();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
name: overseerr
|
||||
adopt-info: overseerr
|
||||
name: jellyseerr
|
||||
adopt-info: jellyseerr
|
||||
license: MIT
|
||||
summary: Request management and media discovery tool for the Plex ecosystem.
|
||||
summary: Request management and media discovery tool for media servers
|
||||
description: >
|
||||
Overseerr is a free and open source software application for managing requests for your media library.
|
||||
It integrates with your existing services such as Sonarr, Radarr and Plex!
|
||||
Jellyseerr is a free and open source software application for managing requests for your media library.
|
||||
It is a a fork of Overseerr built to bring support for & focusing mainly on Jellyfin & Emby media servers!
|
||||
It integrates with your existing services such as Sonarr, Radarr, and Jellyfin/Emby/Plex.
|
||||
base: core18
|
||||
confinement: strict
|
||||
|
||||
@@ -14,7 +15,7 @@ architectures:
|
||||
- build-on: armhf
|
||||
|
||||
parts:
|
||||
overseerr:
|
||||
jellyseerr:
|
||||
plugin: nodejs
|
||||
nodejs-version: '16.17.0'
|
||||
nodejs-package-manager: 'yarn'
|
||||
@@ -36,7 +37,7 @@ parts:
|
||||
override-pull: |
|
||||
snapcraftctl pull
|
||||
# Get information to determine snap grade and version
|
||||
git config --global --add safe.directory /data/parts/overseerr/src
|
||||
git config --global --add safe.directory /data/parts/jellyyseerr/src
|
||||
#setup yarn.rc
|
||||
echo "--install.frozen-lockfile\n--install.network-timeout 1000000" > .yarnrc
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import type { ImageProps } from 'next/image';
|
||||
import type { ImageLoader, ImageProps } from 'next/image';
|
||||
import Image from 'next/image';
|
||||
|
||||
const imageLoader: ImageLoader = ({ src }) => src;
|
||||
|
||||
/**
|
||||
* The CachedImage component should be used wherever
|
||||
* we want to offer the option to locally cache images.
|
||||
*
|
||||
* It uses the `next/image` Image component but overrides
|
||||
* the `unoptimized` prop based on the application setting `cacheImages`.
|
||||
**/
|
||||
const CachedImage = (props: ImageProps) => {
|
||||
const CachedImage = ({ src, ...props }: ImageProps) => {
|
||||
const { currentSettings } = useSettings();
|
||||
|
||||
return <Image unoptimized={!currentSettings.cacheImages} {...props} />;
|
||||
let imageUrl = src;
|
||||
|
||||
if (typeof imageUrl === 'string' && imageUrl.startsWith('http')) {
|
||||
const parsedUrl = new URL(imageUrl);
|
||||
|
||||
if (parsedUrl.host === 'image.tmdb.org' && currentSettings.cacheImages) {
|
||||
imageUrl = imageUrl.replace('https://image.tmdb.org', '/imageproxy');
|
||||
}
|
||||
}
|
||||
|
||||
return <Image unoptimized loader={imageLoader} src={imageUrl} {...props} />;
|
||||
};
|
||||
|
||||
export default CachedImage;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CachedImage from '@app/components/Common/CachedImage';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -30,11 +31,15 @@ const CompanyCard = ({ image, url, name }: CompanyCardProps) => {
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={name}
|
||||
className="relative z-40 max-h-full max-w-full"
|
||||
/>
|
||||
<div className="relative h-full w-full">
|
||||
<CachedImage
|
||||
src={image}
|
||||
alt={name}
|
||||
className="relative z-40 h-full w-full"
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 z-0 h-12 rounded-b-xl bg-gradient-to-t ${
|
||||
isHovered ? 'from-gray-800' : 'from-gray-900'
|
||||
|
||||
@@ -7,6 +7,7 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import IssueComment from '@app/components/IssueDetails/IssueComment';
|
||||
import IssueDescription from '@app/components/IssueDetails/IssueDescription';
|
||||
import { issueOptions } from '@app/components/IssueModal/constants';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
@@ -91,6 +92,13 @@ const IssueDetails = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { mediaUrl, mediaUrl4k } = useDeepLinks({
|
||||
mediaUrl: data?.mediaInfo?.mediaUrl,
|
||||
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
|
||||
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
const CommentSchema = Yup.object().shape({
|
||||
message: Yup.string().required(),
|
||||
});
|
||||
@@ -359,7 +367,7 @@ const IssueDetails = () => {
|
||||
{issueData?.media.mediaUrl && (
|
||||
<Button
|
||||
as="a"
|
||||
href={issueData?.media.mediaUrl}
|
||||
href={mediaUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full"
|
||||
@@ -405,7 +413,7 @@ const IssueDetails = () => {
|
||||
{issueData?.media.mediaUrl4k && (
|
||||
<Button
|
||||
as="a"
|
||||
href={issueData?.media.mediaUrl4k}
|
||||
href={mediaUrl4k}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full"
|
||||
@@ -621,7 +629,7 @@ const IssueDetails = () => {
|
||||
{issueData?.media.mediaUrl && (
|
||||
<Button
|
||||
as="a"
|
||||
href={issueData?.media.mediaUrl}
|
||||
href={mediaUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full"
|
||||
@@ -667,7 +675,7 @@ const IssueDetails = () => {
|
||||
{issueData?.media.mediaUrl4k && (
|
||||
<Button
|
||||
as="a"
|
||||
href={issueData?.media.mediaUrl4k}
|
||||
href={mediaUrl4k}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full"
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import axios from 'axios';
|
||||
import getConfig from 'next/config';
|
||||
import Link from 'next/link';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
@@ -30,7 +31,7 @@ const messages = defineMessages({
|
||||
manageModalNoRequests: 'No requests.',
|
||||
manageModalClearMedia: 'Clear Data',
|
||||
manageModalClearMediaWarning:
|
||||
'* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.',
|
||||
'* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your {mediaServerName} library, the media information will be recreated during the next scan.',
|
||||
openarr: 'Open in {arr}',
|
||||
openarr4k: 'Open in 4K {arr}',
|
||||
downloadstatus: 'Downloads',
|
||||
@@ -79,6 +80,7 @@ const ManageSlideOver = ({
|
||||
const { user: currentUser, hasPermission } = useUser();
|
||||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const { data: watchData } = useSWR<MediaWatchDataResponse>(
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
|
||||
data.mediaInfo &&
|
||||
@@ -505,6 +507,13 @@ const ManageSlideOver = ({
|
||||
mediaType: intl.formatMessage(
|
||||
mediaType === 'movie' ? messages.movie : messages.tvshow
|
||||
),
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? 'Plex'
|
||||
: 'Jellyfin',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import TitleCard from '@app/components/TitleCard';
|
||||
import { ArrowCircleRightIcon } from '@heroicons/react/solid';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -15,6 +17,18 @@ interface ShowMoreCardProps {
|
||||
const ShowMoreCard = ({ url, posters }: ShowMoreCardProps) => {
|
||||
const intl = useIntl();
|
||||
const [isHovered, setHovered] = useState(false);
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
});
|
||||
|
||||
if (!inView) {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<TitleCard.Placeholder />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={url}>
|
||||
<a
|
||||
|
||||
@@ -18,6 +18,7 @@ import PersonCard from '@app/components/PersonCard';
|
||||
import RequestButton from '@app/components/RequestButton';
|
||||
import Slider from '@app/components/Slider';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
@@ -129,31 +130,12 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
setShowManager(router.query.manage == '1' ? true : false);
|
||||
}, [router.query.manage]);
|
||||
|
||||
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.mediaUrl);
|
||||
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.mediaUrl4k);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
|
||||
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
|
||||
) {
|
||||
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
|
||||
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
|
||||
} else {
|
||||
setPlexUrl(data.mediaInfo?.mediaUrl);
|
||||
setPlexUrl4k(data.mediaInfo?.mediaUrl4k);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
data?.mediaInfo?.iOSPlexUrl,
|
||||
data?.mediaInfo?.iOSPlexUrl4k,
|
||||
data?.mediaInfo?.mediaUrl,
|
||||
data?.mediaInfo?.mediaUrl4k,
|
||||
settings.currentSettings.mediaServerType,
|
||||
]);
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: data?.mediaInfo?.mediaUrl,
|
||||
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
|
||||
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
@@ -378,7 +360,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
||||
}
|
||||
tmdbId={data.mediaInfo?.tmdbId}
|
||||
mediaType="movie"
|
||||
plexUrl={plexUrl}
|
||||
plexUrl={plexUrl4k}
|
||||
serviceUrl={data.mediaInfo?.serviceUrl4k}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { RefreshIcon } from '@heroicons/react/outline';
|
||||
import Router from 'next/router';
|
||||
import { useRouter } from 'next/router';
|
||||
import PR from 'pulltorefreshjs';
|
||||
import { useEffect } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
const PullToRefresh: React.FC = () => {
|
||||
const PullToRefresh = () => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
PR.init({
|
||||
mainElement: '#pull-to-refresh',
|
||||
onRefresh() {
|
||||
Router.reload();
|
||||
router.reload();
|
||||
},
|
||||
iconArrow: ReactDOMServer.renderToString(
|
||||
<div className="p-2">
|
||||
@@ -28,11 +30,14 @@ const PullToRefresh: React.FC = () => {
|
||||
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>;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import CachedImage from '@app/components/Common/CachedImage';
|
||||
import Tooltip from '@app/components/Common/Tooltip';
|
||||
import RequestModal from '@app/components/RequestModal';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { withProperties } from '@app/utils/typeHelpers';
|
||||
@@ -61,6 +62,13 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
||||
const { hasPermission } = useUser();
|
||||
const intl = useIntl();
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: requestData?.media?.mediaUrl,
|
||||
mediaUrl4k: requestData?.media?.mediaUrl4k,
|
||||
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
|
||||
@@ -138,11 +146,7 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
@@ -217,6 +221,13 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
fallbackData: request,
|
||||
});
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: requestData?.media?.mediaUrl,
|
||||
mediaUrl4k: requestData?.media?.mediaUrl4k,
|
||||
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
const modifyRequest = async (type: 'approve' | 'decline') => {
|
||||
const response = await axios.post(`/api/v1/request/${request.id}/${type}`);
|
||||
|
||||
@@ -396,11 +407,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
is4k={requestData.is4k}
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
|
||||
@@ -4,6 +4,7 @@ import CachedImage from '@app/components/Common/CachedImage';
|
||||
import ConfirmButton from '@app/components/Common/ConfirmButton';
|
||||
import RequestModal from '@app/components/RequestModal';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import {
|
||||
@@ -61,6 +62,13 @@ const RequestItemError = ({
|
||||
revalidateList();
|
||||
};
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: requestData?.media?.mediaUrl,
|
||||
mediaUrl4k: requestData?.media?.mediaUrl4k,
|
||||
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-64 w-full flex-col justify-center rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-red-500 xl:h-28 xl:flex-row">
|
||||
<div className="flex w-full flex-col justify-between overflow-hidden sm:flex-row">
|
||||
@@ -130,11 +138,7 @@ const RequestItemError = ({
|
||||
).length > 0
|
||||
}
|
||||
is4k={requestData.is4k}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
@@ -316,6 +320,13 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: requestData?.media?.mediaUrl,
|
||||
mediaUrl4k: requestData?.media?.mediaUrl4k,
|
||||
iOSPlexUrl: requestData?.media?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
if (!title && !error) {
|
||||
return (
|
||||
<div
|
||||
@@ -462,11 +473,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
is4k={requestData.is4k}
|
||||
tmdbId={requestData.media.tmdbId}
|
||||
mediaType={requestData.type}
|
||||
plexUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.mediaUrl4k
|
||||
: requestData.media.mediaUrl
|
||||
}
|
||||
plexUrl={requestData.is4k ? plexUrl4k : plexUrl}
|
||||
serviceUrl={
|
||||
requestData.is4k
|
||||
? requestData.media.serviceUrl4k
|
||||
|
||||
@@ -13,7 +13,10 @@ import { Transition } from '@headlessui/react';
|
||||
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { PencilIcon } from '@heroicons/react/solid';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces';
|
||||
import type {
|
||||
CacheItem,
|
||||
CacheResponse,
|
||||
} from '@server/interfaces/api/settingsInterfaces';
|
||||
import type { JobId } from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import cronstrue from 'cronstrue/i18n';
|
||||
@@ -58,6 +61,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
'sonarr-scan': 'Sonarr Scan',
|
||||
'download-sync': 'Download Sync',
|
||||
'download-sync-reset': 'Download Sync Reset',
|
||||
'image-cache-cleanup': 'Image Cache Cleanup',
|
||||
editJobSchedule: 'Modify Job',
|
||||
jobScheduleEditSaved: 'Job edited successfully!',
|
||||
jobScheduleEditFailed: 'Something went wrong while saving the job.',
|
||||
@@ -67,6 +71,11 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
|
||||
editJobScheduleSelectorMinutes:
|
||||
'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}',
|
||||
imagecache: 'Image Cache',
|
||||
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>.',
|
||||
imagecachecount: 'Images Cached',
|
||||
imagecachesize: 'Total Cache Size',
|
||||
});
|
||||
|
||||
interface Job {
|
||||
@@ -132,7 +141,8 @@ const SettingsJobs = () => {
|
||||
} = useSWR<Job[]>('/api/v1/settings/jobs', {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { data: cacheData, mutate: cacheRevalidate } = useSWR<CacheItem[]>(
|
||||
const { data: appData } = useSWR('/api/v1/status/appdata');
|
||||
const { data: cacheData, mutate: cacheRevalidate } = useSWR<CacheResponse>(
|
||||
'/api/v1/settings/cache',
|
||||
{
|
||||
refreshInterval: 10000,
|
||||
@@ -435,7 +445,7 @@ const SettingsJobs = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{cacheData
|
||||
{cacheData?.apiCaches
|
||||
?.filter(
|
||||
(cache) =>
|
||||
!(
|
||||
@@ -465,6 +475,41 @@ const SettingsJobs = () => {
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3>
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.imagecacheDescription, {
|
||||
code: (msg: React.ReactNode) => (
|
||||
<code className="bg-opacity-50">{msg}</code>
|
||||
),
|
||||
appDataPath: appData ? appData.appDataPath : '/app/config',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="section">
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<Table.TH>{intl.formatMessage(messages.cachename)}</Table.TH>
|
||||
<Table.TH>
|
||||
{intl.formatMessage(messages.imagecachecount)}
|
||||
</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.imagecachesize)}</Table.TH>
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
<tr>
|
||||
<Table.TD>The Movie Database (tmdb)</Table.TD>
|
||||
<Table.TD>
|
||||
{intl.formatNumber(cacheData?.imageCache.tmdb.imageCount ?? 0)}
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
{formatBytes(cacheData?.imageCache.tmdb.size ?? 0)}
|
||||
</Table.TD>
|
||||
</tr>
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ const messages = defineMessages({
|
||||
'Do NOT enable this setting unless you understand what you are doing!',
|
||||
cacheImages: 'Enable Image Caching',
|
||||
cacheImagesTip:
|
||||
'Cache and serve optimized images (requires a significant amount of disk space)',
|
||||
'Cache externally sourced images (requires a significant amount of disk space)',
|
||||
trustProxy: 'Enable Proxy Support',
|
||||
trustProxyTip:
|
||||
'Allow Overseerr to correctly register client IP addresses behind a proxy',
|
||||
|
||||
@@ -5,12 +5,14 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import getConfig from 'next/config';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
status: '{status}',
|
||||
status4k: '4K {status}',
|
||||
playonplex: 'Play on Plex',
|
||||
playonplex: 'Play on {mediaServerName}',
|
||||
openinarr: 'Open in {arr}',
|
||||
managemedia: 'Manage {mediaType}',
|
||||
});
|
||||
@@ -37,6 +39,7 @@ const StatusBadge = ({
|
||||
const intl = useIntl();
|
||||
const { hasPermission } = useUser();
|
||||
const settings = useSettings();
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
|
||||
let mediaLink: string | undefined;
|
||||
let mediaLinkDescription: string | undefined;
|
||||
@@ -68,7 +71,14 @@ const StatusBadge = ({
|
||||
: settings.currentSettings.series4kEnabled))
|
||||
) {
|
||||
mediaLink = plexUrl;
|
||||
mediaLinkDescription = intl.formatMessage(messages.playonplex);
|
||||
mediaLinkDescription = intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName:
|
||||
publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
|
||||
? 'Emby'
|
||||
: settings.currentSettings.mediaServerType === MediaServerType.PLEX
|
||||
? 'Plex'
|
||||
: 'Jellyfin',
|
||||
});
|
||||
} else if (hasPermission(Permission.MANAGE_REQUESTS)) {
|
||||
if (mediaType && tmdbId) {
|
||||
mediaLink = `/${mediaType}/${tmdbId}?manage=1`;
|
||||
|
||||
@@ -22,6 +22,7 @@ import RequestModal from '@app/components/RequestModal';
|
||||
import Slider from '@app/components/Slider';
|
||||
import StatusBadge from '@app/components/StatusBadge';
|
||||
import Season from '@app/components/TvDetails/Season';
|
||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||
import useLocale from '@app/hooks/useLocale';
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
@@ -125,31 +126,12 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
setShowManager(router.query.manage == '1' ? true : false);
|
||||
}, [router.query.manage]);
|
||||
|
||||
const [plexUrl, setPlexUrl] = useState(data?.mediaInfo?.mediaUrl);
|
||||
const [plexUrl4k, setPlexUrl4k] = useState(data?.mediaInfo?.mediaUrl4k);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
|
||||
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
|
||||
) {
|
||||
setPlexUrl(data.mediaInfo?.iOSPlexUrl);
|
||||
setPlexUrl4k(data.mediaInfo?.iOSPlexUrl4k);
|
||||
} else {
|
||||
setPlexUrl(data.mediaInfo?.mediaUrl);
|
||||
setPlexUrl4k(data.mediaInfo?.mediaUrl4k);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
data?.mediaInfo?.iOSPlexUrl,
|
||||
data?.mediaInfo?.iOSPlexUrl4k,
|
||||
data?.mediaInfo?.mediaUrl,
|
||||
data?.mediaInfo?.mediaUrl4k,
|
||||
settings.currentSettings.mediaServerType,
|
||||
]);
|
||||
const { mediaUrl: plexUrl, mediaUrl4k: plexUrl4k } = useDeepLinks({
|
||||
mediaUrl: data?.mediaInfo?.mediaUrl,
|
||||
mediaUrl4k: data?.mediaInfo?.mediaUrl4k,
|
||||
iOSPlexUrl: data?.mediaInfo?.iOSPlexUrl,
|
||||
iOSPlexUrl4k: data?.mediaInfo?.iOSPlexUrl4k,
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
@@ -984,9 +966,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
tvdbId={data.externalIds.tvdbId}
|
||||
imdbId={data.externalIds.imdbId}
|
||||
rtUrl={ratingData?.url}
|
||||
mediaUrl={
|
||||
data.mediaInfo?.mediaUrl ?? data.mediaInfo?.mediaUrl4k
|
||||
}
|
||||
mediaUrl={plexUrl ?? plexUrl4k}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export type AvailableLocale =
|
||||
| 'el'
|
||||
| 'es'
|
||||
| 'fr'
|
||||
| 'hr'
|
||||
| 'hu'
|
||||
| 'it'
|
||||
| 'ja'
|
||||
@@ -60,6 +61,10 @@ export const availableLanguages: AvailableLanguageObject = {
|
||||
code: 'fr',
|
||||
display: 'Français',
|
||||
},
|
||||
hr: {
|
||||
code: 'hr',
|
||||
display: 'Hrvatski',
|
||||
},
|
||||
it: {
|
||||
code: 'it',
|
||||
display: 'Italiano',
|
||||
|
||||
45
src/hooks/useDeepLinks.ts
Normal file
45
src/hooks/useDeepLinks.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface useDeepLinksProps {
|
||||
mediaUrl?: string;
|
||||
mediaUrl4k?: string;
|
||||
iOSPlexUrl?: string;
|
||||
iOSPlexUrl4k?: string;
|
||||
}
|
||||
|
||||
const useDeepLinks = ({
|
||||
mediaUrl,
|
||||
mediaUrl4k,
|
||||
iOSPlexUrl,
|
||||
iOSPlexUrl4k,
|
||||
}: useDeepLinksProps) => {
|
||||
const [returnedMediaUrl, setReturnedMediaUrl] = useState(mediaUrl);
|
||||
const [returnedMediaUrl4k, setReturnedMediaUrl4k] = useState(mediaUrl4k);
|
||||
const settings = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
settings.currentSettings.mediaServerType === MediaServerType.PLEX &&
|
||||
(/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1))
|
||||
) {
|
||||
setReturnedMediaUrl(iOSPlexUrl);
|
||||
setReturnedMediaUrl4k(iOSPlexUrl4k);
|
||||
} else {
|
||||
setReturnedMediaUrl(mediaUrl);
|
||||
setReturnedMediaUrl4k(mediaUrl4k);
|
||||
}
|
||||
}, [
|
||||
iOSPlexUrl,
|
||||
iOSPlexUrl4k,
|
||||
mediaUrl,
|
||||
mediaUrl4k,
|
||||
settings.currentSettings.mediaServerType,
|
||||
]);
|
||||
|
||||
return { mediaUrl: returnedMediaUrl, mediaUrl4k: returnedMediaUrl4k };
|
||||
};
|
||||
|
||||
export default useDeepLinks;
|
||||
@@ -37,7 +37,7 @@
|
||||
"components.ManageSlideOver.alltime": "جميع الأوقات",
|
||||
"components.ManageSlideOver.downloadstatus": "التنزيلات",
|
||||
"components.ManageSlideOver.manageModalAdvanced": "متقدم",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* سيتم حذف جميع البيانات بشكل نهائي لـ {mediaType},متضمنا جميع الطلبات.إذا كان هذا المحتوى متوفر في مكتبة بليكس، سيتم إعادة تفاصيل المحتوى في عملية الفحص القادمة.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* سيتم حذف جميع البيانات بشكل نهائي لـ {mediaType},متضمنا جميع الطلبات.إذا كان هذا المحتوى متوفر في مكتبة {mediaServerName}، سيتم إعادة تفاصيل المحتوى في عملية الفحص القادمة.",
|
||||
"components.ManageSlideOver.manageModalRequests": "الطلبات",
|
||||
"components.ManageSlideOver.manageModalTitle": "إدارة {mediaType}",
|
||||
"components.ManageSlideOver.manageModalIssues": "المشاكل المفتوحة",
|
||||
|
||||
@@ -887,7 +887,7 @@
|
||||
"components.IssueModal.CreateIssueModal.whatswrong": "Què passa?",
|
||||
"components.IssueModal.issueAudio": "Àudio",
|
||||
"components.IssueModal.issueOther": "Altre",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Això eliminarà de manera irreversible totes les dades de {mediaType}, incloses les sol·licituds. Si aquest element existeix a la vostra biblioteca Plex, la informació dels continguts es recrearà durant la següent exploració.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Això eliminarà de manera irreversible totes les dades de {mediaType}, incloses les sol·licituds. Si aquest element existeix a la vostra biblioteca {mediaServerName}, la informació dels continguts es recrearà durant la següent exploració.",
|
||||
"components.ManageSlideOver.downloadstatus": "Descàrregues",
|
||||
"components.IssueDetails.toasteditdescriptionsuccess": "La descripció de l'incidència s'ha editat correctament!",
|
||||
"components.IssueList.IssueItem.issuetype": "Tipus",
|
||||
@@ -1104,7 +1104,7 @@
|
||||
"components.RequestBlock.delete": "Suprimeix la sol·licitud",
|
||||
"components.RequestBlock.edit": "Edita la sol·licitud",
|
||||
"components.RequestBlock.lastmodifiedby": "Última modificació per",
|
||||
"components.StatusBadge.playonplex": "Reprodueix a Plex",
|
||||
"components.StatusBadge.playonplex": "Reprodueix a {mediaServerName}",
|
||||
"components.RequestCard.declinerequest": "Rebutja la sol·licitud",
|
||||
"components.StatusBadge.openinarr": "Obre a {arr}",
|
||||
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Sincronització de la llista de seguiment de Plex",
|
||||
|
||||
@@ -548,7 +548,7 @@
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Vyčistit data",
|
||||
"components.ManageSlideOver.alltime": "Pořád",
|
||||
"components.ManageSlideOver.manageModalAdvanced": "Pokročilý",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Tímto nenávratně odstraníte všechna data pro tento {mediaType}, včetně všech požadavků. Pokud tato položka existuje ve vaší knihovně Plex, informace o médiích budou znovu vytvořeny během příštího skenování.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Tímto nenávratně odstraníte všechna data pro tento {mediaType}, včetně všech požadavků. Pokud tato položka existuje ve vaší knihovně {mediaServerName}, informace o médiích budou znovu vytvořeny během příštího skenování.",
|
||||
"components.ManageSlideOver.manageModalMedia": "Média",
|
||||
"components.ManageSlideOver.manageModalMedia4k": "4K Média",
|
||||
"components.ManageSlideOver.markallseasonsavailable": "Označte všechny sezóny jako dostupné",
|
||||
@@ -1087,7 +1087,7 @@
|
||||
"components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Watchlist synchronizace",
|
||||
"components.StatusBadge.managemedia": "Spravovat {mediaType}",
|
||||
"components.StatusBadge.openinarr": "Otevřít v {arr}",
|
||||
"components.StatusBadge.playonplex": "Přehrávání cez Plex",
|
||||
"components.StatusBadge.playonplex": "Přehrávání cez {mediaServerName}",
|
||||
"components.TvDetails.manageseries": "Spravovat sérii",
|
||||
"components.RequestBlock.delete": "Smazat požadavek",
|
||||
"components.RequestBlock.edit": "Upravit požadavek",
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
"components.IssueModal.issueVideo": "Video",
|
||||
"components.Layout.Sidebar.issues": "Problemer",
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Ryd Mediedata",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dette vil slette alle data for denne {mediaType} uden mulighed for gendannelse, inklusiv alle forespørgsler. Hvis dette objekt findes i dit Plex bibliotek vil medieinformationen blive genskabt under næste skanning.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dette vil slette alle data for denne {mediaType} uden mulighed for gendannelse, inklusiv alle forespørgsler. Hvis dette objekt findes i dit {mediaServerName} bibliotek vil medieinformationen blive genskabt under næste skanning.",
|
||||
"components.IssueModal.CreateIssueModal.whatswrong": "Hvad er galt?",
|
||||
"components.IssueModal.issueAudio": "Lyd",
|
||||
"components.IssueModal.issueOther": "Andet",
|
||||
|
||||
@@ -931,7 +931,7 @@
|
||||
"components.Layout.Sidebar.issues": "Probleme",
|
||||
"components.ManageSlideOver.downloadstatus": "Downloads",
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Daten löschen",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dadurch werden alle Daten für diesen {mediaType} unwiderruflich entfernt, einschließlich aller Anfragen. Wenn dieses Element in Ihrer Plex-Bibliothek existiert, werden die Medieninformationen beim nächsten Scan neu erstellt.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dadurch werden alle Daten für diesen {mediaType} unwiderruflich entfernt, einschließlich aller Anfragen. Wenn dieses Element in Ihrer {mediaServerName}-Bibliothek existiert, werden die Medieninformationen beim nächsten Scan neu erstellt.",
|
||||
"components.ManageSlideOver.manageModalIssues": "Problem eröffnen",
|
||||
"components.ManageSlideOver.manageModalNoRequests": "Keine Anfragen.",
|
||||
"components.ManageSlideOver.manageModalRequests": "Anfragen",
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
"components.ManageSlideOver.downloadstatus": "Downloads",
|
||||
"components.ManageSlideOver.manageModalAdvanced": "Advanced",
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Clear Data",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your {mediaServerName} library, the media information will be recreated during the next scan.",
|
||||
"components.ManageSlideOver.manageModalIssues": "Open Issues",
|
||||
"components.ManageSlideOver.manageModalMedia": "Media",
|
||||
"components.ManageSlideOver.manageModalMedia4k": "4K Media",
|
||||
@@ -649,6 +649,11 @@
|
||||
"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.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.imagecachecount": "Images Cached",
|
||||
"components.Settings.SettingsJobsCache.imagecachesize": "Total Cache Size",
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Something went wrong while saving the job.",
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Job edited successfully!",
|
||||
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
|
||||
@@ -759,7 +764,7 @@
|
||||
"components.Settings.applicationTitle": "Application Title",
|
||||
"components.Settings.applicationurl": "Application URL",
|
||||
"components.Settings.cacheImages": "Enable Image Caching",
|
||||
"components.Settings.cacheImagesTip": "Cache and serve optimized images (requires a significant amount of disk space)",
|
||||
"components.Settings.cacheImagesTip": "Cache externally sourced images (requires a significant amount of disk space)",
|
||||
"components.Settings.cancelscan": "Cancel Scan",
|
||||
"components.Settings.copied": "Copied API key to clipboard.",
|
||||
"components.Settings.csrfProtection": "Enable CSRF Protection",
|
||||
@@ -874,7 +879,7 @@
|
||||
"components.Setup.welcome": "Welcome to Jellyseerr",
|
||||
"components.StatusBadge.managemedia": "Manage {mediaType}",
|
||||
"components.StatusBadge.openinarr": "Open in {arr}",
|
||||
"components.StatusBadge.playonplex": "Play on Plex",
|
||||
"components.StatusBadge.playonplex": "Play on {mediaServerName}",
|
||||
"components.StatusBadge.status": "{status}",
|
||||
"components.StatusBadge.status4k": "4K {status}",
|
||||
"components.StatusChacker.newversionDescription": "Jellyseerr has been updated! Please click the button below to reload the page.",
|
||||
|
||||
@@ -953,7 +953,7 @@
|
||||
"components.IssueModal.issueAudio": "Audio",
|
||||
"components.IssueModal.issueSubtitles": "Subtítulo",
|
||||
"components.IssueModal.issueVideo": "Vídeo",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Esto eliminará irreversiblemente todos los datos de {mediaType}, incluyendo todas las solicitudes. Si este elemento existe en la biblioteca de Plex, la información de los contenidos se recreará en el siguiente escaneado.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Esto eliminará irreversiblemente todos los datos de {mediaType}, incluyendo todas las solicitudes. Si este elemento existe en la biblioteca de {mediaServerName}, la información de los contenidos se recreará en el siguiente escaneado.",
|
||||
"components.ManageSlideOver.mark4kavailable": "Marcar como Disponible en 4K",
|
||||
"components.ManageSlideOver.openarr4k": "Abrir en 4K {arr}",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Token de Acceso",
|
||||
|
||||
@@ -877,7 +877,7 @@
|
||||
"components.ManageSlideOver.manageModalNoRequests": "Aucune demande.",
|
||||
"components.ManageSlideOver.manageModalRequests": "Demandes",
|
||||
"components.ManageSlideOver.manageModalTitle": "Gérer {mediaType}",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ceci supprimera de manière irréversible toutes les données de ce(tte) {mediaType}, y compris les demandes éventuelles. Si cet élément existe dans votre bibliothèque Plex, les informations sur le média seront recréées lors de la prochaine analyse.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ceci supprimera de manière irréversible toutes les données de ce(tte) {mediaType}, y compris les demandes éventuelles. Si cet élément existe dans votre bibliothèque {mediaServerName}, les informations sur le média seront recréées lors de la prochaine analyse.",
|
||||
"components.ManageSlideOver.tvshow": "série",
|
||||
"components.NotificationTypeSelector.issuecomment": "Commentaires du problème",
|
||||
"components.NotificationTypeSelector.issuecreatedDescription": "Envoyer des notifications lorsqu'un problème est signalé.",
|
||||
@@ -1099,7 +1099,7 @@
|
||||
"components.RequestCard.declinerequest": "Refuser la demande",
|
||||
"components.StatusBadge.managemedia": "Gérer {mediaType}",
|
||||
"components.StatusBadge.openinarr": "Ouvrir dans {arr}",
|
||||
"components.StatusBadge.playonplex": "Lire sur Plex",
|
||||
"components.StatusBadge.playonplex": "Lire sur {mediaServerName}",
|
||||
"components.TvDetails.Season.somethingwentwrong": "Une erreur s'est produite lors de la récupération des données de la saison.",
|
||||
"components.TvDetails.rtaudiencescore": "Note d'audience de Rotten Tomatoes",
|
||||
"components.TvDetails.rtcriticsscore": "Rotten Tomatoes Tomatomètre",
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"components.ManageSlideOver.movie": "film",
|
||||
"components.Login.validationemailrequired": "Morate unijeti valjanu adresu e-pošte",
|
||||
"components.ManageSlideOver.manageModalRequests": "Zahtjevi",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ovo će nepovratno ukloniti sve podatke za ovaj {mediaType}, uključujući sve zahtjeve. Ako ova stavka postoji u vašoj Plex biblioteci, informacije o medijima ponovno će se stvoriti tijekom sljedećeg skeniranja.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ovo će nepovratno ukloniti sve podatke za ovaj {mediaType}, uključujući sve zahtjeve. Ako ova stavka postoji u vašoj {mediaServerName} biblioteci, informacije o medijima ponovno će se stvoriti tijekom sljedećeg skeniranja.",
|
||||
"components.ManageSlideOver.manageModalMedia4k": "4K Mediji",
|
||||
"components.ManageSlideOver.manageModalNoRequests": "Nema zahtjeva.",
|
||||
"components.ManageSlideOver.manageModalMedia": "Mediji",
|
||||
|
||||
@@ -851,7 +851,7 @@
|
||||
"components.IssueModal.CreateIssueModal.toastFailedCreate": "Valami hiba történt a probléma elküldése során.",
|
||||
"components.IssueDetails.play4konplex": "Lejátszás Plexen 4K-ban",
|
||||
"components.IssueModal.CreateIssueModal.toastviewissue": "Probléma Megtekintése",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ez visszafordíthatatlanul eltávolítja az összes adatot ehhez a {mediaType}-hez, beleértve a kéréseket is. Ha ez az elem létezik a Plex könyvtárában, a médiainformáció a következő beolvasás során újra létrejön.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Ez visszafordíthatatlanul eltávolítja az összes adatot ehhez a {mediaType}-hez, beleértve a kéréseket is. Ha ez az elem létezik a {mediaServerName} könyvtárában, a médiainformáció a következő beolvasás során újra létrejön.",
|
||||
"components.IssueDetails.commentplaceholder": "Hozzászólás írása…",
|
||||
"components.IssueDetails.comments": "Hozzászólások",
|
||||
"components.IssueDetails.deleteissue": "Probléma Törlése",
|
||||
|
||||
@@ -950,7 +950,7 @@
|
||||
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Job modificato correttamente!",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingsfailed": "Impossibile salvare le impostazioni Pushover.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushoversettingssaved": "Impostazioni Pushover salvate con successo!",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Questo rimuoverà irreversibilmente tutti i dati per questo {mediaType}, incluse eventuali richieste. Se questo elemento esiste nella tua libreria Plex, le informazioni multimediali verranno ricreate durante la scansione successiva.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Questo rimuoverà irreversibilmente tutti i dati per questo {mediaType}, incluse eventuali richieste. Se questo elemento esiste nella tua libreria {mediaServerName}, le informazioni multimediali verranno ricreate durante la scansione successiva.",
|
||||
"components.NotificationTypeSelector.issuecreated": "Problema Segnalato",
|
||||
"components.NotificationTypeSelector.issuecreatedDescription": "Invia una notifica quando un problema viene segnalato.",
|
||||
"components.NotificationTypeSelector.issueresolved": "Problema risolto",
|
||||
|
||||
@@ -503,7 +503,7 @@
|
||||
"components.ManageSlideOver.manageModalClearMedia": "データを消去",
|
||||
"components.ManageSlideOver.manageModalRequests": "リクエスト",
|
||||
"components.ManageSlideOver.openarr": "{arr} を開く",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "※リクエストを含め、すべての詳細情報が消去されます。この操作は元に戻すことができません。この作品が Plex ライブラリに存在する場合、詳細情報は次のスキャンで再作成されます。",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "※リクエストを含め、すべての詳細情報が消去されます。この操作は元に戻すことができません。この作品が {mediaServerName} ライブラリに存在する場合、詳細情報は次のスキャンで再作成されます。",
|
||||
"components.ManageSlideOver.openarr4k": "4K {arr} を開く",
|
||||
"components.ManageSlideOver.manageModalNoRequests": "リクエストが有りません。",
|
||||
"components.ManageSlideOver.manageModalTitle": "{mediaType}を管理",
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
"components.IssueModal.CreateIssueModal.problemseason": "Paveikti sezonai",
|
||||
"components.IssueDetails.openedby": "#{issueId} problema atverta {relativeTime}, {username}",
|
||||
"components.Layout.VersionStatus.commitsbehind": "{commitsBehind} {commitsBehind, plural, one {komitas} other {komitai}} behind",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Tai negyžtamai pašalins {mediaType} tipo duomenis, įskaitant rezervacijas. Plex bibliotekoje esančios medijos informacija bus atkurta kito skanavimo metu.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Tai negyžtamai pašalins {mediaType} tipo duomenis, įskaitant rezervacijas. {mediaServerName} bibliotekoje esančios medijos informacija bus atkurta kito skanavimo metu.",
|
||||
"components.NotificationTypeSelector.adminissuecommentDescription": "Gauti pranešimus kai kiti vartotojai komentuoja problemą.",
|
||||
"components.NotificationTypeSelector.adminissueresolvedDescription": "Gauti pranešimus kai kiti vartotojai uždaro problemą.",
|
||||
"components.NotificationTypeSelector.issuecomment": "Problemos komentaras",
|
||||
|
||||
@@ -986,7 +986,7 @@
|
||||
"components.Settings.SettingsJobsCache.cachevsize": "Verdistørrelse",
|
||||
"components.Settings.trustProxyTip": "Tillatt Jellyseerr å registrere klienters IP addresser korrekt bak en proxy",
|
||||
"components.Settings.serviceSettingsDescription": "Konfigurer dine {serverType}tjener(e) nedenfor. Du kan koble til flere forskellige {serverType}tjenere men kun to av dem kan markeres som standard (en som ikke er 4K og en 4K). Administratorer kan endre hvilken tjener som brukes før godkjennelse av nye forespørsler.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dette vil slette all data for denne tittelen uten mulighet for å bli gjennopprettet, det inkluderer alle forespørsler, avvik osv. Hvis denne tittelen finnes i ditt Plex bibliotek vil medieinformasjon bli opprettet på nytt under neste skanning.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Dette vil slette all data for denne tittelen uten mulighet for å bli gjennopprettet, det inkluderer alle forespørsler, avvik osv. Hvis denne tittelen finnes i ditt {mediaServerName} bibliotek vil medieinformasjon bli opprettet på nytt under neste skanning.",
|
||||
"components.Settings.Notifications.NotificationsWebhook.authheader": "Autorisasjonshode",
|
||||
"components.Settings.SettingsJobsCache.cacheksize": "Nøkkelstørrelse",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload",
|
||||
@@ -1097,7 +1097,7 @@
|
||||
"components.Settings.advancedTooltip": "Feil konfigurering av denne innstillingen kan føre til defekt funksjonalitet",
|
||||
"components.TvDetails.Season.somethingwentwrong": "Noe gikk galt under henting av data for denne sesongen.",
|
||||
"components.StatusChecker.reloadApp": "Last inn {applicationTitle} på nytt",
|
||||
"components.StatusBadge.playonplex": "Spill av med Plex",
|
||||
"components.StatusBadge.playonplex": "Spill av med {mediaServerName}",
|
||||
"components.StatusBadge.openinarr": "Vis i {arr}",
|
||||
"components.StatusBadge.managemedia": "Administrer {mediaType}",
|
||||
"components.TvDetails.episodeCount": "{episodeCount, plural, one {# Episode} other {# Episoder}}",
|
||||
|
||||
@@ -939,7 +939,7 @@
|
||||
"components.IssueModal.issueOther": "Andere",
|
||||
"components.Layout.Sidebar.issues": "Problemen",
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Gegevens wissen",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Hiermee worden alle gegevens voor deze {mediaType} onomkeerbaar verwijderd, inclusief eventuele verzoeken. Als dit item in je Plex-bibliotheek staat, worden de mediagegevens opnieuw aangemaakt tijdens de volgende scan.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Hiermee worden alle gegevens voor deze {mediaType} onomkeerbaar verwijderd, inclusief eventuele verzoeken. Als dit item in je {mediaServerName}-bibliotheek staat, worden de mediagegevens opnieuw aangemaakt tijdens de volgende scan.",
|
||||
"components.ManageSlideOver.manageModalRequests": "Verzoeken",
|
||||
"components.ManageSlideOver.manageModalTitle": "{mediaType} beheren",
|
||||
"components.ManageSlideOver.tvshow": "serie",
|
||||
@@ -1042,7 +1042,7 @@
|
||||
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Huidige frequentie",
|
||||
"components.StatusBadge.managemedia": "{mediaType} beheren",
|
||||
"components.StatusBadge.openinarr": "Openen in {arr}",
|
||||
"components.StatusBadge.playonplex": "Afspelen op Plex",
|
||||
"components.StatusBadge.playonplex": "Afspelen op {mediaServerName}",
|
||||
"components.UserProfile.emptywatchlist": "Media die zijn toegevoegd aan je <PlexWatchlistSupportLink>Plex Kijklijst</PlexWatchlistSupportLink> verschijnen hier.",
|
||||
"components.MovieDetails.digitalrelease": "Digitale release",
|
||||
"i18n.restartRequired": "Opnieuw opstarten vereist",
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
"components.PermissionEdit.createissues": "Zgłoś problemy",
|
||||
"components.PermissionEdit.manageissues": "Zarządzaj problemami",
|
||||
"components.PermissionEdit.manageissuesDescription": "Udziel uprawnień do zarządzania problemami z multimediami.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Spowoduje to nieodwracalne usunięcie wszystkich danych dla {mediaType}, w tym wszelkie prośby. Jeśli ten element istnieje w Twojej bibliotece Plex, informacje o multimediach zostaną odtworzone podczas następnego skanowania.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Spowoduje to nieodwracalne usunięcie wszystkich danych dla {mediaType}, w tym wszelkie prośby. Jeśli ten element istnieje w Twojej bibliotece {mediaServerName}, informacje o multimediach zostaną odtworzone podczas następnego skanowania.",
|
||||
"components.IssueModal.CreateIssueModal.providedetail": "Podaj szczegółowe wyjaśnienie napotkanego problemu.",
|
||||
"components.IssueModal.CreateIssueModal.whatswrong": "Co jest nie tak?",
|
||||
"components.Discover.MovieGenreList.moviegenres": "Gatunki filmowe",
|
||||
|
||||
@@ -924,7 +924,7 @@
|
||||
"components.IssueModal.issueOther": "Outros",
|
||||
"components.IssueModal.issueSubtitles": "Legenda",
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Limpar Dados",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Isso irá remover em definitivo todos dados desse(a) {mediaType}, incluindo quaisquer solicitações para esse item. Se este item existir in sua biblioteca do Plex, os dados de mídia serão recriados na próxima sincronia.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Isso irá remover em definitivo todos dados desse(a) {mediaType}, incluindo quaisquer solicitações para esse item. Se este item existir in sua biblioteca do {mediaServerName}, os dados de mídia serão recriados na próxima sincronia.",
|
||||
"components.ManageSlideOver.manageModalIssues": "Problemas Abertos",
|
||||
"components.ManageSlideOver.manageModalNoRequests": "Nenhuma solicitação.",
|
||||
"components.ManageSlideOver.manageModalRequests": "Solicitações",
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"components.RequestBlock.requestdate": "Data do pedido",
|
||||
"components.RequestCard.declinerequest": "Rejeitar Pedido",
|
||||
"components.RequestCard.editrequest": "Editar Pedido",
|
||||
"components.StatusBadge.playonplex": "Reproduzir no Plex",
|
||||
"components.StatusBadge.playonplex": "Reproduzir no {mediaServerName}",
|
||||
"components.RequestBlock.decline": "Rejeitar pedido",
|
||||
"components.RequestBlock.lastmodifiedby": "Última modificação por",
|
||||
"components.RequestBlock.delete": "Deletar pedido",
|
||||
|
||||
@@ -871,7 +871,7 @@
|
||||
"components.IssueDetails.allseasons": "Все сезоны",
|
||||
"components.IssueDetails.allepisodes": "Все эпизоды",
|
||||
"components.ManageSlideOver.manageModalClearMedia": "Очистить данные",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Это приведёт к необратимому удалению всех данных для этого {mediaType}а, включая любые запросы. Если этот элемент существует в вашей библиотеке Plex, мультимедийная информация о нём будет воссоздана во время следующего сканирования.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Это приведёт к необратимому удалению всех данных для этого {mediaType}а, включая любые запросы. Если этот элемент существует в вашей библиотеке {mediaServerName}, мультимедийная информация о нём будет воссоздана во время следующего сканирования.",
|
||||
"components.IssueDetails.problemepisode": "Затронутый эпизод",
|
||||
"components.ManageSlideOver.manageModalRequests": "Запросы",
|
||||
"components.IssueDetails.closeissue": "Закрыть проблему",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"components.IssueModal.CreateIssueModal.submitissue": "Paraqit Problemin",
|
||||
"components.IssueModal.CreateIssueModal.toastSuccessCreate": "Raporti i problemit për <strong>{title}</strong> u paraqit me sukses!",
|
||||
"components.IssueModal.CreateIssueModal.toastviewissue": "Shiko Problemin",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Kjo do të heqë në mënyrë të pakthyeshme të gjitha të dhënat për këtë {mediaType}, duke përfshirë çdo kërkesë. Nëse ky artikull ekziston në bibliotekën tuaj Plex, informacioni i medias do të rikrijohet gjatë skanimit të ardhshëm.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Kjo do të heqë në mënyrë të pakthyeshme të gjitha të dhënat për këtë {mediaType}, duke përfshirë çdo kërkesë. Nëse ky artikull ekziston në bibliotekën tuaj {mediaServerName}, informacioni i medias do të rikrijohet gjatë skanimit të ardhshëm.",
|
||||
"components.AppDataWarning.dockerVolumeMissingDescription": "Monitimi i volumit <code>{appDataPath}</code> nuk u konfigurua siç duhet. Gjithë informacioni do të fshihet kur kontenieri do të mbyllet ose të ristartohet.",
|
||||
"components.Discover.StudioSlider.studios": "Studiot",
|
||||
"components.Layout.UserDropdown.settings": "Cilësimet",
|
||||
|
||||
@@ -609,7 +609,7 @@
|
||||
"components.Settings.SettingsAbout.uptodate": "Najsvežiji",
|
||||
"components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "Morate da navedete važeći JSON korisni teret",
|
||||
"components.Settings.Notifications.validationChatIdRequired": "Morate da navedete važeći ID za ćaskanje",
|
||||
"components.StatusBadge.playonplex": "Igrajte na Plex-u",
|
||||
"components.StatusBadge.playonplex": "Igrajte na {mediaServerName}-u",
|
||||
"components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "Morate da obezbedite pristupni token",
|
||||
"components.UserList.userssaved": "Korisničke dozvole su uspešno sačuvane!"
|
||||
}
|
||||
|
||||
@@ -951,7 +951,7 @@
|
||||
"components.NotificationTypeSelector.issuecreated": "Problem rappoterat",
|
||||
"components.PermissionEdit.createissues": "Rapportera problem",
|
||||
"components.PermissionEdit.viewissues": "Visa problem",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Detta tar bort all data för denna {mediaType}, inklusive eventuella begäranden, på ett oåterkalleligt sätt. Om det här objektet finns i ditt Plex-bibliotek kommer medieinformationen att återskapas vid nästa genomsökning.",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* Detta tar bort all data för denna {mediaType}, inklusive eventuella begäranden, på ett oåterkalleligt sätt. Om det här objektet finns i ditt {mediaServerName}-bibliotek kommer medieinformationen att återskapas vid nästa genomsökning.",
|
||||
"components.ManageSlideOver.manageModalNoRequests": "Inga förfrågningar.",
|
||||
"components.NotificationTypeSelector.userissueresolvedDescription": "Få meddelande när dina rapporterade problem har blivit lösta.",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.pushbulletAccessToken": "Åtkomsttoken",
|
||||
|
||||
@@ -944,7 +944,7 @@
|
||||
"components.NotificationTypeSelector.userissueresolvedDescription": "当您报告的问题解决时获取通知。",
|
||||
"components.ManageSlideOver.alltime": "历史",
|
||||
"components.ManageSlideOver.manageModalAdvanced": "高级",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* 这将会删除所有和{mediaType}相关的数据和所有请求。如果{mediaType}在您的Plex服务器存在,数据将会在媒体库扫描时重新建立。",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "* 这将会删除所有和{mediaType}相关的数据和所有请求。如果{mediaType}在您的{mediaServerName}服务器存在,数据将会在媒体库扫描时重新建立。",
|
||||
"components.ManageSlideOver.manageModalIssues": "未解决问题",
|
||||
"components.ManageSlideOver.manageModalMedia": "媒体",
|
||||
"components.ManageSlideOver.manageModalMedia4k": "4K 媒体",
|
||||
|
||||
@@ -884,7 +884,7 @@
|
||||
"components.IssueModal.issueAudio": "音訊",
|
||||
"components.ManageSlideOver.downloadstatus": "下載狀態",
|
||||
"components.IssueModal.CreateIssueModal.allepisodes": "所有集數",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "※這將會刪除包括使用者請求在內所有有關此{mediaType}的資料。如果這{mediaType}存在於您的 Plex 伺服器,資料將會在媒體庫掃描時重新建立。",
|
||||
"components.ManageSlideOver.manageModalClearMediaWarning": "※這將會刪除包括使用者請求在內所有有關此{mediaType}的資料。如果這{mediaType}存在於您的 {mediaServerName} 伺服器,資料將會在媒體庫掃描時重新建立。",
|
||||
"components.ManageSlideOver.mark4kavailable": "標記 4K 版為可觀看",
|
||||
"components.IssueModal.issueSubtitles": "字幕",
|
||||
"components.IssueModal.issueOther": "其他",
|
||||
@@ -1092,7 +1092,7 @@
|
||||
"components.RequestBlock.delete": "刪除請求",
|
||||
"components.RequestCard.editrequest": "編輯請求",
|
||||
"components.RequestBlock.requestedby": "請求者",
|
||||
"components.StatusBadge.playonplex": "在 Plex 上觀看",
|
||||
"components.StatusBadge.playonplex": "在 {mediaServerName} 上觀看",
|
||||
"components.StatusBadge.managemedia": "管理{mediaType}",
|
||||
"components.StatusBadge.openinarr": "開啟 {arr} 伺服器",
|
||||
"components.TvDetails.status4k": "4K 版{status}",
|
||||
|
||||
@@ -43,6 +43,8 @@ const loadLocaleData = (locale: AvailableLocale): Promise<any> => {
|
||||
return import('../i18n/locale/es.json');
|
||||
case 'fr':
|
||||
return import('../i18n/locale/fr.json');
|
||||
case 'hr':
|
||||
return import('../i18n/locale/hr.json');
|
||||
case 'hu':
|
||||
return import('../i18n/locale/hu.json');
|
||||
case 'it':
|
||||
|
||||
Reference in New Issue
Block a user