mirror of
https://github.com/seerr-team/seerr.git
synced 2025-12-23 23:58:07 -05:00
chore: merge upstream (#1112)
* 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 <lodommerholtcm@gmail.com> * 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 <cleitonsilvacarvalho@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> 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 <hosted@weblate.org> Co-authored-by: Nandor Rusz <nandor.rusz@vodafone.de> Co-authored-by: Thomas Schöneberg <ta.schoeneberg@gmail.com> 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 <aecklon@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Kenneth Hansen <erathor@live.dk> 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 <BeardedWatermelon@users.noreply.hosted.weblate.org> Co-authored-by: Hosted Weblate <hosted@weblate.org> 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 <hosted@weblate.org> Co-authored-by: SoundwaveUwU <SoundwaveUwU@users.noreply.hosted.weblate.org> Co-authored-by: SoundwaveUwU <noreply@1000-7.space> Co-authored-by: Димитър Мазнеков (Topper) <d.maznekov@gmail.com> 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 <goldie.czr@gmail.com> Co-authored-by: Dragos <themsk@yahoo.com> Co-authored-by: Eduard Oancea <uberfly@420blaze.it> Co-authored-by: Hosted Weblate <hosted@weblate.org> 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 <hosted@weblate.org> Co-authored-by: sct <sctsnipe@gmail.com> Co-authored-by: Димитър Мазнеков (Topper) <d.maznekov@gmail.com> 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 <hosted@weblate.org> Co-authored-by: Michael Michael <michaelvelosk@gmail.com> Co-authored-by: sct <sctsnipe@gmail.com> Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com> 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 <hosted@weblate.org> Co-authored-by: dtalens <databio@gmail.com> 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 <hosted@weblate.org> Co-authored-by: Karel Krýda <karel.kryda@gmail.com> Co-authored-by: Smexhy <roman.bartik@icloud.com> 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 <bs3vcenk@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Milo Ivir <mail@milotype.de> Co-authored-by: Stjepan <stjepstjepanovic@gmail.com> Co-authored-by: lpispek <lpispek@gmail.com> 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 <hosted@weblate.org> Co-authored-by: Levente Szajkó <leviko112@gmail.com> Co-authored-by: Nandor Rusz <nandor.rusz@vodafone.de> 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 <hosted@weblate.org> Co-authored-by: osh <osh@osh.cc> 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 <gnu.ewm@protonmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> 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 <francy.ammirati@hotmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> 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 <juve.11@msn.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> 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 <hosted@weblate.org> Co-authored-by: Kobe <kobaubarr@gmail.com> 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 <hosted@weblate.org> Co-authored-by: gallegonovato <fran-carro@hotmail.es> 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 <baptiste.nee@me.com> Co-authored-by: Dimitri <dimitridroeck@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Maxime Lafarie <maxime.lafarie@gmail.com> Co-authored-by: Miguel <mig.mllr@gmail.com> Co-authored-by: asurare <jonathan.biteau16@gmail.com> 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 <hosted@weblate.org> Co-authored-by: Per Erik <urbanlolface@gmail.com> Co-authored-by: Shjosan <shjosan@kakmix.co> Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl> 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 <eero.konttaniemi@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: sct <sctsnipe@gmail.com> 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 <hosted@weblate.org> Co-authored-by: Milan Smudja <smudja@gmail.com> 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 <jshsakura@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> 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 <hyacz@foxmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: lkw123 <lkw20010211@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hans/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: Cleiton Carvalho <cleitonsilvacarvalho@gmail.com> Co-authored-by: Nandor Rusz <nandor.rusz@vodafone.de> Co-authored-by: Thomas Schöneberg <ta.schoeneberg@gmail.com> Co-authored-by: Anders Ecklon <aecklon@gmail.com> Co-authored-by: Kenneth Hansen <erathor@live.dk> Co-authored-by: BeardedWatermelon <BeardedWatermelon@users.noreply.hosted.weblate.org> Co-authored-by: SoundwaveUwU <SoundwaveUwU@users.noreply.hosted.weblate.org> Co-authored-by: SoundwaveUwU <noreply@1000-7.space> Co-authored-by: Димитър Мазнеков (Topper) <d.maznekov@gmail.com> Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Co-authored-by: Don Cezar <goldie.czr@gmail.com> Co-authored-by: Dragos <themsk@yahoo.com> Co-authored-by: Eduard Oancea <uberfly@420blaze.it> Co-authored-by: sct <sctsnipe@gmail.com> Co-authored-by: Michael Michael <michaelvelosk@gmail.com> Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com> Co-authored-by: dtalens <databio@gmail.com> Co-authored-by: Karel Krýda <karel.kryda@gmail.com> Co-authored-by: Smexhy <roman.bartik@icloud.com> Co-authored-by: Bruno Ševčenko <bs3vcenk@gmail.com> Co-authored-by: Milo Ivir <mail@milotype.de> Co-authored-by: Stjepan <stjepstjepanovic@gmail.com> Co-authored-by: lpispek <lpispek@gmail.com> Co-authored-by: Levente Szajkó <leviko112@gmail.com> Co-authored-by: osh <osh@osh.cc> Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com> Co-authored-by: Francesco <francy.ammirati@hotmail.com> Co-authored-by: Fhd-pro <juve.11@msn.com> Co-authored-by: Kobe <kobaubarr@gmail.com> Co-authored-by: gallegonovato <fran-carro@hotmail.es> Co-authored-by: Baptiste <baptiste.nee@me.com> Co-authored-by: Dimitri <dimitridroeck@gmail.com> Co-authored-by: Maxime Lafarie <maxime.lafarie@gmail.com> Co-authored-by: Miguel <mig.mllr@gmail.com> Co-authored-by: asurare <jonathan.biteau16@gmail.com> Co-authored-by: Per Erik <urbanlolface@gmail.com> Co-authored-by: Shjosan <shjosan@kakmix.co> Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl> Co-authored-by: Eero Konttaniemi <eero.konttaniemi@gmail.com> Co-authored-by: Milan Smudja <smudja@gmail.com> Co-authored-by: Developer J <jshsakura@gmail.com> Co-authored-by: Haohao Zhang <hyacz@foxmail.com> Co-authored-by: lkw123 <lkw20010211@gmail.com> * 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 <masesisaac@gmail.com> Co-authored-by: Joseph Risk <j0srisk@gmail.com> 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 <lodommerholtcm@gmail.com> Co-authored-by: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Co-authored-by: Fuochi <ffuochi@hotmail.com> Co-authored-by: Weblate (bot) <hosted@weblate.org> Co-authored-by: Cleiton Carvalho <cleitonsilvacarvalho@gmail.com> Co-authored-by: Nandor Rusz <nandor.rusz@vodafone.de> Co-authored-by: Thomas Schöneberg <ta.schoeneberg@gmail.com> Co-authored-by: Anders Ecklon <aecklon@gmail.com> Co-authored-by: Kenneth Hansen <erathor@live.dk> Co-authored-by: BeardedWatermelon <BeardedWatermelon@users.noreply.hosted.weblate.org> Co-authored-by: SoundwaveUwU <SoundwaveUwU@users.noreply.hosted.weblate.org> Co-authored-by: SoundwaveUwU <noreply@1000-7.space> Co-authored-by: Димитър Мазнеков (Topper) <d.maznekov@gmail.com> Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Co-authored-by: Don Cezar <goldie.czr@gmail.com> Co-authored-by: Dragos <themsk@yahoo.com> Co-authored-by: Eduard Oancea <uberfly@420blaze.it> Co-authored-by: sct <sctsnipe@gmail.com> Co-authored-by: Michael Michael <michaelvelosk@gmail.com> Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com> Co-authored-by: dtalens <databio@gmail.com> Co-authored-by: Karel Krýda <karel.kryda@gmail.com> Co-authored-by: Smexhy <roman.bartik@icloud.com> Co-authored-by: Bruno Ševčenko <bs3vcenk@gmail.com> Co-authored-by: Milo Ivir <mail@milotype.de> Co-authored-by: Stjepan <stjepstjepanovic@gmail.com> Co-authored-by: lpispek <lpispek@gmail.com> Co-authored-by: Levente Szajkó <leviko112@gmail.com> Co-authored-by: osh <osh@osh.cc> Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com> Co-authored-by: Francesco <francy.ammirati@hotmail.com> Co-authored-by: Fhd-pro <juve.11@msn.com> Co-authored-by: Kobe <kobaubarr@gmail.com> Co-authored-by: gallegonovato <fran-carro@hotmail.es> Co-authored-by: Baptiste <baptiste.nee@me.com> Co-authored-by: Dimitri <dimitridroeck@gmail.com> Co-authored-by: Maxime Lafarie <maxime.lafarie@gmail.com> Co-authored-by: Miguel <mig.mllr@gmail.com> Co-authored-by: asurare <jonathan.biteau16@gmail.com> Co-authored-by: Per Erik <urbanlolface@gmail.com> Co-authored-by: Shjosan <shjosan@kakmix.co> Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl> Co-authored-by: Eero Konttaniemi <eero.konttaniemi@gmail.com> Co-authored-by: Milan Smudja <smudja@gmail.com> Co-authored-by: Developer J <jshsakura@gmail.com> Co-authored-by: Haohao Zhang <hyacz@foxmail.com> Co-authored-by: lkw123 <lkw20010211@gmail.com> Co-authored-by: Jordan Jones <me@jjones.tech> Co-authored-by: Brandon Cohen <brandon@z3hn.dev> Co-authored-by: David Emrich <demrich@me.com> Co-authored-by: Max T. Kristiansen <me@maxtrier.dk> Co-authored-by: Damien Fajole <60252259+DamsDev1@users.noreply.github.com> Co-authored-by: Ahmed Siddiqui <36286128+AhmedNSidd@users.noreply.github.com>
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -291,6 +291,12 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=demrich" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=maxnatamo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=DamsDev1" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PlexWatchlistCache>(
|
||||
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<PlexWatchlistCache>(
|
||||
this.authToken,
|
||||
cachedWatchlist
|
||||
);
|
||||
}
|
||||
|
||||
const watchlistDetails = await Promise.all(
|
||||
(data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
'https://metadata.provider.plex.tv'
|
||||
);
|
||||
(cachedWatchlist?.response.MediaContainer.Metadata ?? []).map(
|
||||
async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/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;
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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' });
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
37
server/lib/refreshToken.ts
Normal file
37
server/lib/refreshToken.ts
Normal file
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 * * *',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div
|
||||
@@ -102,7 +117,7 @@ const PullToRefresh = () => {
|
||||
<div
|
||||
className={`${
|
||||
refreshDiv.current?.classList.contains('loading') && 'animate-spin'
|
||||
} relative -top-24 h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 shadow-md shadow-black ring-1 ring-gray-700`}
|
||||
} relative -top-28 h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 shadow-md shadow-black ring-1 ring-gray-700`}
|
||||
style={{ animationDirection: 'reverse' }}
|
||||
>
|
||||
<ArrowPathIcon
|
||||
|
||||
@@ -247,7 +247,11 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
key={`season-${season.id}`}
|
||||
className="mb-1 mr-2 inline-block"
|
||||
>
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
<Badge>
|
||||
{season.seasonNumber === 0
|
||||
? intl.formatMessage(globalMessages.specials)
|
||||
: season.seasonNumber}
|
||||
</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -411,8 +411,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
<span className="mr-2 font-bold ">
|
||||
{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) => {
|
||||
<div className="hide-scrollbar overflow-x-scroll">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
<Badge>
|
||||
{season.seasonNumber === 0
|
||||
? intl.formatMessage(globalMessages.specials)
|
||||
: season.seasonNumber}
|
||||
</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -481,9 +481,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
<span className="card-field-name">
|
||||
{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) => {
|
||||
<div className="hide-scrollbar flex flex-nowrap overflow-x-scroll">
|
||||
{request.seasons.map((season) => (
|
||||
<span key={`season-${season.id}`} className="mr-2">
|
||||
<Badge>{season.seasonNumber}</Badge>
|
||||
<Badge>
|
||||
{season.seasonNumber === 0
|
||||
? intl.formatMessage(globalMessages.specials)
|
||||
: season.seasonNumber}
|
||||
</Badge>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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 <strong>{title}</strong> edited successfully!',
|
||||
requestApproved: 'Request for <strong>{title}</strong> 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 = ({
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
{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 = ({
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-1 py-4 text-sm font-medium leading-5 text-gray-100 md:px-6">
|
||||
{season.seasonNumber === 0
|
||||
? intl.formatMessage(messages.extras)
|
||||
? intl.formatMessage(globalMessages.specials)
|
||||
: intl.formatMessage(messages.seasonnumber, {
|
||||
number: season.seasonNumber,
|
||||
})}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) => {
|
||||
>
|
||||
<div className="flex flex-1 items-center space-x-2 text-lg">
|
||||
<span>
|
||||
{intl.formatMessage(messages.seasonnumber, {
|
||||
seasonNumber: season.seasonNumber,
|
||||
})}
|
||||
{season.seasonNumber === 0
|
||||
? intl.formatMessage(globalMessages.specials)
|
||||
: intl.formatMessage(messages.seasonnumber, {
|
||||
seasonNumber: season.seasonNumber,
|
||||
})}
|
||||
</span>
|
||||
<Badge badgeType="dark">
|
||||
{intl.formatMessage(messages.episodeCount, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -65,6 +65,7 @@ const globalMessages = defineMessages('i18n', {
|
||||
'<strong>{title}</strong> was successfully removed from the Blacklist.',
|
||||
addToBlacklist: 'Add to Blacklist',
|
||||
removefromBlacklist: 'Remove from Blacklist',
|
||||
specials: 'Specials',
|
||||
});
|
||||
|
||||
export default globalMessages;
|
||||
|
||||
@@ -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 <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results",
|
||||
"i18n.specials": "Specials",
|
||||
"i18n.status": "Status",
|
||||
"i18n.test": "Test",
|
||||
"i18n.testing": "Testing…",
|
||||
|
||||
Reference in New Issue
Block a user