From ef5e954db14c9a45b705139672e784010a39cdec Mon Sep 17 00:00:00 2001 From: Gauthier Date: Wed, 20 Nov 2024 12:33:16 +0100 Subject: [PATCH] chore: merge upstream (#1112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(pushover): attach image to pushover notification payload (#3701) * fix: api language query parameter (#3720) * docs: add j0srisk as a contributor for code (#3745) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(tooltip): add tooltip to display exact time on date hover (#3773) Co-authored-by: Loetwiek * docs: add Loetwiek as a contributor for code (#3776) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix(ui): ensure title fits into the `view collection` box (#3696) * fix(docs): correct openapi docs minor issues (#3648) * docs: add Fuochi as a contributor for doc (#3826) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat: translations update from Hosted Weblate (#3597) * feat(lang): translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Portuguese (Brazil)) Currently translated at 99.8% (1232 of 1234 strings) Co-authored-by: Cleiton Carvalho Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (German) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (German) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1236 of 1236 strings) feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Anders Ecklon Co-authored-by: Hosted Weblate Co-authored-by: Kenneth Hansen Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/da/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Greek) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Greek) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: BeardedWatermelon Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/el/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 99.5% (1234 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Romanian) Currently translated at 37.1% (461 of 1240 strings) feat(lang): translated using Weblate (Romanian) Currently translated at 37.0% (459 of 1240 strings) feat(lang): translated using Weblate (Romanian) Currently translated at 34.8% (432 of 1240 strings) Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ro/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Bulgarian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 57.4% (712 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 13.2% (164 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 4.8% (60 of 1240 strings) feat(lang): added translation using Weblate (Bulgarian) Co-authored-by: Hosted Weblate Co-authored-by: sct Co-authored-by: Димитър Мазнеков (Topper) Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/bg/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 97.9% (1215 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 82.0% (1017 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 72.9% (905 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 72.9% (905 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 71.3% (885 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 64.9% (805 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 64.4% (799 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 63.8% (792 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 63.7% (791 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 57.5% (714 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 49.9% (619 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 35.9% (446 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 35.9% (446 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 32.1% (399 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 24.6% (306 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 18.9% (235 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 17.5% (217 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 17.3% (215 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 8.0% (100 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 3.3% (41 of 1240 strings) feat(lang): added translation using Weblate (Ukrainian) Co-authored-by: Hosted Weblate Co-authored-by: Michael Michael Co-authored-by: sct Co-authored-by: Сергій Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/uk/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Catalan) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Catalan) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: dtalens Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Czech) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Czech) Currently translated at 99.6% (1236 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Karel Krýda Co-authored-by: Smexhy Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/cs/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Croatian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.8% (1238 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.8% (1238 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.6% (1236 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.5% (1235 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.5% (1235 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 97.5% (1210 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.5% (1185 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.6% (1182 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.6% (1182 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.2% (1177 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.2% (1177 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 94.3% (1166 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 91.7% (1134 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 91.7% (1134 of 1236 strings) Co-authored-by: Bruno Ševčenko Co-authored-by: Hosted Weblate Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Hungarian) Currently translated at 91.3% (1133 of 1240 strings) feat(lang): translated using Weblate (Hungarian) Currently translated at 89.3% (1108 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Levente Szajkó Co-authored-by: Nandor Rusz Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Hebrew) Currently translated at 13.9% (172 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: osh Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/he/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Polish) Currently translated at 99.1% (1225 of 1236 strings) Co-authored-by: Eryk Michalak Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pl/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Italian) Currently translated at 92.8% (1148 of 1236 strings) Co-authored-by: Francesco Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Fhd-pro Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ar/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Dutch) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Dutch) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: Kobe Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (French) Currently translated at 100.0% (1236 of 1236 strings) feat(lang): translated using Weblate (French) Currently translated at 99.9% (1235 of 1236 strings) feat(lang): translated using Weblate (French) Currently translated at 99.9% (1235 of 1236 strings) Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Hosted Weblate Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Finnish) Currently translated at 2.6% (33 of 1240 strings) feat(lang): added translation using Weblate (Finnish) Co-authored-by: Eero Konttaniemi Co-authored-by: Hosted Weblate Co-authored-by: sct Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fi/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Serbian) Currently translated at 50.8% (630 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Milan Smudja Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Korean) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Developer J Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ko/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Haohao Zhang Co-authored-by: Hosted Weblate Co-authored-by: lkw123 Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hans/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: Cleiton Carvalho Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Co-authored-by: Anders Ecklon Co-authored-by: Kenneth Hansen Co-authored-by: BeardedWatermelon Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: sct Co-authored-by: Michael Michael Co-authored-by: Сергій Co-authored-by: dtalens Co-authored-by: Karel Krýda Co-authored-by: Smexhy Co-authored-by: Bruno Ševčenko Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Co-authored-by: Levente Szajkó Co-authored-by: osh Co-authored-by: Eryk Michalak Co-authored-by: Francesco Co-authored-by: Fhd-pro Co-authored-by: Kobe Co-authored-by: gallegonovato Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: Eero Konttaniemi Co-authored-by: Milan Smudja Co-authored-by: Developer J Co-authored-by: Haohao Zhang Co-authored-by: lkw123 * feat(lang): add lang config for Bulgarian, Finnish, Ukrainian, Indonesian, Slovak, Turkish and Maori (#3834) * fix: correct deeplinks on iPad (#3883) * feat(studios): add a24 to studios list (#3902) * docs: add demrich as a contributor for code (#3906) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(watchlist): Cache watchlist requests with matching E-Tags (#3901) * perf(watchlist): add E-Tag caching to Plex watchlist requests * refactor(watchlist): increase frequency of watchlist requests * fix: sync watchlist every 3 min instead of 3 sec * docs: add maxnatamo as a contributor for code (#3907) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(plex): refresh token schedule (#3875) * feat: refresh token schedule fix #3861 * fix(i18n): add i18n message * refactor(plextv): use randomUUID crypto instead custom function * docs: add DamsDev1 as a contributor for code (#3924) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix: correct icon showing on certain phones when not pulled (#3939) * feat: add support for requesting "Specials" for TV Shows (#3724) * feat: add support for requesting "Specials" for TV Shows This commit is responsible for adding support in Overseerr for requesting "Special" episodes for TV Shows. This request has become especially pertinent when you consider shows like "Doctor Who". These shows have Specials that are critical to understanding the plot of a TV show. fix #779 * chore(yarn.lock): undo inappropriate changes to yarn.lock I was informed by @sct in a comment on the #3724 PR that it was not appropriate to commit the changes that ended up being made to the yarn.lock file. This commit is responsible, then, for undoing the changes to the yarn.lock file that ended up being submitted. * refactor: change loose equality to strict equality I received a comment from OwsleyJr pointing out that we are using loose equality when we could alternatively just be using strict equality to increase the robustness of our code. This commit does exactly that by squashing out previous usages of loose equality in my commits and replacing them with strict equality * refactor: move 'Specials' string to a global message Owsley pointed out that we are redefining the 'Specials' string multiple times throughout this PR. Instead, we can just move it as a global message. This commit does exactly that. It squashes out and previous declarations of the 'Specials' string inside the src files, and moves it directly to the global messages file. * docs: add AhmedNSidd as a contributor for code (#3964) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --------- Co-authored-by: Isaac M Co-authored-by: Joseph Risk Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Loetwiek <79059734+Loetwiek@users.noreply.github.com> Co-authored-by: Loetwiek Co-authored-by: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Co-authored-by: Fuochi Co-authored-by: Weblate (bot) Co-authored-by: Cleiton Carvalho Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Co-authored-by: Anders Ecklon Co-authored-by: Kenneth Hansen Co-authored-by: BeardedWatermelon Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: sct Co-authored-by: Michael Michael Co-authored-by: Сергій Co-authored-by: dtalens Co-authored-by: Karel Krýda Co-authored-by: Smexhy Co-authored-by: Bruno Ševčenko Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Co-authored-by: Levente Szajkó Co-authored-by: osh Co-authored-by: Eryk Michalak Co-authored-by: Francesco Co-authored-by: Fhd-pro Co-authored-by: Kobe Co-authored-by: gallegonovato Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: Eero Konttaniemi Co-authored-by: Milan Smudja Co-authored-by: Developer J Co-authored-by: Haohao Zhang Co-authored-by: lkw123 Co-authored-by: Jordan Jones Co-authored-by: Brandon Cohen Co-authored-by: David Emrich Co-authored-by: Max T. Kristiansen Co-authored-by: Damien Fajole <60252259+DamsDev1@users.noreply.github.com> Co-authored-by: Ahmed Siddiqui <36286128+AhmedNSidd@users.noreply.github.com> --- .all-contributorsrc | 63 ++++++++++ README.md | 6 + overseerr-api.yml | 4 +- server/api/plextv.ts | 112 +++++++++++++----- server/entity/MediaRequest.ts | 4 +- server/job/schedule.ts | 32 ++--- server/lib/cache.ts | 4 +- server/lib/refreshToken.ts | 37 ++++++ server/lib/scanners/plex/index.ts | 4 +- server/lib/scanners/sonarr/index.ts | 6 +- server/lib/settings/index.ts | 6 +- server/lib/watchlistsync.ts | 2 +- .../Discover/StudioSlider/index.tsx | 6 + src/components/Layout/PullToRefresh/index.tsx | 29 +++-- src/components/RequestBlock/index.tsx | 6 +- src/components/RequestCard/index.tsx | 9 +- .../RequestList/RequestItem/index.tsx | 10 +- .../RequestModal/TvRequestModal.tsx | 12 +- .../Settings/SettingsJobsCache/index.tsx | 1 + src/components/TvDetails/index.tsx | 22 +++- src/hooks/useDeepLinks.ts | 2 +- src/i18n/globalMessages.ts | 1 + src/i18n/locale/en.json | 3 +- 23 files changed, 291 insertions(+), 90 deletions(-) create mode 100644 server/lib/refreshToken.ts diff --git a/.all-contributorsrc b/.all-contributorsrc index 3614dbd1..f696ecb9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -448,6 +448,69 @@ "contributions": [ "security" ] + }, + { + "login": "j0srisk", + "name": "Joseph Risk", + "avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4", + "profile": "http://josephrisk.com", + "contributions": [ + "code" + ] + }, + { + "login": "Loetwiek", + "name": "Loetwiek", + "avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4", + "profile": "https://github.com/Loetwiek", + "contributions": [ + "code" + ] + }, + { + "login": "Fuochi", + "name": "Fuochi", + "avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4", + "profile": "https://github.com/Fuochi", + "contributions": [ + "doc" + ] + }, + { + "login": "demrich", + "name": "David Emrich", + "avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4", + "profile": "https://github.com/demrich", + "contributions": [ + "code" + ] + }, + { + "login": "maxnatamo", + "name": "Max T. Kristiansen", + "avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4", + "profile": "https://maxtrier.dk", + "contributions": [ + "code" + ] + }, + { + "login": "DamsDev1", + "name": "Damien Fajole", + "avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4", + "profile": "https://damsdev.me", + "contributions": [ + "code" + ] + }, + { + "login": "AhmedNSidd", + "name": "Ahmed Siddiqui", + "avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4", + "profile": "https://github.com/AhmedNSidd", + "contributions": [ + "code" + ] } ] } diff --git a/README.md b/README.md index fb6c8790..e33184a1 100644 --- a/README.md +++ b/README.md @@ -291,6 +291,12 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon Joseph Risk
Joseph Risk

💻 Loetwiek
Loetwiek

💻 Fuochi
Fuochi

📖 + David Emrich
David Emrich

💻 + Max T. Kristiansen
Max T. Kristiansen

💻 + Damien Fajole
Damien Fajole

💻 + + + Ahmed Siddiqui
Ahmed Siddiqui

💻 diff --git a/overseerr-api.yml b/overseerr-api.yml index dfbbfd08..3e7df27f 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -5486,7 +5486,7 @@ paths: - type: array items: type: number - minimum: 1 + minimum: 0 - type: string enum: [all] is4k: @@ -5592,7 +5592,7 @@ paths: type: array items: type: number - minimum: 1 + minimum: 0 is4k: type: boolean example: false diff --git a/server/api/plextv.ts b/server/api/plextv.ts index 27bed196..92bffa80 100644 --- a/server/api/plextv.ts +++ b/server/api/plextv.ts @@ -3,6 +3,7 @@ import type { PlexDevice } from '@server/interfaces/api/plexInterfaces'; import cacheManager from '@server/lib/cache'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; +import { randomUUID } from 'node:crypto'; import xml2js from 'xml2js'; interface PlexAccountResponse { @@ -127,6 +128,11 @@ export interface PlexWatchlistItem { title: string; } +export interface PlexWatchlistCache { + etag: string; + response: WatchlistResponse; +} + class PlexTvAPI extends ExternalAPI { private authToken: string; @@ -261,6 +267,11 @@ class PlexTvAPI extends ExternalAPI { items: PlexWatchlistItem[]; }> { try { + const watchlistCache = cacheManager.getCache('plexwatchlist'); + let cachedWatchlist = watchlistCache.data.get( + this.authToken + ); + const params = new URLSearchParams({ 'X-Plex-Container-Start': offset.toString(), 'X-Plex-Container-Size': size.toString(), @@ -268,42 +279,62 @@ class PlexTvAPI extends ExternalAPI { const response = await this.fetch( `https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`, { - headers: this.defaultHeaders, + headers: { + ...this.defaultHeaders, + ...(cachedWatchlist?.etag + ? { 'If-None-Match': cachedWatchlist.etag } + : {}), + }, } ); const data = (await response.json()) as WatchlistResponse; + // If we don't recieve HTTP 304, the watchlist has been updated and we need to update the cache. + if (response.status >= 200 && response.status <= 299) { + cachedWatchlist = { + etag: response.headers.get('etag') ?? '', + response: data, + }; + + watchlistCache.data.set( + this.authToken, + cachedWatchlist + ); + } + const watchlistDetails = await Promise.all( - (data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => { - const detailedResponse = await this.getRolling( - `/library/metadata/${watchlistItem.ratingKey}`, - {}, - undefined, - {}, - 'https://metadata.provider.plex.tv' - ); + (cachedWatchlist?.response.MediaContainer.Metadata ?? []).map( + async (watchlistItem) => { + const detailedResponse = await this.getRolling( + `/library/metadata/${watchlistItem.ratingKey}`, + {}, + undefined, + {}, + 'https://metadata.provider.plex.tv' + ); - const metadata = detailedResponse.MediaContainer.Metadata[0]; + const metadata = detailedResponse.MediaContainer.Metadata[0]; - const tmdbString = metadata.Guid.find((guid) => - guid.id.startsWith('tmdb') - ); - const tvdbString = metadata.Guid.find((guid) => - guid.id.startsWith('tvdb') - ); + const tmdbString = metadata.Guid.find((guid) => + guid.id.startsWith('tmdb') + ); + const tvdbString = metadata.Guid.find((guid) => + guid.id.startsWith('tvdb') + ); - return { - ratingKey: metadata.ratingKey, - // This should always be set? But I guess it also cannot be? - // We will filter out the 0's afterwards - tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0, - tvdbId: tvdbString - ? Number(tvdbString.id.split('//')[1]) - : undefined, - title: metadata.title, - type: metadata.type, - }; - }) + return { + ratingKey: metadata.ratingKey, + // This should always be set? But I guess it also cannot be? + // We will filter out the 0's afterwards + tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0, + tvdbId: tvdbString + ? Number(tvdbString.id.split('//')[1]) + : undefined, + title: metadata.title, + type: metadata.type, + }; + } + ) ); const filteredList = watchlistDetails.filter((detail) => detail.tmdbId); @@ -311,7 +342,7 @@ class PlexTvAPI extends ExternalAPI { return { offset, size, - totalSize: data.MediaContainer.totalSize, + totalSize: cachedWatchlist?.response.MediaContainer.totalSize ?? 0, items: filteredList, }; } catch (e) { @@ -327,6 +358,29 @@ class PlexTvAPI extends ExternalAPI { }; } } + + public async pingToken() { + try { + const data: { pong: unknown } = await this.get( + '/api/v2/ping', + {}, + undefined, + { + headers: { + 'X-Plex-Client-Identifier': randomUUID(), + }, + } + ); + if (!data?.pong) { + throw new Error('No pong response'); + } + } catch (e) { + logger.error('Failed to ping token', { + label: 'Plex Refresh Token', + errorMessage: e.message, + }); + } + } } export default PlexTvAPI; diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 6b2c7b56..f56a1b5e 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -257,9 +257,7 @@ export class MediaRequest { >; const requestedSeasons = requestBody.seasons === 'all' - ? tmdbMediaShow.seasons - .map((season) => season.season_number) - .filter((sn) => sn > 0) + ? tmdbMediaShow.seasons.map((season) => season.season_number) : (requestBody.seasons as number[]); let existingSeasons: number[] = []; diff --git a/server/job/schedule.ts b/server/job/schedule.ts index a210988e..ffc19daa 100644 --- a/server/job/schedule.ts +++ b/server/job/schedule.ts @@ -2,6 +2,7 @@ import { MediaServerType } from '@server/constants/server'; import availabilitySync from '@server/lib/availabilitySync'; import downloadTracker from '@server/lib/downloadtracker'; import ImageProxy from '@server/lib/imageproxy'; +import refreshToken from '@server/lib/refreshToken'; import { jellyfinFullScanner, jellyfinRecentScanner, @@ -13,7 +14,6 @@ import type { JobId } from '@server/lib/settings'; import { getSettings } from '@server/lib/settings'; import watchlistSync from '@server/lib/watchlistsync'; import logger from '@server/logger'; -import random from 'lodash/random'; import schedule from 'node-schedule'; interface ScheduledJob { @@ -113,30 +113,20 @@ export const startJobs = (): void => { } // Watchlist Sync - const watchlistSyncJob: ScheduledJob = { + scheduledJobs.push({ id: 'plex-watchlist-sync', name: 'Plex Watchlist Sync', type: 'process', - interval: 'fixed', + interval: 'seconds', cronSchedule: jobs['plex-watchlist-sync'].schedule, - job: schedule.scheduleJob(new Date(Date.now() + 1000 * 60 * 20), () => { + job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => { logger.info('Starting scheduled job: Plex Watchlist Sync', { label: 'Jobs', }); watchlistSync.syncWatchlist(); }), - }; - - // To help alleviate load on Plex's servers, we will add some fuzziness to the next schedule - // after each run - watchlistSyncJob.job.on('run', () => { - watchlistSyncJob.job.schedule( - new Date(Math.floor(Date.now() + 1000 * 60 * random(14, 24, true))) - ); }); - scheduledJobs.push(watchlistSyncJob); - // Run full radarr scan every 24 hours scheduledJobs.push({ id: 'radarr-scan', @@ -233,5 +223,19 @@ export const startJobs = (): void => { }), }); + scheduledJobs.push({ + id: 'plex-refresh-token', + name: 'Plex Refresh Token', + type: 'process', + interval: 'fixed', + cronSchedule: jobs['plex-refresh-token'].schedule, + job: schedule.scheduleJob(jobs['plex-refresh-token'].schedule, () => { + logger.info('Starting scheduled job: Plex Refresh Token', { + label: 'Jobs', + }); + refreshToken.run(); + }), + }); + logger.info('Scheduled jobs loaded', { label: 'Jobs' }); }; diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 011205e7..51d0e08f 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -8,7 +8,8 @@ export type AvailableCacheIds = | 'imdb' | 'github' | 'plexguid' - | 'plextv'; + | 'plextv' + | 'plexwatchlist'; const DEFAULT_TTL = 300; const DEFAULT_CHECK_PERIOD = 120; @@ -68,6 +69,7 @@ class CacheManager { stdTtl: 86400 * 7, // 1 week cache checkPeriod: 60, }), + plexwatchlist: new Cache('plexwatchlist', 'Plex Watchlist'), }; public getCache(id: AvailableCacheIds): Cache { diff --git a/server/lib/refreshToken.ts b/server/lib/refreshToken.ts new file mode 100644 index 00000000..ac7bd346 --- /dev/null +++ b/server/lib/refreshToken.ts @@ -0,0 +1,37 @@ +import PlexTvAPI from '@server/api/plextv'; +import { getRepository } from '@server/datasource'; +import { User } from '@server/entity/User'; +import logger from '@server/logger'; + +class RefreshToken { + public async run() { + const userRepository = getRepository(User); + + const users = await userRepository + .createQueryBuilder('user') + .addSelect('user.plexToken') + .where("user.plexToken != ''") + .getMany(); + + for (const user of users) { + await this.refreshUserToken(user); + } + } + + private async refreshUserToken(user: User) { + if (!user.plexToken) { + logger.warn('Skipping user refresh token for user without plex token', { + label: 'Plex Refresh Token', + user: user.displayName, + }); + return; + } + + const plexTvApi = new PlexTvAPI(user.plexToken); + plexTvApi.pingToken(); + } +} + +const refreshToken = new RefreshToken(); + +export default refreshToken; diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index f6049630..e4af7a1f 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -278,9 +278,7 @@ class PlexScanner const seasons = tvShow.seasons; const processableSeasons: ProcessableSeason[] = []; - const filteredSeasons = seasons.filter((sn) => sn.season_number !== 0); - - for (const season of filteredSeasons) { + for (const season of seasons) { const matchedPlexSeason = metadata.Children?.Metadata.find( (md) => Number(md.index) === season.season_number ); diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 3256c948..5d28e014 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -103,10 +103,8 @@ class SonarrScanner const tmdbId = tvShow.id; - const filteredSeasons = sonarrSeries.seasons.filter( - (sn) => - sn.seasonNumber !== 0 && - tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) + const filteredSeasons = sonarrSeries.seasons.filter((sn) => + tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) ); for (const season of filteredSeasons) { diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 425fc138..50933034 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -282,6 +282,7 @@ export type JobId = | 'plex-recently-added-scan' | 'plex-full-scan' | 'plex-watchlist-sync' + | 'plex-refresh-token' | 'radarr-scan' | 'sonarr-scan' | 'download-sync' @@ -469,7 +470,10 @@ class Settings { schedule: '0 0 3 * * *', }, 'plex-watchlist-sync': { - schedule: '0 */10 * * * *', + schedule: '0 */3 * * * *', + }, + 'plex-refresh-token': { + schedule: '0 0 5 * * *', }, 'radarr-scan': { schedule: '0 0 4 * * *', diff --git a/server/lib/watchlistsync.ts b/server/lib/watchlistsync.ts index 2d198451..4919bf70 100644 --- a/server/lib/watchlistsync.ts +++ b/server/lib/watchlistsync.ts @@ -62,7 +62,7 @@ class WatchlistSync { const plexTvApi = new PlexTvAPI(user.plexToken); - const response = await plexTvApi.getWatchlist({ size: 200 }); + const response = await plexTvApi.getWatchlist({ size: 20 }); const mediaItems = await Media.getRelatedMedia( user, diff --git a/src/components/Discover/StudioSlider/index.tsx b/src/components/Discover/StudioSlider/index.tsx index d1e88d45..6b23f4bd 100644 --- a/src/components/Discover/StudioSlider/index.tsx +++ b/src/components/Discover/StudioSlider/index.tsx @@ -74,6 +74,12 @@ const studios: Studio[] = [ 'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/2Tc1P3Ac8M479naPp1kYT3izLS5.png', url: '/discover/movies/studio/9993', }, + { + name: 'A24', + image: + 'https://image.tmdb.org/t/p/w780_filter(duotone,ffffff,bababa)/1ZXsGaFPgrgS6ZZGS37AqD5uU12.png', + url: '/discover/movies/studio/41077', + }, ]; const StudioSlider = () => { diff --git a/src/components/Layout/PullToRefresh/index.tsx b/src/components/Layout/PullToRefresh/index.tsx index cdedcf43..f2a1c7ba 100644 --- a/src/components/Layout/PullToRefresh/index.tsx +++ b/src/components/Layout/PullToRefresh/index.tsx @@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from 'react'; const PullToRefresh = () => { const router = useRouter(); - const [pullStartPoint, setPullStartPoint] = useState(0); const [pullChange, setPullChange] = useState(0); const refreshDiv = useRef(null); @@ -19,6 +18,7 @@ const PullToRefresh = () => { // Reload function that is called when reload threshold has been hit // Add loading class to determine when to add spin animation const forceReload = () => { + setPullStartPoint(0); refreshDiv.current?.classList.add('loading'); setTimeout(() => { router.reload(); @@ -32,6 +32,8 @@ const PullToRefresh = () => { const pullStart = (e: TouchEvent) => { setPullStartPoint(e.targetTouches[0].screenY); + const html = document.querySelector('html'); + if (window.scrollY === 0 && window.scrollX === 0) { refreshDiv.current?.classList.add('block'); refreshDiv.current?.classList.remove('hidden'); @@ -41,6 +43,7 @@ const PullToRefresh = () => { html.style.overscrollBehaviorY = 'none'; } } else { + setPullStartPoint(0); refreshDiv.current?.classList.remove('block'); refreshDiv.current?.classList.add('hidden'); } @@ -49,7 +52,6 @@ const PullToRefresh = () => { // Tracks how far we have pulled down the refresh icon const pullDown = async (e: TouchEvent) => { const screenY = e.targetTouches[0].screenY; - const pullLength = pullStartPoint < screenY ? Math.abs(screenY - pullStartPoint) : 0; @@ -59,12 +61,11 @@ const PullToRefresh = () => { // Will reload the page if we are past the threshold // Otherwise, we reset the pull const pullFinish = () => { - setPullStartPoint(0); - - if (pullDownReloadThreshold) { + if (pullDownReloadThreshold && pullStartPoint !== 0) { forceReload(); } else { setPullChange(0); + setTimeout(() => setPullStartPoint(0), 200); } document.body.style.touchAction = 'auto'; @@ -83,7 +84,21 @@ const PullToRefresh = () => { window.removeEventListener('touchmove', pullDown); window.removeEventListener('touchend', pullFinish); }; - }, [pullDownInitThreshold, pullDownReloadThreshold, pullStartPoint, router]); + }, [ + pullDownInitThreshold, + pullDownReloadThreshold, + pullStartPoint, + refreshDiv, + router, + setPullStartPoint, + ]); + + if ( + pullStartPoint === 0 && + !refreshDiv.current?.classList.contains('loading') + ) { + return null; + } return (
{
{ key={`season-${season.id}`} className="mb-1 mr-2 inline-block" > - {season.seasonNumber} + + {season.seasonNumber === 0 + ? intl.formatMessage(globalMessages.specials) + : season.seasonNumber} + ))}
diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index 1432148d..e737c733 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -411,8 +411,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { {intl.formatMessage(messages.seasons, { seasonCount: - title.seasons.filter((season) => season.seasonNumber !== 0) - .length === request.seasons.length + title.seasons.length === request.seasons.length ? 0 : request.seasons.length, })} @@ -420,7 +419,11 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
{request.seasons.map((season) => ( - {season.seasonNumber} + + {season.seasonNumber === 0 + ? intl.formatMessage(globalMessages.specials) + : season.seasonNumber} + ))}
diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 80ba2ab7..7f64039c 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -481,9 +481,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { {intl.formatMessage(messages.seasons, { seasonCount: - title.seasons.filter( - (season) => season.seasonNumber !== 0 - ).length === request.seasons.length + title.seasons.length === request.seasons.length ? 0 : request.seasons.length, })} @@ -491,7 +489,11 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
{request.seasons.map((season) => ( - {season.seasonNumber} + + {season.seasonNumber === 0 + ? intl.formatMessage(globalMessages.specials) + : season.seasonNumber} + ))}
diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 71750678..10c9c7db 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -42,7 +42,6 @@ const messages = defineMessages('components.RequestModal', { season: 'Season', numberofepisodes: '# of Episodes', seasonnumber: 'Season {number}', - extras: 'Extras', errorediting: 'Something went wrong while editing the request.', requestedited: 'Request for {title} edited successfully!', requestApproved: 'Request for {title} approved!', @@ -255,9 +254,7 @@ const TvRequestModal = ({ const getAllSeasons = (): number[] => { return (data?.seasons ?? []) - .filter( - (season) => season.seasonNumber !== 0 && season.episodeCount !== 0 - ) + .filter((season) => season.episodeCount !== 0) .map((season) => season.seasonNumber); }; @@ -580,10 +577,7 @@ const TvRequestModal = ({ {data?.seasons - .filter( - (season) => - season.seasonNumber !== 0 && season.episodeCount !== 0 - ) + .filter((season) => season.episodeCount !== 0) .map((season) => { const seasonRequest = getSeasonRequest( season.seasonNumber @@ -660,7 +654,7 @@ const TvRequestModal = ({ {season.seasonNumber === 0 - ? intl.formatMessage(messages.extras) + ? intl.formatMessage(globalMessages.specials) : intl.formatMessage(messages.seasonnumber, { number: season.seasonNumber, })} diff --git a/src/components/Settings/SettingsJobsCache/index.tsx b/src/components/Settings/SettingsJobsCache/index.tsx index 975de36c..aeba1531 100644 --- a/src/components/Settings/SettingsJobsCache/index.tsx +++ b/src/components/Settings/SettingsJobsCache/index.tsx @@ -58,6 +58,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages( 'plex-recently-added-scan': 'Plex Recently Added Scan', 'plex-full-scan': 'Plex Full Library Scan', 'plex-watchlist-sync': 'Plex Watchlist Sync', + 'plex-refresh-token': 'Plex Refresh Token', 'jellyfin-full-scan': 'Jellyfin Full Library Scan', 'jellyfin-recently-added-scan': 'Jellyfin Recently Added Scan', 'availability-sync': 'Media Availability Sync', diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index f4d058a8..bc4dfd7a 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -238,6 +238,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { ); } + // Does NOT include "Specials" const seasonCount = data.seasons.filter( (season) => season.seasonNumber !== 0 && season.episodeCount !== 0 ).length; @@ -299,9 +300,17 @@ const TvDetails = ({ tv }: TvDetailsProps) => { return [...requestedSeasons, ...availableSeasons]; }; - const isComplete = seasonCount <= getAllRequestedSeasons(false).length; + const showHasSpecials = data.seasons.some( + (season) => season.seasonNumber === 0 + ); - const is4kComplete = seasonCount <= getAllRequestedSeasons(true).length; + const isComplete = + (showHasSpecials ? seasonCount + 1 : seasonCount) <= + getAllRequestedSeasons(false).length; + + const is4kComplete = + (showHasSpecials ? seasonCount + 1 : seasonCount) <= + getAllRequestedSeasons(true).length; const streamingProviders = data?.watchProviders?.find((provider) => provider.iso_3166_1 === region) @@ -784,7 +793,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => { {data.seasons .slice() .reverse() - .filter((season) => season.seasonNumber !== 0) .map((season) => { const show4k = settings.currentSettings.series4kEnabled && @@ -838,9 +846,11 @@ const TvDetails = ({ tv }: TvDetailsProps) => { >
- {intl.formatMessage(messages.seasonnumber, { - seasonNumber: season.seasonNumber, - })} + {season.seasonNumber === 0 + ? intl.formatMessage(globalMessages.specials) + : intl.formatMessage(messages.seasonnumber, { + seasonNumber: season.seasonNumber, + })} {intl.formatMessage(messages.episodeCount, { diff --git a/src/hooks/useDeepLinks.ts b/src/hooks/useDeepLinks.ts index 98308659..bc367229 100644 --- a/src/hooks/useDeepLinks.ts +++ b/src/hooks/useDeepLinks.ts @@ -23,7 +23,7 @@ const useDeepLinks = ({ if ( settings.currentSettings.mediaServerType === MediaServerType.PLEX && (/iPad|iPhone|iPod/.test(navigator.userAgent) || - (navigator.userAgent === 'MacIntel' && navigator.maxTouchPoints > 1)) + (navigator.userAgent.includes('Mac') && navigator.maxTouchPoints > 1)) ) { setReturnedMediaUrl(iOSPlexUrl); setReturnedMediaUrl4k(iOSPlexUrl4k); diff --git a/src/i18n/globalMessages.ts b/src/i18n/globalMessages.ts index 6aa5ed1d..1765f193 100644 --- a/src/i18n/globalMessages.ts +++ b/src/i18n/globalMessages.ts @@ -65,6 +65,7 @@ const globalMessages = defineMessages('i18n', { '{title} was successfully removed from the Blacklist.', addToBlacklist: 'Add to Blacklist', removefromBlacklist: 'Remove from Blacklist', + specials: 'Specials', }); export default globalMessages; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 7086acfc..73ff71a6 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -536,7 +536,6 @@ "components.RequestModal.cancel": "Cancel Request", "components.RequestModal.edit": "Edit Request", "components.RequestModal.errorediting": "Something went wrong while editing the request.", - "components.RequestModal.extras": "Extras", "components.RequestModal.numberofepisodes": "# of Episodes", "components.RequestModal.pending4krequest": "Pending 4K Request", "components.RequestModal.pendingapproval": "Your request is pending approval.", @@ -847,6 +846,7 @@ "components.Settings.SettingsJobsCache.nextexecution": "Next Execution", "components.Settings.SettingsJobsCache.plex-full-scan": "Plex Full Library Scan", "components.Settings.SettingsJobsCache.plex-recently-added-scan": "Plex Recently Added Scan", + "components.Settings.SettingsJobsCache.plex-refresh-token": "Plex Refresh Token", "components.Settings.SettingsJobsCache.plex-watchlist-sync": "Plex Watchlist Sync", "components.Settings.SettingsJobsCache.process": "Process", "components.Settings.SettingsJobsCache.radarr-scan": "Radarr Scan", @@ -1381,6 +1381,7 @@ "i18n.saving": "Saving…", "i18n.settings": "Settings", "i18n.showingresults": "Showing {from} to {to} of {total} results", + "i18n.specials": "Specials", "i18n.status": "Status", "i18n.test": "Test", "i18n.testing": "Testing…",