diff --git a/.all-contributorsrc b/.all-contributorsrc index 3cf5e765..faa3f753 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -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": "\"All-orange.svg\"/>", @@ -745,5 +763,6 @@ "projectOwner": "sct", "repoType": "github", "repoHost": "https://github.com", - "skipCi": false + "skipCi": false, + "commitConvention": "angular" } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8890dcae..50096840 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/snap.yaml b/.github/workflows/snap.yaml index bf00e04d..63f6555c 100644 --- a/.github/workflows/snap.yaml +++ b/.github/workflows/snap.yaml @@ -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 diff --git a/.gitignore b/.gitignore index 70a5d6f2..9a8925ab 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ tsconfig.tsbuildinfo # Webstorm .idea + +# Config Cache Directory +config/cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 466c9994..d7e9d1cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,95 +1,4 @@ -## [1.2.1](https://github.com/fallenbagel/jellyseerr/compare/v1.2.0...v1.2.1) (2022-10-18) - - -### 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)) - - -### 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)) - -# [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 diff --git a/README.md b/README.md index 614dcadb..5f0d9da7 100644 --- a/README.md +++ b/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) diff --git a/cypress.config.ts b/cypress.config.ts index 07b0c8b1..457aa326 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'cypress'; export default defineConfig({ - projectId: 'onnqy3', + projectId: 'xkm1b4', e2e: { baseUrl: 'http://localhost:5055', experimentalSessionAndOrigin: true, diff --git a/docs/extending-overseerr/fail2ban.md b/docs/extending-overseerr/fail2ban.md index 1cf9131f..4f2b1c59 100644 --- a/docs/extending-overseerr/fail2ban.md +++ b/docs/extending-overseerr/fail2ban.md @@ -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":"" ``` -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. diff --git a/docs/using-overseerr/settings/README.md b/docs/using-overseerr/settings/README.md index 82043073..477129fc 100644 --- a/docs/using-overseerr/settings/README.md +++ b/docs/using-overseerr/settings/README.md @@ -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. diff --git a/overseerr-api.yml b/overseerr-api.yml index 33052ad4..128729f0 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -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 diff --git a/package.json b/package.json index b51f7c98..77e02ad1 100644 --- a/package.json +++ b/package.json @@ -225,7 +225,7 @@ { "path": "semantic-release-docker-buildx", "buildArgs": { - "COMMIT_TAG": "$GITHUB_SHA" + "COMMIT_TAG": "$GIT_SHA" }, "imageNames": [ "fallenbagel/jellyseerr" diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 79b0778a..b126b55f 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -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 { try { const contents = await this.axios.get( - `/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( diff --git a/server/entity/Media.ts b/server/entity/Media.ts index cf6f5a28..12228200 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -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) { diff --git a/server/entity/User.ts b/server/entity/User.ts index b5f78110..8780e2d8 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -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 }) diff --git a/server/index.ts b/server/index.ts index 615e789b..b6eb13f5 100644 --- a/server/index.ts +++ b/server/index.ts @@ -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( ( diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index bafd15b1..32ed6a54 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -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; diff --git a/server/job/jellyfinsync/index.ts b/server/job/jellyfinsync/index.ts index 85c8dcc5..9863f065 100644 --- a/server/job/jellyfinsync/index.ts +++ b/server/job/jellyfinsync/index.ts @@ -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; } }); }); diff --git a/server/job/schedule.ts b/server/job/schedule.ts index 356c475e..6d960073 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -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' }); }; diff --git a/server/lib/imageproxy.ts b/server/lib/imageproxy.ts new file mode 100644 index 00000000..34a097d5 --- /dev/null +++ b/server/lib/imageproxy.ts @@ -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 { + 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 { + 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 { + 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 { + 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; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 29e2fcf1..930ca280 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -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) { diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 0280a428..1dabcdf3 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -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); diff --git a/server/routes/imageproxy.ts b/server/routes/imageproxy.ts new file mode 100644 index 00000000..6cf104f5 --- /dev/null +++ b/server/routes/imageproxy.ts @@ -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; diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index dc9dbcb4..005076e1 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -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 }>( diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 16d8a50b..486ebc36 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -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(); diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3b693643..61db367a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -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) diff --git a/src/components/Common/CachedImage/index.tsx b/src/components/Common/CachedImage/index.tsx index b1695937..6dfb8ee7 100644 --- a/src/components/Common/CachedImage/index.tsx +++ b/src/components/Common/CachedImage/index.tsx @@ -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 ; + 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 ; }; export default CachedImage; diff --git a/src/components/CompanyCard/index.tsx b/src/components/CompanyCard/index.tsx index 762d1a08..13b92a33 100644 --- a/src/components/CompanyCard/index.tsx +++ b/src/components/CompanyCard/index.tsx @@ -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} > - {name} +
+ +
{ : 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 && (
diff --git a/src/components/MediaSlider/ShowMoreCard/index.tsx b/src/components/MediaSlider/ShowMoreCard/index.tsx index 99900ac9..2d3cee23 100644 --- a/src/components/MediaSlider/ShowMoreCard/index.tsx +++ b/src/components/MediaSlider/ShowMoreCard/index.tsx @@ -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 ( +
+ +
+ ); + } + return ( { 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 ; @@ -378,7 +360,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { } tmdbId={data.mediaInfo?.tmdbId} mediaType="movie" - plexUrl={plexUrl} + plexUrl={plexUrl4k} serviceUrl={data.mediaInfo?.serviceUrl4k} /> )} diff --git a/src/components/PullToRefresh/index.tsx b/src/components/PullToRefresh/index.tsx index ce92ea60..dd782dbe 100644 --- a/src/components/PullToRefresh/index.tsx +++ b/src/components/PullToRefresh/index.tsx @@ -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(
@@ -28,11 +30,14 @@ const PullToRefresh: React.FC = () => { instructionsReleaseToRefresh: ReactDOMServer.renderToString(
), instructionsRefreshing: ReactDOMServer.renderToString(
), distReload: 60, + distIgnore: 15, + shouldPullToRefresh: () => + !window.scrollY && document.body.style.overflow !== 'hidden', }); return () => { PR.destroyAll(); }; - }, []); + }, [router]); return
; }; diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 9ccbcde0..27bb3383 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -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 diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 6c232dc8..e5a00de7 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -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 (
@@ -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 (
{ 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 diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 7317c8e8..f3402e2e 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -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 {appDataPath}/cache/images.', + imagecachecount: 'Images Cached', + imagecachesize: 'Total Cache Size', }); interface Job { @@ -132,7 +141,8 @@ const SettingsJobs = () => { } = useSWR('/api/v1/settings/jobs', { refreshInterval: 5000, }); - const { data: cacheData, mutate: cacheRevalidate } = useSWR( + const { data: appData } = useSWR('/api/v1/status/appdata'); + const { data: cacheData, mutate: cacheRevalidate } = useSWR( '/api/v1/settings/cache', { refreshInterval: 10000, @@ -435,7 +445,7 @@ const SettingsJobs = () => { - {cacheData + {cacheData?.apiCaches ?.filter( (cache) => !( @@ -465,6 +475,41 @@ const SettingsJobs = () => {
+
+

{intl.formatMessage(messages.imagecache)}

+

+ {intl.formatMessage(messages.imagecacheDescription, { + code: (msg: React.ReactNode) => ( + {msg} + ), + appDataPath: appData ? appData.appDataPath : '/app/config', + })} +

+
+
+ + + + {intl.formatMessage(messages.cachename)} + + {intl.formatMessage(messages.imagecachecount)} + + {intl.formatMessage(messages.imagecachesize)} + + + + + The Movie Database (tmdb) + + {intl.formatNumber(cacheData?.imageCache.tmdb.imageCount ?? 0)} + + + {formatBytes(cacheData?.imageCache.tmdb.size ?? 0)} + + + +
+
); }; diff --git a/src/components/Settings/SettingsMain.tsx b/src/components/Settings/SettingsMain.tsx index ef0810f5..7d4e188e 100644 --- a/src/components/Settings/SettingsMain.tsx +++ b/src/components/Settings/SettingsMain.tsx @@ -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', diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx index 22fa2bbe..cd5e0ad1 100644 --- a/src/components/StatusBadge/index.tsx +++ b/src/components/StatusBadge/index.tsx @@ -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`; diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 7c38a423..1792027e 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -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 ; @@ -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} />
diff --git a/src/context/LanguageContext.tsx b/src/context/LanguageContext.tsx index 0cf4d7d7..115f4f4b 100644 --- a/src/context/LanguageContext.tsx +++ b/src/context/LanguageContext.tsx @@ -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', diff --git a/src/hooks/useDeepLinks.ts b/src/hooks/useDeepLinks.ts new file mode 100644 index 00000000..98308659 --- /dev/null +++ b/src/hooks/useDeepLinks.ts @@ -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; diff --git a/src/i18n/locale/ar.json b/src/i18n/locale/ar.json index bba3d171..95b6e328 100644 --- a/src/i18n/locale/ar.json +++ b/src/i18n/locale/ar.json @@ -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": "المشاكل المفتوحة", diff --git a/src/i18n/locale/ca.json b/src/i18n/locale/ca.json index a0eaeb14..9733c4b6 100644 --- a/src/i18n/locale/ca.json +++ b/src/i18n/locale/ca.json @@ -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", diff --git a/src/i18n/locale/cs.json b/src/i18n/locale/cs.json index 817e12ed..dedf8b15 100644 --- a/src/i18n/locale/cs.json +++ b/src/i18n/locale/cs.json @@ -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", diff --git a/src/i18n/locale/da.json b/src/i18n/locale/da.json index cd6da0d2..15da8118 100644 --- a/src/i18n/locale/da.json +++ b/src/i18n/locale/da.json @@ -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", diff --git a/src/i18n/locale/de.json b/src/i18n/locale/de.json index 61153d82..86c3f86c 100644 --- a/src/i18n/locale/de.json +++ b/src/i18n/locale/de.json @@ -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", diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index b5acab96..27f21134 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -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 {appDataPath}/cache/images.", + "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.", diff --git a/src/i18n/locale/es.json b/src/i18n/locale/es.json index e66603c6..6798681e 100644 --- a/src/i18n/locale/es.json +++ b/src/i18n/locale/es.json @@ -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", diff --git a/src/i18n/locale/fr.json b/src/i18n/locale/fr.json index 5a2545fa..2acc4107 100644 --- a/src/i18n/locale/fr.json +++ b/src/i18n/locale/fr.json @@ -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", diff --git a/src/i18n/locale/hr.json b/src/i18n/locale/hr.json index 710ba1fb..ae6e1fd1 100644 --- a/src/i18n/locale/hr.json +++ b/src/i18n/locale/hr.json @@ -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", diff --git a/src/i18n/locale/hu.json b/src/i18n/locale/hu.json index 90ba3a93..46bbb76d 100644 --- a/src/i18n/locale/hu.json +++ b/src/i18n/locale/hu.json @@ -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", diff --git a/src/i18n/locale/it.json b/src/i18n/locale/it.json index b87455ca..446c24fe 100644 --- a/src/i18n/locale/it.json +++ b/src/i18n/locale/it.json @@ -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", diff --git a/src/i18n/locale/ja.json b/src/i18n/locale/ja.json index fd0ecea6..acdff9ad 100644 --- a/src/i18n/locale/ja.json +++ b/src/i18n/locale/ja.json @@ -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}を管理", diff --git a/src/i18n/locale/lt.json b/src/i18n/locale/lt.json index e7667f3c..73b5189c 100644 --- a/src/i18n/locale/lt.json +++ b/src/i18n/locale/lt.json @@ -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", diff --git a/src/i18n/locale/nb_NO.json b/src/i18n/locale/nb_NO.json index fc0cd2c1..b90ddc89 100644 --- a/src/i18n/locale/nb_NO.json +++ b/src/i18n/locale/nb_NO.json @@ -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}}", diff --git a/src/i18n/locale/nl.json b/src/i18n/locale/nl.json index 264755c1..1a06183f 100644 --- a/src/i18n/locale/nl.json +++ b/src/i18n/locale/nl.json @@ -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 Plex Kijklijst verschijnen hier.", "components.MovieDetails.digitalrelease": "Digitale release", "i18n.restartRequired": "Opnieuw opstarten vereist", diff --git a/src/i18n/locale/pl.json b/src/i18n/locale/pl.json index 57bc19e8..aecc2a52 100644 --- a/src/i18n/locale/pl.json +++ b/src/i18n/locale/pl.json @@ -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", diff --git a/src/i18n/locale/pt_BR.json b/src/i18n/locale/pt_BR.json index 57d71696..7f657efe 100644 --- a/src/i18n/locale/pt_BR.json +++ b/src/i18n/locale/pt_BR.json @@ -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", diff --git a/src/i18n/locale/ru.json b/src/i18n/locale/ru.json index a4e5dc9e..2c822999 100644 --- a/src/i18n/locale/ru.json +++ b/src/i18n/locale/ru.json @@ -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": "Закрыть проблему", diff --git a/src/i18n/locale/sq.json b/src/i18n/locale/sq.json index 01ad8a02..3573bf6c 100644 --- a/src/i18n/locale/sq.json +++ b/src/i18n/locale/sq.json @@ -5,7 +5,7 @@ "components.IssueModal.CreateIssueModal.submitissue": "Paraqit Problemin", "components.IssueModal.CreateIssueModal.toastSuccessCreate": "Raporti i problemit për {title} 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 {appDataPath} 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", diff --git a/src/i18n/locale/sr.json b/src/i18n/locale/sr.json index bfc44009..bf1364c5 100644 --- a/src/i18n/locale/sr.json +++ b/src/i18n/locale/sr.json @@ -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!" } diff --git a/src/i18n/locale/sv.json b/src/i18n/locale/sv.json index baadcbc0..715c4992 100644 --- a/src/i18n/locale/sv.json +++ b/src/i18n/locale/sv.json @@ -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", diff --git a/src/i18n/locale/zh_Hans.json b/src/i18n/locale/zh_Hans.json index 54cba85f..59febf2e 100644 --- a/src/i18n/locale/zh_Hans.json +++ b/src/i18n/locale/zh_Hans.json @@ -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 媒体", diff --git a/src/i18n/locale/zh_Hant.json b/src/i18n/locale/zh_Hant.json index e9b3b482..4aff692d 100644 --- a/src/i18n/locale/zh_Hant.json +++ b/src/i18n/locale/zh_Hant.json @@ -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}", diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d546f0a8..4738b8cf 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -43,6 +43,8 @@ const loadLocaleData = (locale: AvailableLocale): Promise => { 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':