mirror of
https://github.com/seerr-team/seerr.git
synced 2025-12-23 15:48:12 -05:00
chore: merge upstream (#1592)
* 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> * feat(lang): Translations update from Hosted Weblate (#3835) * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Bulgarian) Currently translated at 100.0% (1240 of 1240 strings) Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Димитър Мазнеков (Topper) <d.maznekov@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/bg/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. 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) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Michael Michael <michaelvelosk@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/uk/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Catalan) Currently translated at 100.0% (1241 of 1241 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/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Hungarian) Currently translated at 99.2% (1231 of 1240 strings) Co-authored-by: Dargo <fuszi88@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Polish) Currently translated at 98.8% (1227 of 1241 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: senza <senza@users.noreply.hosted.weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pl/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Dutch) Currently translated at 100.0% (1241 of 1241 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Robin Van de Vyvere <irazoxgames@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1241 of 1241 strings) feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1241 of 1241 strings) Co-authored-by: Frostar <dasangra@hotmail.com> 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/ 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) Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (French) Currently translated at 100.0% (1241 of 1241 strings) feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Nackophilz <zrv4flra@anonaddy.me> Co-authored-by: TayZ3r <artimmo@hotmail.fr> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1241 of 1241 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) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Per Erik <urbanlolface@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ 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.9% (36 of 1240 strings) Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fi/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Albanian) Currently translated at 95.8% (1189 of 1240 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: W L <wl@mailhole.de> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sq/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Korean) Currently translated at 100.0% (1241 of 1241 strings) feat(lang): translated using Weblate (Korean) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Hyun Lee <hyun@yahoo.com> Co-authored-by: cutiekeek <cutiekeek@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ko/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Portuguese (Portugal)) Currently translated at 98.4% (1221 of 1240 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Rafael Souto <git@rafaelsouto.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_PT/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Chinese (Traditional Han script)) Currently translated at 99.9% (1239 of 1240 strings) Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Chinese (Traditional Han script)) Currently translated at 98.2% (1219 of 1241 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Marc Lerno <mlerno1192@student.carlalbert.edu> Co-authored-by: dtalens <databio@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Norwegian Bokmål) Currently translated at 89.9% (1115 of 1240 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: exentler <gurandsrud@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nb_NO/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: Димитър Мазнеков (Topper) <d.maznekov@gmail.com> Co-authored-by: Michael Michael <michaelvelosk@gmail.com> Co-authored-by: dtalens <databio@gmail.com> Co-authored-by: Dargo <fuszi88@gmail.com> Co-authored-by: senza <senza@users.noreply.hosted.weblate.org> Co-authored-by: Robin Van de Vyvere <irazoxgames@gmail.com> Co-authored-by: Frostar <dasangra@hotmail.com> Co-authored-by: gallegonovato <fran-carro@hotmail.es> Co-authored-by: Nackophilz <zrv4flra@anonaddy.me> Co-authored-by: TayZ3r <artimmo@hotmail.fr> Co-authored-by: Per Erik <urbanlolface@gmail.com> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: W L <wl@mailhole.de> Co-authored-by: Hyun Lee <hyun@yahoo.com> Co-authored-by: cutiekeek <cutiekeek@gmail.com> Co-authored-by: Rafael Souto <git@rafaelsouto.com> Co-authored-by: Marc Lerno <mlerno1192@student.carlalbert.edu> Co-authored-by: exentler <gurandsrud@gmail.com> * feat(ui): prevent password manager interference & improve service links (#3989) * docs: add s0up4200 as a contributor for code (#4047) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix(ui): update Plex Logo (#3955) * docs: add JackW6809 as a contributor for code (#4048) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat: requests/issues menu count (#3470) * feat: request and issue count added to sidebar/mobile menu * fix: added permission check for count visibility * refactor: modified badge design for count * fix: properly update issue and request counts in certain scenarios (#4051) * fix: center count badge on sidebar and mobile menu (#4052) * fix: request english trailers as a fallback when using other languages (#4009) Co-authored-by: Stancu Florin <florin@stancu.me> * docs: add StancuFlorin as a contributor for code (#4053) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat: added the PWA badge indicator for requests pending (#3411) refactor: removed unnecessary code when sending web push notification fix: moved all notify user logic into webpush refactor: n refactor: remove all unnecessary prettier changes fix: n fix: n fix: n fix: n fix: increment sw version fix: n * fix: improve count badge styling (#4056) * fix: improved web push management (#3421) refactor: organized placement of new button + added comments fix: added api routes for push registration fix: modified get request to confirm key identity fix: added back notification types to always show feat: added a manageable device list refactor: modified device list to make it mobile friendly fix: correct typo for enabling notifications * Revert "fix: improved web push management (#3421)" (#4058) * fix: manage webpush notifications (#4059) * feat(lang): Translations update from Hosted Weblate (#4025) * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Felipe Garcia <garcia.o.felipe@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (German) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Rico <rico.jambor@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Romanian) Currently translated at 40.8% (507 of 1240 strings) Co-authored-by: George L <lazugeorgem@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ro/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Yaroslav Buzko <yaroslav@buzko.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/uk/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Hungarian) Currently translated at 99.9% (1239 of 1240 strings) feat(lang): translated using Weblate (Hungarian) Currently translated at 99.7% (1237 of 1240 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: ugyes <ferenc.bodi@live.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Italian) Currently translated at 95.3% (1182 of 1240 strings) feat(lang): translated using Weblate (Italian) Currently translated at 95.3% (1182 of 1240 strings) Co-authored-by: Alberto Giardino <alberto.giardino@al-ce.it> Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * feat(lang): added translation using Weblate (Slovenian) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: sct <sctsnipe@gmail.com> * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: Felipe Garcia <garcia.o.felipe@gmail.com> Co-authored-by: Rico <rico.jambor@gmail.com> Co-authored-by: George L <lazugeorgem@gmail.com> Co-authored-by: Yaroslav Buzko <yaroslav@buzko.com> Co-authored-by: ugyes <ferenc.bodi@live.com> Co-authored-by: Alberto Giardino <alberto.giardino@al-ce.it> Co-authored-by: sct <sctsnipe@gmail.com> * fix: change localhost to process.env.HOST for client requests (#3839) * Change localhost to process.env.HOST for client requests * refactor: reformat * docs: add lmiklosko as a contributor for code (#4063) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix: set the correct TTL for the cookie store (#3946) The time-to-live (TTL) of cookies stored in the database was incorrect because the connect-typeorm library takes a TTL in seconds and not milliseconds, making cookies valid for ~82 years instead of 30 days. Co-authored-by: Ryan Cohen <ryan@sct.dev> * docs: add gauthier-th as a contributor for code (#4064) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix: update migration script (#4065) * fix: update migration script fix: remove insert for new entities * fix: correct migration name * fix: correct name inside migration * fix(servarr): merge series tags instead of overwriting them (#4019) * Merge series tags instead of overwriting when adding a series that already exists Currently, a request coming in for a series that already exists in sonarr nukes the tags in sonarr for the series in favor of the tags coming from overseerr. This change merges the two lists of tags and deduplicates them before sending them to sonarr. * fix(servarr api): merge request media tags with servarr instead of overwriting --------- Co-authored-by: Danshil Kokil Mungur <me@danshilm.com> * build(snap): update snap build actions * build: temporarily disable snap builds (#4074) * build: fix deploy docs action (#4076) [skip ci] * fix: display request button when the show has requestable specials and is available (#4081) * chore(deps): pin node.js (#4075) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): pin dependencies (#4084) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * feat(lang): Translations update from Hosted Weblate (#4060) * feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1259 of 1259 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1258 of 1258 strings) Update translation files Updated by "Cleanup translation files" hook in Weblate. feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1240 of 1240 strings) Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: st7105 <st7105@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Romanian) Currently translated at 50.6% (638 of 1259 strings) feat(lang): translated using Weblate (Romanian) Currently translated at 40.4% (509 of 1259 strings) feat(lang): translated using Weblate (Romanian) Currently translated at 40.3% (508 of 1259 strings) Co-authored-by: George L <lazugeorgem@gmail.com> 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 (Italian) Currently translated at 94.2% (1187 of 1259 strings) Co-authored-by: danieledu007 <danielguillen274@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (French) Currently translated at 100.0% (1259 of 1259 strings) Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: lolo <laurent.brelle@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Finnish) Currently translated at 9.2% (117 of 1259 strings) feat(lang): translated using Weblate (Finnish) Currently translated at 8.6% (109 of 1259 strings) Co-authored-by: Aleksi T <aleksi.tuhkanen@gmail.com> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: rijohi <risto.hirvilammi@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fi/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: st7105 <st7105@gmail.com> Co-authored-by: George L <lazugeorgem@gmail.com> Co-authored-by: danieledu007 <danielguillen274@gmail.com> Co-authored-by: lolo <laurent.brelle@gmail.com> Co-authored-by: Aleksi T <aleksi.tuhkanen@gmail.com> Co-authored-by: rijohi <risto.hirvilammi@gmail.com> * fix: correct specials affecting availability status (#4092) * fix: remove specials affecting availability status * refactor: add comments for scanner * fix: prevent request badge showing when no related media (#4100) * fix: availability sync requests (#3460) * fix: modified media status handling when media has been deleted fix: requests will now be updated to completed on scan fix: modified components to display deleted as a status fix: corrected media status switching away from deleted fix: modified components to display deleted as a status fix: corrected media status switching away from deleted fix: base scanner will set requests to completed correctly fix: mark available button correctly sets requests as completed fix: status will now stay deleted after declined request refactor: request completion handling moved to entity fix: prevented notifications from sending to old deleted requests refactor: cleaned up code and added more detail to logs refactor: updated to reflect latest availability sync changes * fix: fetch requests only if necessary in db and remove unneeded code * fix: update request button logic to accomodate specials fix: remove completed filtering in tv details * fix: correctly set seasons status when using the manual button * refactor: improve reliability of season request completion refactor: remove seasonrequest code * fix: send notification for 4k movies fix: same for shows * feat: add completed filter to requests list refactor: correct label * fix: correct series setting to partially available (#4109) * fix: correct edge case with deletion not updating requests (#4110) * fix: correct notification sending for wrong status (#4113) * fix: add missing translations * fix: update lockfile * fix(notifications): sending test-notifications for email now respects the allowSelfSigned value (#4112) * docs: add vfaergestad as a contributor for code (#4114) * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix: handle currently available non-completed media (#4115) * fix: check media status has changed before modifying request fix: refactor: code cleanup * fix: manually load database entity seasons * fix: handle partial seasons more reliably (#4116) refactor: remove matchingOldSeason variable * fix: pwa app badge (#4117) * fix: clear app badge at zero without notification * fix: correct users check sometimes failing when searching push subs * fix: filter specials from modal all seasons and watchlist (#4108) * fix: filter specials from modal all seasons and watchlist * fix: skip specials when marking available * fix: edge case where specials were marked as completed * fix: add proper migrations * fix: remove undesirable i18n changes --------- 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> Co-authored-by: Dargo <fuszi88@gmail.com> Co-authored-by: senza <senza@users.noreply.hosted.weblate.org> Co-authored-by: Robin Van de Vyvere <irazoxgames@gmail.com> Co-authored-by: Frostar <dasangra@hotmail.com> Co-authored-by: Nackophilz <zrv4flra@anonaddy.me> Co-authored-by: TayZ3r <artimmo@hotmail.fr> Co-authored-by: Oskari Lavinto <olavinto@protonmail.com> Co-authored-by: W L <wl@mailhole.de> Co-authored-by: Hyun Lee <hyun@yahoo.com> Co-authored-by: cutiekeek <cutiekeek@gmail.com> Co-authored-by: Rafael Souto <git@rafaelsouto.com> Co-authored-by: Marc Lerno <mlerno1192@student.carlalbert.edu> Co-authored-by: exentler <gurandsrud@gmail.com> Co-authored-by: soup <s0up4200@pm.me> Co-authored-by: JackOXI <53652452+JackW6809@users.noreply.github.com> Co-authored-by: Stancu Florin <StancuFlorin@users.noreply.github.com> Co-authored-by: Stancu Florin <florin@stancu.me> Co-authored-by: Brandon Cohen <cohbrandon@gmail.com> Co-authored-by: Felipe Garcia <garcia.o.felipe@gmail.com> Co-authored-by: Rico <rico.jambor@gmail.com> Co-authored-by: George L <lazugeorgem@gmail.com> Co-authored-by: Yaroslav Buzko <yaroslav@buzko.com> Co-authored-by: ugyes <ferenc.bodi@live.com> Co-authored-by: Alberto Giardino <alberto.giardino@al-ce.it> Co-authored-by: Lukas Miklosko <44380311+lmiklosko@users.noreply.github.com> Co-authored-by: Ryan Cohen <ryan@sct.dev> Co-authored-by: Andrew Kennedy <andrew-kennedy@users.noreply.github.com> Co-authored-by: Danshil Kokil Mungur <me@danshilm.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: st7105 <st7105@gmail.com> Co-authored-by: danieledu007 <danielguillen274@gmail.com> Co-authored-by: lolo <laurent.brelle@gmail.com> Co-authored-by: Aleksi T <aleksi.tuhkanen@gmail.com> Co-authored-by: rijohi <risto.hirvilammi@gmail.com> Co-authored-by: vfaergestad <49147564+vfaergestad@users.noreply.github.com>
This commit is contained in:
@@ -277,7 +277,8 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13810742?v=4",
|
||||
"profile": "https://athfan.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -847,6 +848,114 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JackW6809",
|
||||
"name": "JackOXI",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
|
||||
"profile": "https://github.com/JackW6809",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "StancuFlorin",
|
||||
"name": "Stancu Florin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
|
||||
"profile": "http://indicus.ro",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lmiklosko",
|
||||
"name": "Lukas Miklosko",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/44380311?v=4",
|
||||
"profile": "https://github.com/lmiklosko",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gauthier-th",
|
||||
"name": "Gauthier",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
|
||||
"profile": "https://gauthierth.fr/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "vfaergestad",
|
||||
"name": "vfaergestad",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/49147564?v=4",
|
||||
"profile": "https://github.com/vfaergestad",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -19,5 +19,6 @@
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"files.associations": {
|
||||
"globals.css": "tailwindcss"
|
||||
}
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/i18n/locale"]
|
||||
}
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -42,13 +42,13 @@ FROM node:22-alpine
|
||||
ARG BUILD_DATE
|
||||
ARG BUILD_VERSION
|
||||
LABEL \
|
||||
org.opencontainers.image.authors="Fallenbagel" \
|
||||
org.opencontainers.image.source="https://github.com/fallenbagel/jellyseerr" \
|
||||
org.opencontainers.image.created=${BUILD_DATE} \
|
||||
org.opencontainers.image.version=${BUILD_VERSION} \
|
||||
org.opencontainers.image.title="Jellyseerr" \
|
||||
org.opencontainers.image.description="Open-source media request and discovery manager for Jellyfin, Plex, and Emby." \
|
||||
org.opencontainers.image.licenses="MIT"
|
||||
org.opencontainers.image.authors="Fallenbagel" \
|
||||
org.opencontainers.image.source="https://github.com/fallenbagel/jellyseerr" \
|
||||
org.opencontainers.image.created=${BUILD_DATE} \
|
||||
org.opencontainers.image.version=${BUILD_VERSION} \
|
||||
org.opencontainers.image.title="Jellyseerr" \
|
||||
org.opencontainers.image.description="Open-source media request and discovery manager for Jellyfin, Plex, and Emby." \
|
||||
org.opencontainers.image.licenses="MIT"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -358,6 +358,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lmiklosko"><img src="https://avatars.githubusercontent.com/u/44380311?v=4?s=100" width="100px;" alt="Lukas Miklosko"/><br /><sub><b>Lukas Miklosko</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=lmiklosko" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=gauthier-th" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vfaergestad"><img src="https://avatars.githubusercontent.com/u/49147564?v=4?s=100" width="100px;" alt="vfaergestad"/><br /><sub><b>vfaergestad</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=vfaergestad" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1157,7 +1157,7 @@ components:
|
||||
status:
|
||||
type: number
|
||||
example: 0
|
||||
description: Availability of the media. 1 = `UNKNOWN`, 2 = `PENDING`, 3 = `PROCESSING`, 4 = `PARTIALLY_AVAILABLE`, 5 = `AVAILABLE`
|
||||
description: Availability of the media. 1 = `UNKNOWN`, 2 = `PENDING`, 3 = `PROCESSING`, 4 = `PARTIALLY_AVAILABLE`, 5 = `AVAILABLE`, 6 = `DELETED`
|
||||
requests:
|
||||
type: array
|
||||
readOnly: true
|
||||
@@ -5676,6 +5676,8 @@ paths:
|
||||
processing,
|
||||
unavailable,
|
||||
failed,
|
||||
deleted,
|
||||
completed,
|
||||
]
|
||||
- in: query
|
||||
name: sort
|
||||
@@ -6422,7 +6424,16 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
enum: [all, available, partial, allavailable, processing, pending]
|
||||
enum:
|
||||
[
|
||||
all,
|
||||
available,
|
||||
partial,
|
||||
allavailable,
|
||||
processing,
|
||||
pending,
|
||||
deleted,
|
||||
]
|
||||
- in: query
|
||||
name: sort
|
||||
schema:
|
||||
@@ -6498,7 +6509,7 @@ paths:
|
||||
example: available
|
||||
schema:
|
||||
type: string
|
||||
enum: [available, partial, processing, pending, unknown]
|
||||
enum: [available, partial, processing, pending, unknown, deleted]
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
"swagger-ui-express": "4.6.2",
|
||||
"swr": "2.2.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typeorm": "0.3.12",
|
||||
"undici": "^7.3.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"web-push": "3.5.0",
|
||||
|
||||
42
pnpm-lock.yaml
generated
42
pnpm-lock.yaml
generated
@@ -64,7 +64,7 @@ importers:
|
||||
version: 2.11.0
|
||||
connect-typeorm:
|
||||
specifier: 1.1.4
|
||||
version: 1.1.4(typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)))
|
||||
version: 1.1.4(typeorm@0.3.12(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)))
|
||||
cookie-parser:
|
||||
specifier: 1.4.7
|
||||
version: 1.4.7
|
||||
@@ -213,8 +213,8 @@ importers:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
typeorm:
|
||||
specifier: 0.3.11
|
||||
version: 0.3.11(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))
|
||||
specifier: 0.3.12
|
||||
version: 0.3.12(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))
|
||||
ua-parser-js:
|
||||
specifier: ^1.0.35
|
||||
version: 1.0.40
|
||||
@@ -6973,6 +6973,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mkdirp@2.1.6:
|
||||
resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
modify-values@1.0.1:
|
||||
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -9190,8 +9195,8 @@ packages:
|
||||
typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
|
||||
typeorm@0.3.11:
|
||||
resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==}
|
||||
typeorm@0.3.12:
|
||||
resolution: {integrity: sha512-sYSxBmCf1nJLLTcYtwqZ+lQIRtLPyUoO93rHTOKk9vJCyT4UfRtU7oRsJvfvKP3nnZTD1hzz2SEy2zwPEN6OyA==}
|
||||
engines: {node: '>= 12.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -9202,7 +9207,7 @@ packages:
|
||||
ioredis: ^5.0.4
|
||||
mongodb: ^3.6.0
|
||||
mssql: ^7.3.0
|
||||
mysql2: ^2.2.5
|
||||
mysql2: ^2.2.5 || ^3.0.1
|
||||
oracledb: ^5.1.0
|
||||
pg: ^8.5.1
|
||||
pg-native: ^3.0.0
|
||||
@@ -14913,13 +14918,13 @@ snapshots:
|
||||
ini: 1.3.8
|
||||
proto-list: 1.2.4
|
||||
|
||||
connect-typeorm@1.1.4(typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))):
|
||||
connect-typeorm@1.1.4(typeorm@0.3.12(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))):
|
||||
dependencies:
|
||||
'@types/debug': 0.0.31
|
||||
'@types/express-session': 1.17.6
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
express-session: 1.17.3
|
||||
typeorm: 0.3.11(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))
|
||||
typeorm: 0.3.12(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -15794,7 +15799,7 @@ snapshots:
|
||||
debug: 4.3.5
|
||||
enhanced-resolve: 5.17.0
|
||||
eslint: 8.35.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.5
|
||||
@@ -15816,7 +15821,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0):
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
optionalDependencies:
|
||||
@@ -18282,6 +18287,8 @@ snapshots:
|
||||
|
||||
mkdirp@1.0.4: {}
|
||||
|
||||
mkdirp@2.1.6: {}
|
||||
|
||||
modify-values@1.0.1: {}
|
||||
|
||||
moment@2.30.1: {}
|
||||
@@ -20699,7 +20706,7 @@ snapshots:
|
||||
|
||||
typedarray@0.0.6: {}
|
||||
|
||||
typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)):
|
||||
typeorm@0.3.12(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5)):
|
||||
dependencies:
|
||||
'@sqltools/formatter': 1.2.5
|
||||
app-root-path: 3.1.0
|
||||
@@ -20707,15 +20714,15 @@ snapshots:
|
||||
chalk: 4.1.2
|
||||
cli-highlight: 2.1.11
|
||||
date-fns: 2.29.3
|
||||
debug: 4.3.5
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
dotenv: 16.4.5
|
||||
glob: 7.2.3
|
||||
glob: 8.1.0
|
||||
js-yaml: 4.1.0
|
||||
mkdirp: 1.0.4
|
||||
mkdirp: 2.1.6
|
||||
reflect-metadata: 0.1.13
|
||||
sha.js: 2.4.11
|
||||
tslib: 2.6.3
|
||||
uuid: 8.3.2
|
||||
tslib: 2.8.1
|
||||
uuid: 9.0.1
|
||||
xml2js: 0.4.23
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
@@ -20884,8 +20891,7 @@ snapshots:
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
uuid@9.0.1:
|
||||
optional: true
|
||||
uuid@9.0.1: {}
|
||||
|
||||
uvu@0.5.6:
|
||||
dependencies:
|
||||
|
||||
@@ -367,12 +367,12 @@ class PlexTvAPI extends ExternalAPI {
|
||||
|
||||
public async pingToken() {
|
||||
try {
|
||||
const data: { pong: unknown } = await this.get('/api/v2/ping', {
|
||||
const response = await this.axios.get('/api/v2/ping', {
|
||||
headers: {
|
||||
'X-Plex-Client-Identifier': randomUUID(),
|
||||
},
|
||||
});
|
||||
if (!data?.pong) {
|
||||
if (!response?.data?.pong) {
|
||||
throw new Error('No pong response');
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -3,6 +3,7 @@ export enum MediaRequestStatus {
|
||||
APPROVED,
|
||||
DECLINED,
|
||||
FAILED,
|
||||
COMPLETED,
|
||||
}
|
||||
|
||||
export enum MediaType {
|
||||
@@ -17,4 +18,5 @@ export enum MediaStatus {
|
||||
PARTIALLY_AVAILABLE,
|
||||
AVAILABLE,
|
||||
BLACKLISTED,
|
||||
DELETED,
|
||||
}
|
||||
|
||||
@@ -181,7 +181,8 @@ export class MediaRequest {
|
||||
// If there is an existing movie request that isn't declined, don't allow a new one.
|
||||
if (
|
||||
requestBody.mediaType === MediaType.MOVIE &&
|
||||
existing[0].status !== MediaRequestStatus.DECLINED
|
||||
existing[0].status !== MediaRequestStatus.DECLINED &&
|
||||
existing[0].status !== MediaRequestStatus.COMPLETED
|
||||
) {
|
||||
logger.warn('Duplicate request for media blocked', {
|
||||
tmdbId: tmdbMedia.id,
|
||||
@@ -388,7 +389,9 @@ export class MediaRequest {
|
||||
>;
|
||||
let requestedSeasons =
|
||||
requestBody.seasons === 'all'
|
||||
? tmdbMediaShow.seasons.map((season) => season.season_number)
|
||||
? tmdbMediaShow.seasons
|
||||
.filter((season) => season.season_number !== 0)
|
||||
.map((season) => season.season_number)
|
||||
: (requestBody.seasons as number[]);
|
||||
if (!settings.main.enableSpecialEpisodes) {
|
||||
requestedSeasons = requestedSeasons.filter((sn) => sn > 0);
|
||||
@@ -404,7 +407,8 @@ export class MediaRequest {
|
||||
.filter(
|
||||
(request) =>
|
||||
request.is4k === requestBody.is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
request.status !== MediaRequestStatus.DECLINED &&
|
||||
request.status !== MediaRequestStatus.COMPLETED
|
||||
)
|
||||
.reduce((seasons, request) => {
|
||||
const combinedSeasons = request.seasons.map(
|
||||
@@ -423,7 +427,9 @@ export class MediaRequest {
|
||||
.filter(
|
||||
(season) =>
|
||||
season[requestBody.is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.UNKNOWN
|
||||
MediaStatus.UNKNOWN &&
|
||||
season[requestBody.is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.DELETED
|
||||
)
|
||||
.map((season) => season.seasonNumber),
|
||||
];
|
||||
@@ -732,7 +738,8 @@ export class MediaRequest {
|
||||
|
||||
if (
|
||||
media.mediaType === MediaType.MOVIE &&
|
||||
this.status === MediaRequestStatus.DECLINED
|
||||
this.status === MediaRequestStatus.DECLINED &&
|
||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
|
||||
) {
|
||||
const statusField = this.is4k ? 'status4k' : 'status';
|
||||
await mediaRepository.update(
|
||||
@@ -753,7 +760,8 @@ export class MediaRequest {
|
||||
media.requests.filter(
|
||||
(request) => request.status === MediaRequestStatus.PENDING
|
||||
).length === 0 &&
|
||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
|
||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
|
||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
|
||||
) {
|
||||
const statusField = this.is4k ? 'status4k' : 'status';
|
||||
mediaRepository.update(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { MediaRequestStatus } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import {
|
||||
AfterRemove,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@@ -36,18 +34,6 @@ class SeasonRequest {
|
||||
constructor(init?: Partial<SeasonRequest>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@AfterRemove()
|
||||
public async handleRemoveParent(): Promise<void> {
|
||||
const mediaRequestRepository = getRepository(MediaRequest);
|
||||
const requestToBeDeleted = await mediaRequestRepository.findOneOrFail({
|
||||
where: { id: this.request.id },
|
||||
});
|
||||
|
||||
if (requestToBeDeleted.seasons.length === 0) {
|
||||
await mediaRequestRepository.delete({ id: this.request.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SeasonRequest;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import MediaRequest from '@server/entity/MediaRequest';
|
||||
import type Season from '@server/entity/Season';
|
||||
import SeasonRequest from '@server/entity/SeasonRequest';
|
||||
import { User } from '@server/entity/User';
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
@@ -42,7 +41,7 @@ class AvailabilitySync {
|
||||
|
||||
try {
|
||||
logger.info(`Starting availability sync...`, {
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
});
|
||||
const pageSize = 50;
|
||||
|
||||
@@ -456,11 +455,11 @@ class AvailabilitySync {
|
||||
} catch (ex) {
|
||||
logger.error('Failed to complete availability sync.', {
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
});
|
||||
} finally {
|
||||
logger.info(`Availability sync complete.`, {
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
});
|
||||
this.running = false;
|
||||
}
|
||||
@@ -496,98 +495,66 @@ class AvailabilitySync {
|
||||
} while (mediaPage.length > 0);
|
||||
}
|
||||
|
||||
private findMediaStatus(
|
||||
requests: MediaRequest[],
|
||||
is4k: boolean
|
||||
): MediaStatus {
|
||||
const filteredRequests = requests.filter(
|
||||
(request) => request.is4k === is4k
|
||||
);
|
||||
|
||||
let mediaStatus: MediaStatus;
|
||||
|
||||
if (
|
||||
filteredRequests.some(
|
||||
(request) => request.status === MediaRequestStatus.APPROVED
|
||||
)
|
||||
) {
|
||||
mediaStatus = MediaStatus.PROCESSING;
|
||||
} else if (
|
||||
filteredRequests.some(
|
||||
(request) => request.status === MediaRequestStatus.PENDING
|
||||
)
|
||||
) {
|
||||
mediaStatus = MediaStatus.PENDING;
|
||||
} else {
|
||||
mediaStatus = MediaStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
return mediaStatus;
|
||||
}
|
||||
|
||||
private async mediaUpdater(
|
||||
media: Media,
|
||||
is4k: boolean,
|
||||
mediaServerType: MediaServerType
|
||||
): Promise<void> {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
try {
|
||||
// Find all related requests only if
|
||||
// the related media has an available status
|
||||
const requests = await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoinAndSelect('request.media', 'media')
|
||||
.where('(media.id = :id)', {
|
||||
id: media.id,
|
||||
})
|
||||
.andWhere(
|
||||
`(request.is4k = :is4k AND media.${
|
||||
is4k ? 'status4k' : 'status'
|
||||
} IN (:...mediaStatus))`,
|
||||
{
|
||||
mediaStatus: [
|
||||
MediaStatus.AVAILABLE,
|
||||
MediaStatus.PARTIALLY_AVAILABLE,
|
||||
],
|
||||
is4k: is4k,
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
// Check if a season is processing or pending to
|
||||
// make sure we set the media to the correct status
|
||||
let mediaStatus = MediaStatus.UNKNOWN;
|
||||
// If media type is tv, check if a season is processing
|
||||
// to see if we need to keep the external metadata
|
||||
let isMediaProcessing = false;
|
||||
|
||||
if (media.mediaType === 'tv') {
|
||||
mediaStatus = this.findMediaStatus(requests, is4k);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
const request = await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoinAndSelect('request.media', 'media')
|
||||
.where('(media.id = :id)', {
|
||||
id: media.id,
|
||||
})
|
||||
.andWhere(
|
||||
'(request.is4k = :is4k AND request.status = :requestStatus)',
|
||||
{
|
||||
requestStatus: MediaRequestStatus.APPROVED,
|
||||
is4k: is4k,
|
||||
}
|
||||
)
|
||||
.getOne();
|
||||
|
||||
if (request) {
|
||||
isMediaProcessing = true;
|
||||
}
|
||||
}
|
||||
|
||||
media[is4k ? 'status4k' : 'status'] = mediaStatus;
|
||||
media[is4k ? 'serviceId4k' : 'serviceId'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
? media[is4k ? 'serviceId4k' : 'serviceId']
|
||||
: null;
|
||||
// Set the non-4K or 4K media to deleted
|
||||
// and change related columns to null if media
|
||||
// is not processing
|
||||
media[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED;
|
||||
media[is4k ? 'serviceId4k' : 'serviceId'] = isMediaProcessing
|
||||
? media[is4k ? 'serviceId4k' : 'serviceId']
|
||||
: null;
|
||||
media[is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
isMediaProcessing
|
||||
? media[is4k ? 'externalServiceId4k' : 'externalServiceId']
|
||||
: null;
|
||||
media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
isMediaProcessing
|
||||
? media[is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']
|
||||
: null;
|
||||
if (mediaServerType === MediaServerType.PLEX) {
|
||||
media[is4k ? 'ratingKey4k' : 'ratingKey'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
||||
: null;
|
||||
media[is4k ? 'ratingKey4k' : 'ratingKey'] = isMediaProcessing
|
||||
? media[is4k ? 'ratingKey4k' : 'ratingKey']
|
||||
: null;
|
||||
} else if (
|
||||
mediaServerType === MediaServerType.JELLYFIN ||
|
||||
mediaServerType === MediaServerType.EMBY
|
||||
) {
|
||||
media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId'] =
|
||||
mediaStatus === MediaStatus.PROCESSING
|
||||
isMediaProcessing
|
||||
? media[is4k ? 'jellyfinMediaId4k' : 'jellyfinMediaId']
|
||||
: null;
|
||||
}
|
||||
@@ -602,18 +569,11 @@ class AvailabilitySync {
|
||||
: mediaServerType === MediaServerType.JELLYFIN
|
||||
? 'jellyfin'
|
||||
: 'emby'
|
||||
} instance. Status will be changed to unknown.`,
|
||||
} instance. Status will be changed to deleted.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
|
||||
await mediaRepository.save({ media, ...media });
|
||||
|
||||
// Only delete media request if type is movie.
|
||||
// Type tv request deletion is handled
|
||||
// in the season request entity
|
||||
if (requests.length > 0 && media.mediaType === 'movie') {
|
||||
await requestRepository.remove(requests);
|
||||
}
|
||||
await mediaRepository.save(media);
|
||||
} catch (ex) {
|
||||
logger.debug(
|
||||
`Failure updating the ${is4k ? '4K' : 'non-4K'} ${
|
||||
@@ -621,7 +581,7 @@ class AvailabilitySync {
|
||||
} [TMDB ID ${media.tmdbId}].`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -634,61 +594,44 @@ class AvailabilitySync {
|
||||
mediaServerType: MediaServerType
|
||||
): Promise<void> {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||
|
||||
// Filter out only the values that are false
|
||||
// (media that should be deleted)
|
||||
const seasonsPendingRemoval = new Map(
|
||||
// Disabled linter as only the value is needed from the filter
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
[...seasons].filter(([_, exists]) => !exists)
|
||||
);
|
||||
// Retrieve the season keys to pass into our log
|
||||
const seasonKeys = [...seasonsPendingRemoval.keys()];
|
||||
|
||||
// let isSeasonRemoved = false;
|
||||
|
||||
try {
|
||||
// Need to check and see if there are any related season
|
||||
// requests. If they are, we will need to delete them.
|
||||
const seasonRequests = await seasonRequestRepository
|
||||
.createQueryBuilder('seasonRequest')
|
||||
.leftJoinAndSelect('seasonRequest.request', 'request')
|
||||
.leftJoinAndSelect('request.media', 'media')
|
||||
.where('(media.id = :id)', { id: media.id })
|
||||
.andWhere(
|
||||
'(request.is4k = :is4k AND seasonRequest.seasonNumber IN (:...seasonNumbers))',
|
||||
{
|
||||
seasonNumbers: seasonKeys,
|
||||
is4k: is4k,
|
||||
}
|
||||
)
|
||||
.getMany();
|
||||
|
||||
for (const mediaSeason of media.seasons) {
|
||||
if (seasonsPendingRemoval.has(mediaSeason.seasonNumber)) {
|
||||
mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||
mediaSeason[is4k ? 'status4k' : 'status'] = MediaStatus.DELETED;
|
||||
}
|
||||
}
|
||||
|
||||
if (media.status === MediaStatus.AVAILABLE) {
|
||||
if (media.status === MediaStatus.AVAILABLE && !is4k) {
|
||||
media.status = MediaStatus.PARTIALLY_AVAILABLE;
|
||||
logger.info(
|
||||
`Marking the non-4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
{ label: 'Availability Sync' }
|
||||
);
|
||||
}
|
||||
|
||||
if (media.status4k === MediaStatus.AVAILABLE) {
|
||||
if (media.status4k === MediaStatus.AVAILABLE && is4k) {
|
||||
media.status4k = MediaStatus.PARTIALLY_AVAILABLE;
|
||||
logger.info(
|
||||
`Marking the 4K show [TMDB ID ${media.tmdbId}] as PARTIALLY_AVAILABLE because season removal has occurred.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
{ label: 'Availability Sync' }
|
||||
);
|
||||
}
|
||||
|
||||
await mediaRepository.save({ media, ...media });
|
||||
|
||||
if (seasonRequests.length > 0) {
|
||||
await seasonRequestRepository.remove(seasonRequests);
|
||||
}
|
||||
media.lastSeasonChange = new Date();
|
||||
await mediaRepository.save(media);
|
||||
|
||||
logger.info(
|
||||
`The ${is4k ? '4K' : 'non-4K'} season(s) [${seasonKeys}] [TMDB ID ${
|
||||
@@ -701,7 +644,7 @@ class AvailabilitySync {
|
||||
: mediaServerType === MediaServerType.JELLYFIN
|
||||
? 'jellyfin'
|
||||
: 'emby'
|
||||
} instance. Status will be changed to unknown.`,
|
||||
} instance. Status will be changed to deleted.`,
|
||||
{ label: 'AvailabilitySync' }
|
||||
);
|
||||
} catch (ex) {
|
||||
@@ -711,7 +654,7 @@ class AvailabilitySync {
|
||||
} season(s) [${seasonKeys}], TMDB ID ${media.tmdbId}.`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -725,7 +668,9 @@ class AvailabilitySync {
|
||||
|
||||
// Check for availability in all of the available radarr servers
|
||||
// If any find the media, we will assume the media exists
|
||||
for (const server of this.radarrServers) {
|
||||
for (const server of this.radarrServers.filter(
|
||||
(server) => server.is4k === is4k
|
||||
)) {
|
||||
const radarrAPI = new RadarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: RadarrAPI.buildUrl(server, '/api/v3'),
|
||||
@@ -734,13 +679,13 @@ class AvailabilitySync {
|
||||
try {
|
||||
let radarr: RadarrMovie | undefined;
|
||||
|
||||
if (!server.is4k && media.externalServiceId && !is4k) {
|
||||
if (media.externalServiceId && !is4k) {
|
||||
radarr = await radarrAPI.getMovie({
|
||||
id: media.externalServiceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (server.is4k && media.externalServiceId4k && is4k) {
|
||||
if (media.externalServiceId4k && is4k) {
|
||||
radarr = await radarrAPI.getMovie({
|
||||
id: media.externalServiceId4k,
|
||||
});
|
||||
@@ -762,7 +707,7 @@ class AvailabilitySync {
|
||||
}] from Radarr.`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -781,7 +726,9 @@ class AvailabilitySync {
|
||||
|
||||
// Check for availability in all of the available sonarr servers
|
||||
// If any find the media, we will assume the media exists
|
||||
for (const server of this.sonarrServers) {
|
||||
for (const server of this.sonarrServers.filter((server) => {
|
||||
return server.is4k === is4k;
|
||||
})) {
|
||||
const sonarrAPI = new SonarrAPI({
|
||||
apiKey: server.apiKey,
|
||||
url: SonarrAPI.buildUrl(server, '/api/v3'),
|
||||
@@ -790,13 +737,13 @@ class AvailabilitySync {
|
||||
try {
|
||||
let sonarr: SonarrSeries | undefined;
|
||||
|
||||
if (!server.is4k && media.externalServiceId && !is4k) {
|
||||
if (media.externalServiceId && !is4k) {
|
||||
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId);
|
||||
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] =
|
||||
sonarr.seasons;
|
||||
}
|
||||
|
||||
if (server.is4k && media.externalServiceId4k && is4k) {
|
||||
if (media.externalServiceId4k && is4k) {
|
||||
sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k);
|
||||
this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] =
|
||||
sonarr.seasons;
|
||||
@@ -815,7 +762,7 @@ class AvailabilitySync {
|
||||
}] from Sonarr.`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -861,7 +808,9 @@ class AvailabilitySync {
|
||||
// Check each sonarr instance to see if the media still exists
|
||||
// If found, we will assume the media exists and prevent removal
|
||||
// We can use the cache we built when we fetched the series with mediaExistsInSonarr
|
||||
for (const server of this.sonarrServers) {
|
||||
for (const server of this.sonarrServers.filter(
|
||||
(server) => server.is4k === is4k
|
||||
)) {
|
||||
let sonarrSeasons: SonarrSeason[] | undefined;
|
||||
|
||||
if (media.externalServiceId && !is4k) {
|
||||
@@ -936,7 +885,7 @@ class AvailabilitySync {
|
||||
} [TMDB ID ${media.tmdbId}] from Plex.`,
|
||||
{
|
||||
errorMessage: ex.message,
|
||||
label: 'AvailabilitySync',
|
||||
label: 'Availability Sync',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1125,4 +1074,5 @@ class AvailabilitySync {
|
||||
}
|
||||
|
||||
const availabilitySync = new AvailabilitySync();
|
||||
|
||||
export default availabilitySync;
|
||||
|
||||
@@ -68,7 +68,7 @@ class PushoverAgent
|
||||
logger.error('Error getting image payload', {
|
||||
label: 'Notifications',
|
||||
errorMessage: e.message,
|
||||
response: e?.response?.data,
|
||||
response: e.response?.data,
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ class WebPushAgent
|
||||
const allSubs = await userPushSubRepository
|
||||
.createQueryBuilder('pushSub')
|
||||
.leftJoinAndSelect('pushSub.user', 'user')
|
||||
.where('pushSub.userId IN (:users)', {
|
||||
.where('pushSub.userId IN (:...users)', {
|
||||
users: manageUsers.map((user) => user.id),
|
||||
})
|
||||
.getMany();
|
||||
|
||||
@@ -281,7 +281,9 @@ class BaseScanner<T> {
|
||||
? MediaStatus.AVAILABLE
|
||||
: season.episodes > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: !season.is4kOverride && season.processing
|
||||
: !season.is4kOverride &&
|
||||
season.processing &&
|
||||
existingSeason.status !== MediaStatus.DELETED
|
||||
? MediaStatus.PROCESSING
|
||||
: existingSeason.status;
|
||||
|
||||
@@ -294,7 +296,9 @@ class BaseScanner<T> {
|
||||
? MediaStatus.AVAILABLE
|
||||
: this.enable4kShow && season.episodes4k > 0
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: season.is4kOverride && season.processing
|
||||
: season.is4kOverride &&
|
||||
season.processing &&
|
||||
existingSeason.status4k !== MediaStatus.DELETED
|
||||
? MediaStatus.PROCESSING
|
||||
: existingSeason.status4k;
|
||||
} else {
|
||||
@@ -324,19 +328,25 @@ class BaseScanner<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// We want to skip specials when checking if a show is available
|
||||
const isAllStandardSeasons =
|
||||
seasons.length &&
|
||||
seasons.every(
|
||||
(season) =>
|
||||
season.episodes === season.totalEpisodes && season.episodes > 0
|
||||
);
|
||||
seasons
|
||||
.filter((season) => season.seasonNumber !== 0)
|
||||
.every(
|
||||
(season) =>
|
||||
season.episodes === season.totalEpisodes && season.episodes > 0
|
||||
);
|
||||
|
||||
const isAll4kSeasons =
|
||||
seasons.length &&
|
||||
seasons.every(
|
||||
(season) =>
|
||||
season.episodes4k === season.totalEpisodes && season.episodes4k > 0
|
||||
);
|
||||
seasons
|
||||
.filter((season) => season.seasonNumber !== 0)
|
||||
.every(
|
||||
(season) =>
|
||||
season.episodes4k === season.totalEpisodes &&
|
||||
season.episodes4k > 0
|
||||
);
|
||||
|
||||
if (media) {
|
||||
media.seasons = [...media.seasons, ...newSeasons];
|
||||
@@ -398,16 +408,23 @@ class BaseScanner<T> {
|
||||
}
|
||||
|
||||
// If the show is already available, and there are no new seasons, dont adjust
|
||||
// the status
|
||||
// the status. Skip specials when performing availability check
|
||||
const shouldStayAvailable =
|
||||
media.status === MediaStatus.AVAILABLE &&
|
||||
newSeasons.filter((season) => season.status !== MediaStatus.UNKNOWN)
|
||||
.length === 0;
|
||||
newSeasons.filter(
|
||||
(season) =>
|
||||
season.status !== MediaStatus.UNKNOWN &&
|
||||
season.status !== MediaStatus.DELETED &&
|
||||
season.seasonNumber !== 0
|
||||
).length === 0;
|
||||
const shouldStayAvailable4k =
|
||||
media.status4k === MediaStatus.AVAILABLE &&
|
||||
newSeasons.filter((season) => season.status4k !== MediaStatus.UNKNOWN)
|
||||
.length === 0;
|
||||
|
||||
newSeasons.filter(
|
||||
(season) =>
|
||||
season.status4k !== MediaStatus.UNKNOWN &&
|
||||
season.status4k !== MediaStatus.DELETED &&
|
||||
season.seasonNumber !== 0
|
||||
).length === 0;
|
||||
media.status =
|
||||
isAllStandardSeasons || shouldStayAvailable
|
||||
? MediaStatus.AVAILABLE
|
||||
@@ -417,11 +434,13 @@ class BaseScanner<T> {
|
||||
season.status === MediaStatus.AVAILABLE
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: !seasons.length ||
|
||||
: (!seasons.length && media.status !== MediaStatus.DELETED) ||
|
||||
media.seasons.some(
|
||||
(season) => season.status === MediaStatus.PROCESSING
|
||||
)
|
||||
? MediaStatus.PROCESSING
|
||||
: media.status === MediaStatus.DELETED
|
||||
? MediaStatus.DELETED
|
||||
: MediaStatus.UNKNOWN;
|
||||
media.status4k =
|
||||
(isAll4kSeasons || shouldStayAvailable4k) && this.enable4kShow
|
||||
@@ -433,11 +452,13 @@ class BaseScanner<T> {
|
||||
season.status4k === MediaStatus.AVAILABLE
|
||||
)
|
||||
? MediaStatus.PARTIALLY_AVAILABLE
|
||||
: !seasons.length ||
|
||||
: (!seasons.length && media.status4k !== MediaStatus.DELETED) ||
|
||||
media.seasons.some(
|
||||
(season) => season.status4k === MediaStatus.PROCESSING
|
||||
)
|
||||
? MediaStatus.PROCESSING
|
||||
: media.status4k === MediaStatus.DELETED
|
||||
? MediaStatus.DELETED
|
||||
: MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
this.log(`Updating existing title: ${title}`);
|
||||
|
||||
17
server/migration/postgres/1745492376568-UpdateWebPush.ts
Normal file
17
server/migration/postgres/1745492376568-UpdateWebPush.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateWebPush1745492376568 implements MigrationInterface {
|
||||
name = 'UpdateWebPush1745492376568';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" RENAME COLUMN "blacklistedtags" TO "blacklistedTags"`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" RENAME COLUMN "blacklistedTags" TO "blacklistedtags"`
|
||||
);
|
||||
}
|
||||
}
|
||||
79
server/migration/sqlite/1745492372230-UpdateWebPush.ts
Normal file
79
server/migration/sqlite/1745492372230-UpdateWebPush.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UpdateWebPush1745492372230 implements MigrationInterface {
|
||||
name = 'UpdateWebPush1745492372230';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
|
||||
);
|
||||
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, "blacklistedTags" varchar, CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"), CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "blacklist"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" RENAME TO "temporary_blacklist"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"), CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "temporary_blacklist"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_blacklist"`);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import TheMovieDb from '@server/api/themoviedb';
|
||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import Season from '@server/entity/Season';
|
||||
import { User } from '@server/entity/User';
|
||||
import type {
|
||||
MediaResultsResponse,
|
||||
@@ -101,6 +102,7 @@ mediaRoutes.post<
|
||||
isAuthenticated(Permission.MANAGE_REQUESTS),
|
||||
async (req, res, next) => {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const seasonRepository = getRepository(Season);
|
||||
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: Number(req.params.id) },
|
||||
@@ -115,11 +117,25 @@ mediaRoutes.post<
|
||||
switch (req.params.status) {
|
||||
case 'available':
|
||||
media[is4k ? 'status4k' : 'status'] = MediaStatus.AVAILABLE;
|
||||
|
||||
if (media.mediaType === MediaType.TV) {
|
||||
// Mark all seasons available
|
||||
media.seasons.forEach((season) => {
|
||||
const expectedSeasons = req.body.seasons ?? [];
|
||||
|
||||
for (const expectedSeason of expectedSeasons) {
|
||||
let season = media.seasons.find(
|
||||
(s) => s.seasonNumber === expectedSeason?.seasonNumber
|
||||
);
|
||||
|
||||
if (!season) {
|
||||
// Create the season if it doesn't exist
|
||||
season = seasonRepository.create({
|
||||
seasonNumber: expectedSeason?.seasonNumber,
|
||||
});
|
||||
media.seasons.push(season);
|
||||
}
|
||||
|
||||
season[is4k ? 'status4k' : 'status'] = MediaStatus.AVAILABLE;
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'partial':
|
||||
|
||||
@@ -44,7 +44,6 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
switch (req.query.filter) {
|
||||
case 'approved':
|
||||
case 'processing':
|
||||
case 'available':
|
||||
statusFilter = [MediaRequestStatus.APPROVED];
|
||||
break;
|
||||
case 'pending':
|
||||
@@ -59,12 +58,18 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
case 'failed':
|
||||
statusFilter = [MediaRequestStatus.FAILED];
|
||||
break;
|
||||
case 'completed':
|
||||
case 'available':
|
||||
case 'deleted':
|
||||
statusFilter = [MediaRequestStatus.COMPLETED];
|
||||
break;
|
||||
default:
|
||||
statusFilter = [
|
||||
MediaRequestStatus.PENDING,
|
||||
MediaRequestStatus.APPROVED,
|
||||
MediaRequestStatus.DECLINED,
|
||||
MediaRequestStatus.FAILED,
|
||||
MediaRequestStatus.COMPLETED,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -83,6 +88,9 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
MediaStatus.PARTIALLY_AVAILABLE,
|
||||
];
|
||||
break;
|
||||
case 'deleted':
|
||||
mediaStatusFilter = [MediaStatus.DELETED];
|
||||
break;
|
||||
default:
|
||||
mediaStatusFilter = [
|
||||
MediaStatus.UNKNOWN,
|
||||
@@ -90,6 +98,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
||||
MediaStatus.PROCESSING,
|
||||
MediaStatus.PARTIALLY_AVAILABLE,
|
||||
MediaStatus.AVAILABLE,
|
||||
MediaStatus.DELETED,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -298,7 +307,7 @@ requestRoutes.get('/count', async (_req, res, next) => {
|
||||
try {
|
||||
const query = requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoinAndSelect('request.media', 'media');
|
||||
.innerJoinAndSelect('request.media', 'media');
|
||||
|
||||
const totalCount = await query.getCount();
|
||||
|
||||
@@ -492,7 +501,8 @@ requestRoutes.put<{ requestId: string }>(
|
||||
(r) =>
|
||||
r.is4k === request.is4k &&
|
||||
r.id !== request.id &&
|
||||
r.status !== MediaRequestStatus.DECLINED
|
||||
r.status !== MediaRequestStatus.DECLINED &&
|
||||
r.status !== MediaRequestStatus.COMPLETED
|
||||
)
|
||||
.reduce((seasons, r) => {
|
||||
const combinedSeasons = r.seasons.map(
|
||||
|
||||
131
server/subscriber/MediaRequestSubscriber.ts
Normal file
131
server/subscriber/MediaRequestSubscriber.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from '@server/constants/media';
|
||||
import { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||
import logger from '@server/logger';
|
||||
import { truncate } from 'lodash';
|
||||
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
|
||||
import { EventSubscriber } from 'typeorm';
|
||||
|
||||
@EventSubscriber()
|
||||
export class MediaRequestSubscriber
|
||||
implements EntitySubscriberInterface<MediaRequest>
|
||||
{
|
||||
private async notifyAvailableMovie(entity: MediaRequest) {
|
||||
if (
|
||||
entity.media[entity.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.AVAILABLE
|
||||
) {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const movie = await tmdb.getMovie({
|
||||
movieId: entity.media.tmdbId,
|
||||
});
|
||||
|
||||
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||
event: `${entity.is4k ? '4K ' : ''}Movie Request Now Available`,
|
||||
notifyAdmin: false,
|
||||
notifySystem: true,
|
||||
notifyUser: entity.requestedBy,
|
||||
subject: `${movie.title}${
|
||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: truncate(movie.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
media: entity.media,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
request: entity,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong sending media notification(s)', {
|
||||
label: 'Notifications',
|
||||
errorMessage: e.message,
|
||||
mediaId: entity.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async notifyAvailableSeries(entity: MediaRequest) {
|
||||
// Find all seasons in the related media entity
|
||||
// and see if they are available, then we can check
|
||||
// if the request contains the same seasons
|
||||
const requestedSeasons =
|
||||
entity.seasons?.map((entitySeason) => entitySeason.seasonNumber) ?? [];
|
||||
const availableSeasons = entity.media.seasons.filter(
|
||||
(season) =>
|
||||
season[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE &&
|
||||
requestedSeasons.includes(season.seasonNumber)
|
||||
);
|
||||
const isMediaAvailable =
|
||||
availableSeasons.length > 0 &&
|
||||
availableSeasons.length === requestedSeasons.length;
|
||||
|
||||
if (isMediaAvailable) {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const tv = await tmdb.getTvShow({ tvId: entity.media.tmdbId });
|
||||
|
||||
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||
event: `${entity.is4k ? '4K ' : ''}Series Request Now Available`,
|
||||
subject: `${tv.name}${
|
||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: truncate(tv.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
notifyAdmin: false,
|
||||
notifySystem: true,
|
||||
notifyUser: entity.requestedBy,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||
media: entity.media,
|
||||
extra: [
|
||||
{
|
||||
name: 'Requested Seasons',
|
||||
value: entity.seasons
|
||||
.map((season) => season.seasonNumber)
|
||||
.join(', '),
|
||||
},
|
||||
],
|
||||
request: entity,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong sending media notification(s)', {
|
||||
label: 'Notifications',
|
||||
errorMessage: e.message,
|
||||
mediaId: entity.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public afterUpdate(event: UpdateEvent<MediaRequest>): void {
|
||||
if (!event.entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.entity.status === MediaRequestStatus.COMPLETED) {
|
||||
if (event.entity.media.mediaType === MediaType.MOVIE) {
|
||||
this.notifyAvailableMovie(event.entity as MediaRequest);
|
||||
}
|
||||
if (event.entity.media.mediaType === MediaType.TV) {
|
||||
this.notifyAvailableSeries(event.entity as MediaRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public listenTo(): typeof MediaRequest {
|
||||
return MediaRequest;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
@@ -8,172 +7,12 @@ import { getRepository } from '@server/datasource';
|
||||
import Media from '@server/entity/Media';
|
||||
import { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import Season from '@server/entity/Season';
|
||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||
import logger from '@server/logger';
|
||||
import { truncate } from 'lodash';
|
||||
import SeasonRequest from '@server/entity/SeasonRequest';
|
||||
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
|
||||
import { EventSubscriber, In, Not } from 'typeorm';
|
||||
import { EventSubscriber } from 'typeorm';
|
||||
|
||||
@EventSubscriber()
|
||||
export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
||||
private async notifyAvailableMovie(
|
||||
entity: Media,
|
||||
dbEntity: Media,
|
||||
is4k: boolean
|
||||
) {
|
||||
if (
|
||||
entity[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE &&
|
||||
dbEntity[is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE
|
||||
) {
|
||||
if (entity.mediaType === MediaType.MOVIE) {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const relatedRequests = await requestRepository.find({
|
||||
where: {
|
||||
media: {
|
||||
id: entity.id,
|
||||
},
|
||||
is4k,
|
||||
status: Not(MediaRequestStatus.DECLINED),
|
||||
},
|
||||
});
|
||||
|
||||
if (relatedRequests.length > 0) {
|
||||
const tmdb = new TheMovieDb();
|
||||
|
||||
try {
|
||||
const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
|
||||
|
||||
relatedRequests.forEach((request) => {
|
||||
notificationManager.sendNotification(
|
||||
Notification.MEDIA_AVAILABLE,
|
||||
{
|
||||
event: `${is4k ? '4K ' : ''}Movie Request Now Available`,
|
||||
notifyAdmin: false,
|
||||
notifySystem: true,
|
||||
notifyUser: request.requestedBy,
|
||||
subject: `${movie.title}${
|
||||
movie.release_date
|
||||
? ` (${movie.release_date.slice(0, 4)})`
|
||||
: ''
|
||||
}`,
|
||||
message: truncate(movie.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
media: entity,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
request,
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong sending media notification(s)', {
|
||||
label: 'Notifications',
|
||||
errorMessage: e.message,
|
||||
mediaId: entity.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async notifyAvailableSeries(
|
||||
entity: Media,
|
||||
dbEntity: Media,
|
||||
is4k: boolean
|
||||
) {
|
||||
const seasonRepository = getRepository(Season);
|
||||
const newAvailableSeasons = entity.seasons
|
||||
.filter(
|
||||
(season) =>
|
||||
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||
)
|
||||
.map((season) => season.seasonNumber);
|
||||
const oldSeasonIds = dbEntity.seasons.map((season) => season.id);
|
||||
const oldSeasons = await seasonRepository.findBy({ id: In(oldSeasonIds) });
|
||||
const oldAvailableSeasons = oldSeasons
|
||||
.filter(
|
||||
(season) =>
|
||||
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||
)
|
||||
.map((season) => season.seasonNumber);
|
||||
|
||||
const changedSeasons = newAvailableSeasons.filter(
|
||||
(seasonNumber) => !oldAvailableSeasons.includes(seasonNumber)
|
||||
);
|
||||
|
||||
if (changedSeasons.length > 0) {
|
||||
const tmdb = new TheMovieDb();
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const processedSeasons: number[] = [];
|
||||
|
||||
for (const changedSeasonNumber of changedSeasons) {
|
||||
const requests = await requestRepository.find({
|
||||
where: {
|
||||
media: {
|
||||
id: entity.id,
|
||||
},
|
||||
is4k,
|
||||
status: Not(MediaRequestStatus.DECLINED),
|
||||
},
|
||||
});
|
||||
const request = requests.find(
|
||||
(request) =>
|
||||
// Check if the season is complete AND it contains the current season that was just marked available
|
||||
request.seasons.every((season) =>
|
||||
newAvailableSeasons.includes(season.seasonNumber)
|
||||
) &&
|
||||
request.seasons.some(
|
||||
(season) => season.seasonNumber === changedSeasonNumber
|
||||
)
|
||||
);
|
||||
|
||||
if (request && !processedSeasons.includes(changedSeasonNumber)) {
|
||||
processedSeasons.push(
|
||||
...request.seasons.map((season) => season.seasonNumber)
|
||||
);
|
||||
|
||||
try {
|
||||
const tv = await tmdb.getTvShow({ tvId: entity.tmdbId });
|
||||
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
|
||||
event: `${is4k ? '4K ' : ''}Series Request Now Available`,
|
||||
subject: `${tv.name}${
|
||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
||||
}`,
|
||||
message: truncate(tv.overview, {
|
||||
length: 500,
|
||||
separator: /\s/,
|
||||
omission: '…',
|
||||
}),
|
||||
notifyAdmin: false,
|
||||
notifySystem: true,
|
||||
notifyUser: request.requestedBy,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
|
||||
media: entity,
|
||||
extra: [
|
||||
{
|
||||
name: 'Requested Seasons',
|
||||
value: request.seasons
|
||||
.map((season) => season.seasonNumber)
|
||||
.join(', '),
|
||||
},
|
||||
],
|
||||
request,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong sending media notification(s)', {
|
||||
label: 'Notifications',
|
||||
errorMessage: e.message,
|
||||
mediaId: entity.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateChildRequestStatus(event: Media, is4k: boolean) {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
@@ -192,57 +31,101 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
||||
}
|
||||
}
|
||||
|
||||
public beforeUpdate(event: UpdateEvent<Media>): void {
|
||||
private async updateRelatedMediaRequest(
|
||||
event: Media,
|
||||
databaseEvent: Media,
|
||||
is4k: boolean
|
||||
) {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||
|
||||
const relatedRequests = await requestRepository.find({
|
||||
relations: {
|
||||
media: true,
|
||||
},
|
||||
where: {
|
||||
media: { id: event.id },
|
||||
status: MediaRequestStatus.APPROVED,
|
||||
is4k,
|
||||
},
|
||||
});
|
||||
|
||||
// Check the media entity status and if available
|
||||
// or deleted, set the related request to completed
|
||||
if (relatedRequests.length > 0) {
|
||||
const completedRequests: MediaRequest[] = [];
|
||||
|
||||
for (const request of relatedRequests) {
|
||||
let shouldComplete = false;
|
||||
|
||||
if (
|
||||
(event[request.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.AVAILABLE ||
|
||||
event[request.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.DELETED) &&
|
||||
event.mediaType === MediaType.MOVIE
|
||||
) {
|
||||
shouldComplete = true;
|
||||
} else if (event.mediaType === 'tv') {
|
||||
const allSeasonResults = await Promise.all(
|
||||
request.seasons.map(async (requestSeason) => {
|
||||
const matchingSeason = event.seasons.find(
|
||||
(mediaSeason) =>
|
||||
mediaSeason.seasonNumber === requestSeason.seasonNumber
|
||||
);
|
||||
const matchingOldSeason = databaseEvent.seasons.find(
|
||||
(oldSeason) =>
|
||||
oldSeason.seasonNumber === requestSeason.seasonNumber
|
||||
);
|
||||
|
||||
if (!matchingSeason) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentSeasonStatus =
|
||||
matchingSeason[request.is4k ? 'status4k' : 'status'];
|
||||
const previousSeasonStatus =
|
||||
matchingOldSeason?.[request.is4k ? 'status4k' : 'status'];
|
||||
|
||||
const hasStatusChanged =
|
||||
currentSeasonStatus !== previousSeasonStatus;
|
||||
|
||||
const shouldUpdate =
|
||||
(hasStatusChanged ||
|
||||
requestSeason.status === MediaRequestStatus.COMPLETED) &&
|
||||
(currentSeasonStatus === MediaStatus.AVAILABLE ||
|
||||
currentSeasonStatus === MediaStatus.DELETED);
|
||||
|
||||
if (shouldUpdate) {
|
||||
requestSeason.status = MediaRequestStatus.COMPLETED;
|
||||
await seasonRequestRepository.save(requestSeason);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
const allSeasonsReady = allSeasonResults.every((result) => result);
|
||||
shouldComplete = allSeasonsReady;
|
||||
}
|
||||
|
||||
if (shouldComplete) {
|
||||
request.status = MediaRequestStatus.COMPLETED;
|
||||
completedRequests.push(request);
|
||||
}
|
||||
}
|
||||
|
||||
await requestRepository.save(completedRequests);
|
||||
}
|
||||
}
|
||||
|
||||
public async beforeUpdate(event: UpdateEvent<Media>): Promise<void> {
|
||||
if (!event.entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
event.entity.mediaType === MediaType.MOVIE &&
|
||||
event.entity.status === MediaStatus.AVAILABLE
|
||||
) {
|
||||
this.notifyAvailableMovie(
|
||||
event.entity as Media,
|
||||
event.databaseEntity,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
event.entity.mediaType === MediaType.MOVIE &&
|
||||
event.entity.status4k === MediaStatus.AVAILABLE
|
||||
) {
|
||||
this.notifyAvailableMovie(
|
||||
event.entity as Media,
|
||||
event.databaseEntity,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
event.entity.mediaType === MediaType.TV &&
|
||||
(event.entity.status === MediaStatus.AVAILABLE ||
|
||||
event.entity.status === MediaStatus.PARTIALLY_AVAILABLE)
|
||||
) {
|
||||
this.notifyAvailableSeries(
|
||||
event.entity as Media,
|
||||
event.databaseEntity,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
event.entity.mediaType === MediaType.TV &&
|
||||
(event.entity.status4k === MediaStatus.AVAILABLE ||
|
||||
event.entity.status4k === MediaStatus.PARTIALLY_AVAILABLE)
|
||||
) {
|
||||
this.notifyAvailableSeries(
|
||||
event.entity as Media,
|
||||
event.databaseEntity,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
event.entity.status === MediaStatus.AVAILABLE &&
|
||||
event.databaseEntity.status === MediaStatus.PENDING
|
||||
@@ -256,6 +139,65 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
|
||||
) {
|
||||
this.updateChildRequestStatus(event.entity as Media, true);
|
||||
}
|
||||
|
||||
// Manually load related seasons into databaseEntity
|
||||
// for seasonStatusCheck in afterUpdate
|
||||
const seasons = await event.manager
|
||||
.getRepository(Season)
|
||||
.createQueryBuilder('season')
|
||||
.leftJoin('season.media', 'media')
|
||||
.where('media.id = :id', { id: event.databaseEntity.id })
|
||||
.getMany();
|
||||
|
||||
event.databaseEntity.seasons = seasons;
|
||||
}
|
||||
|
||||
public async afterUpdate(event: UpdateEvent<Media>): Promise<void> {
|
||||
if (!event.entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validStatuses = [
|
||||
MediaStatus.PARTIALLY_AVAILABLE,
|
||||
MediaStatus.AVAILABLE,
|
||||
MediaStatus.DELETED,
|
||||
];
|
||||
|
||||
const seasonStatusCheck = (is4k: boolean) => {
|
||||
return event.entity?.seasons?.some((season: Season, index: number) => {
|
||||
const previousSeason = event.databaseEntity.seasons[index];
|
||||
|
||||
return (
|
||||
season[is4k ? 'status4k' : 'status'] !==
|
||||
previousSeason?.[is4k ? 'status4k' : 'status']
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
(event.entity.status !== event.databaseEntity?.status ||
|
||||
(event.entity.mediaType === MediaType.TV &&
|
||||
seasonStatusCheck(false))) &&
|
||||
validStatuses.includes(event.entity.status)
|
||||
) {
|
||||
this.updateRelatedMediaRequest(
|
||||
event.entity as Media,
|
||||
event.databaseEntity as Media,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(event.entity.status4k !== event.databaseEntity?.status4k ||
|
||||
(event.entity.mediaType === MediaType.TV && seasonStatusCheck(true))) &&
|
||||
validStatuses.includes(event.entity.status4k)
|
||||
) {
|
||||
this.updateRelatedMediaRequest(
|
||||
event.entity as Media,
|
||||
event.databaseEntity as Media,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public listenTo(): typeof Media {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ClockIcon,
|
||||
EyeSlashIcon,
|
||||
MinusSmallIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { MediaStatus } from '@server/constants/media';
|
||||
|
||||
@@ -59,6 +60,10 @@ const StatusBadgeMini = ({
|
||||
);
|
||||
indicatorIcon = <MinusSmallIcon />;
|
||||
break;
|
||||
case MediaStatus.DELETED:
|
||||
badgeStyle.push('bg-red-500 border-red-400 ring-red-400 text-red-100');
|
||||
indicatorIcon = <TrashIcon />;
|
||||
break;
|
||||
}
|
||||
|
||||
if (inProgress) {
|
||||
|
||||
@@ -152,6 +152,9 @@ const ManageSlideOver = ({
|
||||
if (data.mediaInfo) {
|
||||
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
|
||||
is4k,
|
||||
...(mediaType === 'tv' && {
|
||||
seasons: data.seasons.filter((season) => season.seasonNumber !== 0),
|
||||
}),
|
||||
});
|
||||
revalidate();
|
||||
}
|
||||
|
||||
@@ -206,6 +206,11 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
{intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
)}
|
||||
{request.status === MediaRequestStatus.COMPLETED && (
|
||||
<Badge badgeType="success">
|
||||
{intl.formatMessage(globalMessages.completed)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center text-sm leading-5 sm:mt-0">
|
||||
|
||||
@@ -268,7 +268,9 @@ const RequestButton = ({
|
||||
|
||||
// Standard request button
|
||||
if (
|
||||
(!media || media.status === MediaStatus.UNKNOWN) &&
|
||||
(!media ||
|
||||
media.status === MediaStatus.UNKNOWN ||
|
||||
(media.status === MediaStatus.DELETED && !activeRequest)) &&
|
||||
hasPermission(
|
||||
[
|
||||
Permission.REQUEST,
|
||||
@@ -295,7 +297,6 @@ const RequestButton = ({
|
||||
type: 'or',
|
||||
}) &&
|
||||
media &&
|
||||
media.status !== MediaStatus.AVAILABLE &&
|
||||
media.status !== MediaStatus.BLACKLISTED &&
|
||||
!isShowComplete
|
||||
) {
|
||||
@@ -312,7 +313,9 @@ const RequestButton = ({
|
||||
|
||||
// 4K request button
|
||||
if (
|
||||
(!media || media.status4k === MediaStatus.UNKNOWN) &&
|
||||
(!media ||
|
||||
media.status4k === MediaStatus.UNKNOWN ||
|
||||
(media.status4k === MediaStatus.DELETED && !active4kRequest)) &&
|
||||
hasPermission(
|
||||
[
|
||||
Permission.REQUEST_4K,
|
||||
@@ -341,8 +344,7 @@ const RequestButton = ({
|
||||
type: 'or',
|
||||
}) &&
|
||||
media &&
|
||||
media.status4k !== MediaStatus.AVAILABLE &&
|
||||
media.status !== MediaStatus.BLACKLISTED &&
|
||||
media.status4k !== MediaStatus.BLACKLISTED &&
|
||||
!is4kShowComplete &&
|
||||
settings.currentSettings.series4kEnabled
|
||||
) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { MediaRequestStatus } from '@server/constants/media';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
@@ -440,6 +440,15 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
>
|
||||
{intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
) : requestData.status === MediaRequestStatus.PENDING &&
|
||||
requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.DELETED ? (
|
||||
<Badge
|
||||
badgeType="warning"
|
||||
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||
>
|
||||
{intl.formatMessage(globalMessages.pending)}
|
||||
</Badge>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
TrashIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { MediaRequestStatus } from '@server/constants/media';
|
||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
||||
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
||||
@@ -509,6 +509,15 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
>
|
||||
{intl.formatMessage(globalMessages.failed)}
|
||||
</Badge>
|
||||
) : requestData.status === MediaRequestStatus.PENDING &&
|
||||
requestData.media[requestData.is4k ? 'status4k' : 'status'] ===
|
||||
MediaStatus.DELETED ? (
|
||||
<Badge
|
||||
badgeType="warning"
|
||||
href={`/${requestData.type}/${requestData.media.tmdbId}?manage=1`}
|
||||
>
|
||||
{intl.formatMessage(globalMessages.pending)}
|
||||
</Badge>
|
||||
) : (
|
||||
<StatusBadge
|
||||
status={
|
||||
|
||||
@@ -39,6 +39,8 @@ enum Filter {
|
||||
AVAILABLE = 'available',
|
||||
UNAVAILABLE = 'unavailable',
|
||||
FAILED = 'failed',
|
||||
DELETED = 'deleted',
|
||||
COMPLETED = 'completed',
|
||||
}
|
||||
|
||||
type Sort = 'added' | 'modified';
|
||||
@@ -178,6 +180,9 @@ const RequestList = () => {
|
||||
<option value="approved">
|
||||
{intl.formatMessage(globalMessages.approved)}
|
||||
</option>
|
||||
<option value="completed">
|
||||
{intl.formatMessage(globalMessages.completed)}
|
||||
</option>
|
||||
<option value="processing">
|
||||
{intl.formatMessage(globalMessages.processing)}
|
||||
</option>
|
||||
@@ -190,6 +195,9 @@ const RequestList = () => {
|
||||
<option value="unavailable">
|
||||
{intl.formatMessage(globalMessages.unavailable)}
|
||||
</option>
|
||||
<option value="deleted">
|
||||
{intl.formatMessage(globalMessages.deleted)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-2 flex flex-grow sm:mb-0 lg:flex-grow-0">
|
||||
|
||||
@@ -81,7 +81,8 @@ const CollectionRequestModal = ({
|
||||
.filter(
|
||||
(request) =>
|
||||
request.is4k === is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
request.status !== MediaRequestStatus.DECLINED &&
|
||||
request.status !== MediaRequestStatus.COMPLETED
|
||||
)
|
||||
.map((part) => part.id),
|
||||
];
|
||||
@@ -170,7 +171,9 @@ const CollectionRequestModal = ({
|
||||
|
||||
return (part?.mediaInfo?.requests ?? []).find(
|
||||
(request) =>
|
||||
request.is4k === is4k && request.status !== MediaRequestStatus.DECLINED
|
||||
request.is4k === is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED &&
|
||||
request.status !== MediaRequestStatus.COMPLETED
|
||||
);
|
||||
};
|
||||
|
||||
@@ -368,7 +371,9 @@ const CollectionRequestModal = ({
|
||||
const partMedia =
|
||||
part.mediaInfo &&
|
||||
part.mediaInfo[is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.UNKNOWN
|
||||
MediaStatus.UNKNOWN &&
|
||||
part.mediaInfo[is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.DELETED
|
||||
? part.mediaInfo
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -249,7 +249,8 @@ const TvRequestModal = ({
|
||||
.filter(
|
||||
(request) =>
|
||||
request.is4k === is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
request.status !== MediaRequestStatus.DECLINED &&
|
||||
request.status !== MediaRequestStatus.COMPLETED
|
||||
)
|
||||
.reduce((requestedSeasons, request) => {
|
||||
return [
|
||||
@@ -314,12 +315,16 @@ const TvRequestModal = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const standardUnrequestedSeasons = unrequestedSeasons.filter(
|
||||
(seasonNumber) => seasonNumber !== 0
|
||||
);
|
||||
|
||||
if (
|
||||
data &&
|
||||
selectedSeasons.length >= 0 &&
|
||||
selectedSeasons.length < unrequestedSeasons.length
|
||||
selectedSeasons.length < standardUnrequestedSeasons.length
|
||||
) {
|
||||
setSelectedSeasons(unrequestedSeasons);
|
||||
setSelectedSeasons(standardUnrequestedSeasons);
|
||||
} else {
|
||||
setSelectedSeasons([]);
|
||||
}
|
||||
@@ -330,9 +335,9 @@ const TvRequestModal = ({
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
selectedSeasons.length ===
|
||||
selectedSeasons.filter((season) => season !== 0).length ===
|
||||
getAllSeasons().filter(
|
||||
(season) => !getAllRequestedSeasons().includes(season)
|
||||
(season) => !getAllRequestedSeasons().includes(season) && season !== 0
|
||||
).length
|
||||
);
|
||||
};
|
||||
@@ -347,7 +352,8 @@ const TvRequestModal = ({
|
||||
(data.mediaInfo.requests || []).filter(
|
||||
(request) =>
|
||||
request.is4k === is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
request.status !== MediaRequestStatus.DECLINED &&
|
||||
request.status !== MediaRequestStatus.COMPLETED
|
||||
).length > 0
|
||||
) {
|
||||
data.mediaInfo.requests
|
||||
@@ -355,7 +361,9 @@ const TvRequestModal = ({
|
||||
.forEach((request) => {
|
||||
if (!seasonRequest) {
|
||||
seasonRequest = request.seasons.find(
|
||||
(season) => season.seasonNumber === seasonNumber
|
||||
(season) =>
|
||||
season.seasonNumber === seasonNumber &&
|
||||
season.status !== MediaRequestStatus.COMPLETED
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -577,7 +585,9 @@ const TvRequestModal = ({
|
||||
(sn) =>
|
||||
sn.seasonNumber === season.seasonNumber &&
|
||||
sn[is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.UNKNOWN
|
||||
MediaStatus.UNKNOWN &&
|
||||
sn[is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.DELETED
|
||||
);
|
||||
return (
|
||||
<tr key={`season-${season.id}`}>
|
||||
|
||||
@@ -371,6 +371,17 @@ const StatusBadge = ({
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
case MediaStatus.DELETED:
|
||||
return (
|
||||
<Tooltip content={mediaLinkDescription}>
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(is4k ? messages.status4k : messages.status, {
|
||||
status: intl.formatMessage(globalMessages.deleted),
|
||||
})}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -467,7 +467,9 @@ const TitleCard = ({
|
||||
<div
|
||||
className={`px-2 text-white ${
|
||||
!showRequestButton ||
|
||||
(currentStatus && currentStatus !== MediaStatus.UNKNOWN)
|
||||
(currentStatus &&
|
||||
currentStatus !== MediaStatus.UNKNOWN &&
|
||||
currentStatus !== MediaStatus.DELETED)
|
||||
? 'pb-2'
|
||||
: 'pb-11'
|
||||
}`}
|
||||
@@ -493,7 +495,8 @@ const TitleCard = ({
|
||||
WebkitLineClamp:
|
||||
!showRequestButton ||
|
||||
(currentStatus &&
|
||||
currentStatus !== MediaStatus.UNKNOWN)
|
||||
currentStatus !== MediaStatus.UNKNOWN &&
|
||||
currentStatus !== MediaStatus.DELETED)
|
||||
? 5
|
||||
: 3,
|
||||
display: '-webkit-box',
|
||||
@@ -510,7 +513,9 @@ const TitleCard = ({
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2 py-2">
|
||||
{showRequestButton &&
|
||||
(!currentStatus || currentStatus === MediaStatus.UNKNOWN) && (
|
||||
(!currentStatus ||
|
||||
currentStatus === MediaStatus.UNKNOWN ||
|
||||
currentStatus === MediaStatus.DELETED) && (
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
|
||||
@@ -276,7 +276,8 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
.filter(
|
||||
(request) =>
|
||||
request.is4k === is4k &&
|
||||
request.status !== MediaRequestStatus.DECLINED
|
||||
request.status !== MediaRequestStatus.DECLINED &&
|
||||
request.status !== MediaRequestStatus.COMPLETED
|
||||
)
|
||||
.reduce((requestedSeasons, request) => {
|
||||
return [
|
||||
@@ -809,18 +810,30 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
season.seasonNumber === s.seasonNumber &&
|
||||
s.status4k !== MediaStatus.UNKNOWN
|
||||
);
|
||||
const request = (data.mediaInfo?.requests ?? []).find(
|
||||
(r) =>
|
||||
!!r.seasons.find(
|
||||
(s) => s.seasonNumber === season.seasonNumber
|
||||
) && !r.is4k
|
||||
);
|
||||
const request4k = (data.mediaInfo?.requests ?? []).find(
|
||||
(r) =>
|
||||
!!r.seasons.find(
|
||||
(s) => s.seasonNumber === season.seasonNumber
|
||||
) && r.is4k
|
||||
);
|
||||
const request = (data.mediaInfo?.requests ?? [])
|
||||
.filter(
|
||||
(r) =>
|
||||
!!r.seasons.find(
|
||||
(s) => s.seasonNumber === season.seasonNumber
|
||||
) && !r.is4k
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() -
|
||||
new Date(a.createdAt).getTime()
|
||||
)[0];
|
||||
const request4k = (data.mediaInfo?.requests ?? [])
|
||||
.filter(
|
||||
(r) =>
|
||||
!!r.seasons.find(
|
||||
(s) => s.seasonNumber === season.seasonNumber
|
||||
) && r.is4k
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() -
|
||||
new Date(a.createdAt).getTime()
|
||||
)[0];
|
||||
|
||||
if (season.episodeCount === 0) {
|
||||
return null;
|
||||
@@ -853,7 +866,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
</div>
|
||||
{((!mSeason &&
|
||||
request?.status === MediaRequestStatus.APPROVED) ||
|
||||
mSeason?.status === MediaStatus.PROCESSING) && (
|
||||
mSeason?.status === MediaStatus.PROCESSING ||
|
||||
(request?.status === MediaRequestStatus.APPROVED &&
|
||||
mSeason?.status === MediaStatus.DELETED)) && (
|
||||
<>
|
||||
<div className="hidden md:flex">
|
||||
<Badge badgeType="primary">
|
||||
@@ -912,10 +927,28 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{mSeason?.status === MediaStatus.DELETED &&
|
||||
request?.status !== MediaRequestStatus.APPROVED && (
|
||||
<>
|
||||
<div className="hidden md:flex">
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(globalMessages.deleted)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex md:hidden">
|
||||
<StatusBadgeMini
|
||||
status={MediaStatus.DELETED}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{((!mSeason4k &&
|
||||
request4k?.status ===
|
||||
MediaRequestStatus.APPROVED) ||
|
||||
mSeason4k?.status4k === MediaStatus.PROCESSING) &&
|
||||
mSeason4k?.status4k === MediaStatus.PROCESSING ||
|
||||
(request4k?.status ===
|
||||
MediaRequestStatus.APPROVED &&
|
||||
mSeason4k?.status4k === MediaStatus.DELETED)) &&
|
||||
show4k && (
|
||||
<>
|
||||
<div className="hidden md:flex">
|
||||
@@ -998,6 +1031,27 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{mSeason4k?.status4k === MediaStatus.DELETED &&
|
||||
request4k?.status !== MediaRequestStatus.APPROVED &&
|
||||
show4k && (
|
||||
<>
|
||||
<div className="hidden md:flex">
|
||||
<Badge badgeType="danger">
|
||||
{intl.formatMessage(messages.status4k, {
|
||||
status: intl.formatMessage(
|
||||
globalMessages.deleted
|
||||
),
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex md:hidden">
|
||||
<StatusBadgeMini
|
||||
status={MediaStatus.DELETED}
|
||||
is4k={true}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
className={`${
|
||||
open ? 'rotate-180' : ''
|
||||
|
||||
@@ -3,6 +3,7 @@ import defineMessages from '@app/utils/defineMessages';
|
||||
const globalMessages = defineMessages('i18n', {
|
||||
available: 'Available',
|
||||
partiallyavailable: 'Partially Available',
|
||||
deleted: 'Deleted',
|
||||
processing: 'Processing',
|
||||
unavailable: 'Unavailable',
|
||||
notrequested: 'Not Requested',
|
||||
@@ -14,6 +15,7 @@ const globalMessages = defineMessages('i18n', {
|
||||
pending: 'Pending',
|
||||
declined: 'Declined',
|
||||
approved: 'Approved',
|
||||
completed: 'Completed',
|
||||
movie: 'Movie',
|
||||
movies: 'Movies',
|
||||
collection: 'Collection',
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"components.Discover.StudioSlider.studios": "Studios",
|
||||
"components.Discover.TvGenreList.seriesgenres": "Series Genres",
|
||||
"components.Discover.TvGenreSlider.tvgenres": "Series Genres",
|
||||
"components.DiscoverTvUpcoming.upcomingtv": "Upcoming Series",
|
||||
"components.Discover.createnewslider": "Create New Slider",
|
||||
"components.Discover.customizediscover": "Customize Discover",
|
||||
"components.Discover.discover": "Discover",
|
||||
@@ -136,7 +137,6 @@
|
||||
"components.Discover.upcomingtv": "Upcoming Series",
|
||||
"components.Discover.updatefailed": "Something went wrong updating the discover customization settings.",
|
||||
"components.Discover.updatesuccess": "Updated discover customization settings.",
|
||||
"components.DiscoverTvUpcoming.upcomingtv": "Upcoming Series",
|
||||
"components.DownloadBlock.estimatedtime": "Estimated {time}",
|
||||
"components.DownloadBlock.formattedTitle": "{title}: Season {seasonNumber} Episode {episodeNumber}",
|
||||
"components.IssueDetails.IssueComment.areyousuredelete": "Are you sure you want to delete this comment?",
|
||||
@@ -948,7 +948,6 @@
|
||||
"components.Settings.SettingsMain.validationApplicationTitle": "You must provide an application title",
|
||||
"components.Settings.SettingsMain.validationApplicationUrl": "You must provide a valid URL",
|
||||
"components.Settings.SettingsMain.validationApplicationUrlTrailingSlash": "URL must not end in a trailing slash",
|
||||
"components.Settings.SettingsNetwork.advancedNetworkSettings": "Advanced Network Settings",
|
||||
"components.Settings.SettingsNetwork.csrfProtection": "Enable CSRF Protection",
|
||||
"components.Settings.SettingsNetwork.csrfProtectionHoverTip": "Do NOT enable this setting unless you understand what you are doing!",
|
||||
"components.Settings.SettingsNetwork.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
|
||||
@@ -1184,7 +1183,7 @@
|
||||
"components.Setup.librarieserror": "Validation failed. Please toggle the libraries again to continue.",
|
||||
"components.Setup.servertype": "Choose Server Type",
|
||||
"components.Setup.setup": "Setup",
|
||||
"components.Setup.signin": "Sign in to your account",
|
||||
"components.Setup.signin": "Sign In",
|
||||
"components.Setup.signinMessage": "Get started by signing in",
|
||||
"components.Setup.signinWithEmby": "Enter your Emby details",
|
||||
"components.Setup.signinWithJellyfin": "Enter your Jellyfin details",
|
||||
@@ -1477,9 +1476,11 @@
|
||||
"i18n.canceling": "Canceling…",
|
||||
"i18n.close": "Close",
|
||||
"i18n.collection": "Collection",
|
||||
"i18n.completed": "Completed",
|
||||
"i18n.decline": "Decline",
|
||||
"i18n.declined": "Declined",
|
||||
"i18n.delete": "Delete",
|
||||
"i18n.deleted": "Deleted",
|
||||
"i18n.deleting": "Deleting…",
|
||||
"i18n.delimitedlist": "{a}, {b}",
|
||||
"i18n.edit": "Edit",
|
||||
|
||||
@@ -144,18 +144,32 @@ const CoreApp: Omit<NextAppComponentType, 'origGetInitialProps'> = ({
|
||||
clearAppBadge?: () => Promise<void>;
|
||||
};
|
||||
|
||||
if ('setAppBadge' in navigator) {
|
||||
if (
|
||||
!router.pathname.match(/(login|setup|resetpassword)/) &&
|
||||
hasPermission(Permission.ADMIN)
|
||||
) {
|
||||
requestsCount().then((data) =>
|
||||
newNavigator?.setAppBadge?.(data.pending)
|
||||
);
|
||||
} else {
|
||||
newNavigator?.clearAppBadge?.();
|
||||
const handleBadgeUpdate = () => {
|
||||
if ('setAppBadge' in newNavigator) {
|
||||
if (
|
||||
!router.pathname.match(/(login|setup|resetpassword)/) &&
|
||||
hasPermission(Permission.ADMIN)
|
||||
) {
|
||||
requestsCount().then((data) => {
|
||||
if (data.pending > 0) {
|
||||
newNavigator.setAppBadge?.(data.pending);
|
||||
} else {
|
||||
newNavigator.clearAppBadge?.();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
newNavigator.clearAppBadge?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleBadgeUpdate();
|
||||
|
||||
window.addEventListener('focus', handleBadgeUpdate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleBadgeUpdate);
|
||||
};
|
||||
}, [hasPermission, router.pathname]);
|
||||
|
||||
if (router.pathname.match(/(login|setup|resetpassword)/)) {
|
||||
|
||||
Reference in New Issue
Block a user