mirror of
https://github.com/sct/overseerr.git
synced 2025-12-28 00:54:03 +01:00
* 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) --------- 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: 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>
886 lines
24 KiB
TypeScript
886 lines
24 KiB
TypeScript
import PlexTvAPI from '@server/api/plextv';
|
|
import type { SortOptions } from '@server/api/themoviedb';
|
|
import TheMovieDb from '@server/api/themoviedb';
|
|
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
|
import { MediaType } from '@server/constants/media';
|
|
import { getRepository } from '@server/datasource';
|
|
import Media from '@server/entity/Media';
|
|
import { User } from '@server/entity/User';
|
|
import { Watchlist } from '@server/entity/Watchlist';
|
|
import type {
|
|
GenreSliderItem,
|
|
WatchlistResponse,
|
|
} from '@server/interfaces/api/discoverInterfaces';
|
|
import { getSettings } from '@server/lib/settings';
|
|
import logger from '@server/logger';
|
|
import { mapProductionCompany } from '@server/models/Movie';
|
|
import {
|
|
mapCollectionResult,
|
|
mapMovieResult,
|
|
mapPersonResult,
|
|
mapTvResult,
|
|
} from '@server/models/Search';
|
|
import { mapNetwork } from '@server/models/Tv';
|
|
import { isCollection, isMovie, isPerson } from '@server/utils/typeHelpers';
|
|
import { Router } from 'express';
|
|
import { sortBy } from 'lodash';
|
|
import { z } from 'zod';
|
|
|
|
export const createTmdbWithRegionLanguage = (user?: User): TheMovieDb => {
|
|
const settings = getSettings();
|
|
|
|
const region =
|
|
user?.settings?.region === 'all'
|
|
? ''
|
|
: user?.settings?.region
|
|
? user?.settings?.region
|
|
: settings.main.region;
|
|
|
|
const originalLanguage =
|
|
user?.settings?.originalLanguage === 'all'
|
|
? ''
|
|
: user?.settings?.originalLanguage
|
|
? user?.settings?.originalLanguage
|
|
: settings.main.originalLanguage;
|
|
|
|
return new TheMovieDb({
|
|
region,
|
|
originalLanguage,
|
|
});
|
|
};
|
|
|
|
const discoverRoutes = Router();
|
|
|
|
const QueryFilterOptions = z.object({
|
|
page: z.coerce.string().optional(),
|
|
sortBy: z.coerce.string().optional(),
|
|
primaryReleaseDateGte: z.coerce.string().optional(),
|
|
primaryReleaseDateLte: z.coerce.string().optional(),
|
|
firstAirDateGte: z.coerce.string().optional(),
|
|
firstAirDateLte: z.coerce.string().optional(),
|
|
studio: z.coerce.string().optional(),
|
|
genre: z.coerce.string().optional(),
|
|
keywords: z.coerce.string().optional(),
|
|
language: z.coerce.string().optional(),
|
|
withRuntimeGte: z.coerce.string().optional(),
|
|
withRuntimeLte: z.coerce.string().optional(),
|
|
voteAverageGte: z.coerce.string().optional(),
|
|
voteAverageLte: z.coerce.string().optional(),
|
|
voteCountGte: z.coerce.string().optional(),
|
|
voteCountLte: z.coerce.string().optional(),
|
|
network: z.coerce.string().optional(),
|
|
watchProviders: z.coerce.string().optional(),
|
|
watchRegion: z.coerce.string().optional(),
|
|
});
|
|
|
|
export type FilterOptions = z.infer<typeof QueryFilterOptions>;
|
|
|
|
discoverRoutes.get('/movies', async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
try {
|
|
const query = QueryFilterOptions.parse(req.query);
|
|
const keywords = query.keywords;
|
|
const data = await tmdb.getDiscoverMovies({
|
|
page: Number(query.page),
|
|
sortBy: query.sortBy as SortOptions,
|
|
language: req.locale ?? query.language,
|
|
originalLanguage: query.language,
|
|
genre: query.genre,
|
|
studio: query.studio,
|
|
primaryReleaseDateLte: query.primaryReleaseDateLte
|
|
? new Date(query.primaryReleaseDateLte).toISOString().split('T')[0]
|
|
: undefined,
|
|
primaryReleaseDateGte: query.primaryReleaseDateGte
|
|
? new Date(query.primaryReleaseDateGte).toISOString().split('T')[0]
|
|
: undefined,
|
|
keywords,
|
|
withRuntimeGte: query.withRuntimeGte,
|
|
withRuntimeLte: query.withRuntimeLte,
|
|
voteAverageGte: query.voteAverageGte,
|
|
voteAverageLte: query.voteAverageLte,
|
|
voteCountGte: query.voteCountGte,
|
|
voteCountLte: query.voteCountLte,
|
|
watchProviders: query.watchProviders,
|
|
watchRegion: query.watchRegion,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
let keywordData: TmdbKeyword[] = [];
|
|
if (keywords) {
|
|
const splitKeywords = keywords.split(',');
|
|
|
|
keywordData = await Promise.all(
|
|
splitKeywords.map(async (keywordId) => {
|
|
return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) });
|
|
})
|
|
);
|
|
}
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
keywords: keywordData,
|
|
results: data.results.map((result) =>
|
|
mapMovieResult(
|
|
result,
|
|
media.find(
|
|
(req) =>
|
|
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving popular movies', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve popular movies.',
|
|
});
|
|
}
|
|
});
|
|
|
|
discoverRoutes.get<{ language: string }>(
|
|
'/movies/language/:language',
|
|
async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
try {
|
|
const languages = await tmdb.getLanguages();
|
|
|
|
const language = languages.find(
|
|
(lang) => lang.iso_639_1 === req.params.language
|
|
);
|
|
|
|
if (!language) {
|
|
return next({ status: 404, message: 'Language not found.' });
|
|
}
|
|
|
|
const data = await tmdb.getDiscoverMovies({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
originalLanguage: req.params.language,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
language,
|
|
results: data.results.map((result) =>
|
|
mapMovieResult(
|
|
result,
|
|
media.find(
|
|
(req) =>
|
|
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving movies by language', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
language: req.params.language,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve movies by language.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get<{ genreId: string }>(
|
|
'/movies/genre/:genreId',
|
|
async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
try {
|
|
const genres = await tmdb.getMovieGenres({
|
|
language: (req.query.language as string) ?? req.locale,
|
|
});
|
|
|
|
const genre = genres.find(
|
|
(genre) => genre.id === Number(req.params.genreId)
|
|
);
|
|
|
|
if (!genre) {
|
|
return next({ status: 404, message: 'Genre not found.' });
|
|
}
|
|
|
|
const data = await tmdb.getDiscoverMovies({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
genre: req.params.genreId as string,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
genre,
|
|
results: data.results.map((result) =>
|
|
mapMovieResult(
|
|
result,
|
|
media.find(
|
|
(req) =>
|
|
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving movies by genre', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
genreId: req.params.genreId,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve movies by genre.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get<{ studioId: string }>(
|
|
'/movies/studio/:studioId',
|
|
async (req, res, next) => {
|
|
const tmdb = new TheMovieDb();
|
|
|
|
try {
|
|
const studio = await tmdb.getStudio(Number(req.params.studioId));
|
|
|
|
const data = await tmdb.getDiscoverMovies({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
studio: req.params.studioId as string,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
studio: mapProductionCompany(studio),
|
|
results: data.results.map((result) =>
|
|
mapMovieResult(
|
|
result,
|
|
media.find(
|
|
(med) =>
|
|
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving movies by studio', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
studioId: req.params.studioId,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve movies by studio.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get('/movies/upcoming', async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
const now = new Date();
|
|
const offset = now.getTimezoneOffset();
|
|
const date = new Date(now.getTime() - offset * 60 * 1000)
|
|
.toISOString()
|
|
.split('T')[0];
|
|
|
|
try {
|
|
const data = await tmdb.getDiscoverMovies({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
primaryReleaseDateGte: date,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
results: data.results.map((result) =>
|
|
mapMovieResult(
|
|
result,
|
|
media.find(
|
|
(med) =>
|
|
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving upcoming movies', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve upcoming movies.',
|
|
});
|
|
}
|
|
});
|
|
|
|
discoverRoutes.get('/tv', async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
try {
|
|
const query = QueryFilterOptions.parse(req.query);
|
|
const keywords = query.keywords;
|
|
const data = await tmdb.getDiscoverTv({
|
|
page: Number(query.page),
|
|
sortBy: query.sortBy as SortOptions,
|
|
language: req.locale ?? query.language,
|
|
genre: query.genre,
|
|
network: query.network ? Number(query.network) : undefined,
|
|
firstAirDateLte: query.firstAirDateLte
|
|
? new Date(query.firstAirDateLte).toISOString().split('T')[0]
|
|
: undefined,
|
|
firstAirDateGte: query.firstAirDateGte
|
|
? new Date(query.firstAirDateGte).toISOString().split('T')[0]
|
|
: undefined,
|
|
originalLanguage: query.language,
|
|
keywords,
|
|
withRuntimeGte: query.withRuntimeGte,
|
|
withRuntimeLte: query.withRuntimeLte,
|
|
voteAverageGte: query.voteAverageGte,
|
|
voteAverageLte: query.voteAverageLte,
|
|
voteCountGte: query.voteCountGte,
|
|
voteCountLte: query.voteCountLte,
|
|
watchProviders: query.watchProviders,
|
|
watchRegion: query.watchRegion,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
let keywordData: TmdbKeyword[] = [];
|
|
if (keywords) {
|
|
const splitKeywords = keywords.split(',');
|
|
|
|
keywordData = await Promise.all(
|
|
splitKeywords.map(async (keywordId) => {
|
|
return await tmdb.getKeywordDetails({ keywordId: Number(keywordId) });
|
|
})
|
|
);
|
|
}
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
keywords: keywordData,
|
|
results: data.results.map((result) =>
|
|
mapTvResult(
|
|
result,
|
|
media.find(
|
|
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving popular series', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve popular series.',
|
|
});
|
|
}
|
|
});
|
|
|
|
discoverRoutes.get<{ language: string }>(
|
|
'/tv/language/:language',
|
|
async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
try {
|
|
const languages = await tmdb.getLanguages();
|
|
|
|
const language = languages.find(
|
|
(lang) => lang.iso_639_1 === req.params.language
|
|
);
|
|
|
|
if (!language) {
|
|
return next({ status: 404, message: 'Language not found.' });
|
|
}
|
|
|
|
const data = await tmdb.getDiscoverTv({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
originalLanguage: req.params.language,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
language,
|
|
results: data.results.map((result) =>
|
|
mapTvResult(
|
|
result,
|
|
media.find(
|
|
(med) =>
|
|
med.tmdbId === result.id && med.mediaType === MediaType.TV
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving series by language', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
language: req.params.language,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve series by language.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get<{ genreId: string }>(
|
|
'/tv/genre/:genreId',
|
|
async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
try {
|
|
const genres = await tmdb.getTvGenres({
|
|
language: (req.query.language as string) ?? req.locale,
|
|
});
|
|
|
|
const genre = genres.find(
|
|
(genre) => genre.id === Number(req.params.genreId)
|
|
);
|
|
|
|
if (!genre) {
|
|
return next({ status: 404, message: 'Genre not found.' });
|
|
}
|
|
|
|
const data = await tmdb.getDiscoverTv({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
genre: req.params.genreId,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
genre,
|
|
results: data.results.map((result) =>
|
|
mapTvResult(
|
|
result,
|
|
media.find(
|
|
(med) =>
|
|
med.tmdbId === result.id && med.mediaType === MediaType.TV
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving series by genre', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
genreId: req.params.genreId,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve series by genre.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get<{ networkId: string }>(
|
|
'/tv/network/:networkId',
|
|
async (req, res, next) => {
|
|
const tmdb = new TheMovieDb();
|
|
|
|
try {
|
|
const network = await tmdb.getNetwork(Number(req.params.networkId));
|
|
|
|
const data = await tmdb.getDiscoverTv({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
network: Number(req.params.networkId),
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
network: mapNetwork(network),
|
|
results: data.results.map((result) =>
|
|
mapTvResult(
|
|
result,
|
|
media.find(
|
|
(med) =>
|
|
med.tmdbId === result.id && med.mediaType === MediaType.TV
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving series by network', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
networkId: req.params.networkId,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve series by network.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get('/tv/upcoming', async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
const now = new Date();
|
|
const offset = now.getTimezoneOffset();
|
|
const date = new Date(now.getTime() - offset * 60 * 1000)
|
|
.toISOString()
|
|
.split('T')[0];
|
|
|
|
try {
|
|
const data = await tmdb.getDiscoverTv({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
firstAirDateGte: date,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
results: data.results.map((result) =>
|
|
mapTvResult(
|
|
result,
|
|
media.find(
|
|
(med) => med.tmdbId === result.id && med.mediaType === MediaType.TV
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving upcoming series', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve upcoming series.',
|
|
});
|
|
}
|
|
});
|
|
|
|
discoverRoutes.get('/trending', async (req, res, next) => {
|
|
const tmdb = createTmdbWithRegionLanguage(req.user);
|
|
|
|
try {
|
|
const data = await tmdb.getAllTrending({
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
results: data.results.map((result) =>
|
|
isMovie(result)
|
|
? mapMovieResult(
|
|
result,
|
|
media.find(
|
|
(med) =>
|
|
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
|
|
)
|
|
)
|
|
: isPerson(result)
|
|
? mapPersonResult(result)
|
|
: isCollection(result)
|
|
? mapCollectionResult(result)
|
|
: mapTvResult(
|
|
result,
|
|
media.find(
|
|
(med) =>
|
|
med.tmdbId === result.id && med.mediaType === MediaType.TV
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving trending items', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve trending items.',
|
|
});
|
|
}
|
|
});
|
|
|
|
discoverRoutes.get<{ keywordId: string }>(
|
|
'/keyword/:keywordId/movies',
|
|
async (req, res, next) => {
|
|
const tmdb = new TheMovieDb();
|
|
|
|
try {
|
|
const data = await tmdb.getMoviesByKeyword({
|
|
keywordId: Number(req.params.keywordId),
|
|
page: Number(req.query.page),
|
|
language: (req.query.language as string) ?? req.locale,
|
|
});
|
|
|
|
const media = await Media.getRelatedMedia(
|
|
req.user,
|
|
data.results.map((result) => result.id)
|
|
);
|
|
|
|
return res.status(200).json({
|
|
page: data.page,
|
|
totalPages: data.total_pages,
|
|
totalResults: data.total_results,
|
|
results: data.results.map((result) =>
|
|
mapMovieResult(
|
|
result,
|
|
media.find(
|
|
(med) =>
|
|
med.tmdbId === result.id && med.mediaType === MediaType.MOVIE
|
|
)
|
|
)
|
|
),
|
|
});
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving movies by keyword', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
keywordId: req.params.keywordId,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve movies by keyword.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
|
|
'/genreslider/movie',
|
|
async (req, res, next) => {
|
|
const tmdb = new TheMovieDb();
|
|
|
|
try {
|
|
const mappedGenres: GenreSliderItem[] = [];
|
|
|
|
const genres = await tmdb.getMovieGenres({
|
|
language: (req.query.language as string) ?? req.locale,
|
|
});
|
|
|
|
await Promise.all(
|
|
genres.map(async (genre) => {
|
|
const genreData = await tmdb.getDiscoverMovies({
|
|
genre: genre.id.toString(),
|
|
});
|
|
|
|
mappedGenres.push({
|
|
id: genre.id,
|
|
name: genre.name,
|
|
backdrops: genreData.results
|
|
.filter((title) => !!title.backdrop_path)
|
|
.map((title) => title.backdrop_path) as string[],
|
|
});
|
|
})
|
|
);
|
|
|
|
const sortedData = sortBy(mappedGenres, 'name');
|
|
|
|
return res.status(200).json(sortedData);
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving the movie genre slider', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve movie genre slider.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get<{ language: string }, GenreSliderItem[]>(
|
|
'/genreslider/tv',
|
|
async (req, res, next) => {
|
|
const tmdb = new TheMovieDb();
|
|
|
|
try {
|
|
const mappedGenres: GenreSliderItem[] = [];
|
|
|
|
const genres = await tmdb.getTvGenres({
|
|
language: (req.query.language as string) ?? req.locale,
|
|
});
|
|
|
|
await Promise.all(
|
|
genres.map(async (genre) => {
|
|
const genreData = await tmdb.getDiscoverTv({
|
|
genre: genre.id.toString(),
|
|
});
|
|
|
|
mappedGenres.push({
|
|
id: genre.id,
|
|
name: genre.name,
|
|
backdrops: genreData.results
|
|
.filter((title) => !!title.backdrop_path)
|
|
.map((title) => title.backdrop_path) as string[],
|
|
});
|
|
})
|
|
);
|
|
|
|
const sortedData = sortBy(mappedGenres, 'name');
|
|
|
|
return res.status(200).json(sortedData);
|
|
} catch (e) {
|
|
logger.debug('Something went wrong retrieving the series genre slider', {
|
|
label: 'API',
|
|
errorMessage: e.message,
|
|
});
|
|
return next({
|
|
status: 500,
|
|
message: 'Unable to retrieve series genre slider.',
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
discoverRoutes.get<Record<string, unknown>, WatchlistResponse>(
|
|
'/watchlist',
|
|
async (req, res) => {
|
|
const userRepository = getRepository(User);
|
|
const itemsPerPage = 20;
|
|
const page = Number(req.query.page) ?? 1;
|
|
const offset = (page - 1) * itemsPerPage;
|
|
|
|
const activeUser = await userRepository.findOne({
|
|
where: { id: req.user?.id },
|
|
select: ['id', 'plexToken'],
|
|
});
|
|
|
|
if (activeUser) {
|
|
const [result, total] = await getRepository(Watchlist).findAndCount({
|
|
where: { requestedBy: { id: activeUser?.id } },
|
|
relations: {
|
|
/*requestedBy: true,media:true*/
|
|
},
|
|
// loadRelationIds: true,
|
|
take: itemsPerPage,
|
|
skip: offset,
|
|
});
|
|
if (total) {
|
|
return res.json({
|
|
page: page,
|
|
totalPages: Math.ceil(total / itemsPerPage),
|
|
totalResults: total,
|
|
results: result,
|
|
});
|
|
}
|
|
}
|
|
if (!activeUser?.plexToken) {
|
|
// We will just return an empty array if the user has no Plex token
|
|
return res.json({
|
|
page: 1,
|
|
totalPages: 1,
|
|
totalResults: 0,
|
|
results: [],
|
|
});
|
|
}
|
|
|
|
const plexTV = new PlexTvAPI(activeUser.plexToken);
|
|
|
|
const watchlist = await plexTV.getWatchlist({ offset });
|
|
|
|
return res.json({
|
|
page,
|
|
totalPages: Math.ceil(watchlist.totalSize / itemsPerPage),
|
|
totalResults: watchlist.totalSize,
|
|
results: watchlist.items.map((item) => ({
|
|
ratingKey: item.ratingKey,
|
|
title: item.title,
|
|
mediaType: item.type === 'show' ? 'tv' : 'movie',
|
|
tmdbId: item.tmdbId,
|
|
})),
|
|
});
|
|
}
|
|
);
|
|
|
|
export default discoverRoutes;
|