From 163593237536a1137804a930cec03fed9639a319 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Mon, 24 Mar 2025 16:45:33 +0100 Subject: [PATCH] chore: merge upstream (#1466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(pushover): attach image to pushover notification payload (#3701) * fix: api language query parameter (#3720) * docs: add j0srisk as a contributor for code (#3745) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(tooltip): add tooltip to display exact time on date hover (#3773) Co-authored-by: Loetwiek * docs: add Loetwiek as a contributor for code (#3776) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix(ui): ensure title fits into the `view collection` box (#3696) * fix(docs): correct openapi docs minor issues (#3648) * docs: add Fuochi as a contributor for doc (#3826) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat: translations update from Hosted Weblate (#3597) * feat(lang): translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Portuguese (Brazil)) Currently translated at 99.8% (1232 of 1234 strings) Co-authored-by: Cleiton Carvalho Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (German) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (German) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1236 of 1236 strings) feat(lang): translated using Weblate (Danish) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Anders Ecklon Co-authored-by: Hosted Weblate Co-authored-by: Kenneth Hansen Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/da/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Greek) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Greek) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: BeardedWatermelon Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/el/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 99.5% (1234 of 1240 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) feat(lang): translated using Weblate (Russian) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Romanian) Currently translated at 37.1% (461 of 1240 strings) feat(lang): translated using Weblate (Romanian) Currently translated at 37.0% (459 of 1240 strings) feat(lang): translated using Weblate (Romanian) Currently translated at 34.8% (432 of 1240 strings) Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ro/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Bulgarian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 57.4% (712 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 13.2% (164 of 1240 strings) feat(lang): translated using Weblate (Bulgarian) Currently translated at 4.8% (60 of 1240 strings) feat(lang): added translation using Weblate (Bulgarian) Co-authored-by: Hosted Weblate Co-authored-by: sct Co-authored-by: Димитър Мазнеков (Topper) Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/bg/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 97.9% (1215 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 82.0% (1017 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 72.9% (905 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 72.9% (905 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 71.3% (885 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 64.9% (805 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 64.4% (799 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 63.8% (792 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 63.7% (791 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 57.5% (714 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 49.9% (619 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 35.9% (446 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 35.9% (446 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 32.1% (399 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 24.6% (306 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 18.9% (235 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 17.5% (217 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 17.3% (215 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 8.0% (100 of 1240 strings) feat(lang): translated using Weblate (Ukrainian) Currently translated at 3.3% (41 of 1240 strings) feat(lang): added translation using Weblate (Ukrainian) Co-authored-by: Hosted Weblate Co-authored-by: Michael Michael Co-authored-by: sct Co-authored-by: Сергій Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/uk/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Catalan) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Catalan) Currently translated at 100.0% (1240 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: dtalens Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ca/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Czech) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Czech) Currently translated at 99.6% (1236 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Karel Krýda Co-authored-by: Smexhy Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/cs/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Croatian) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.8% (1238 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.8% (1238 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.6% (1236 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.5% (1235 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.5% (1235 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 99.1% (1230 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 97.5% (1210 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.5% (1185 of 1240 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.6% (1182 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.6% (1182 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.2% (1177 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 95.2% (1177 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 94.3% (1166 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 91.7% (1134 of 1236 strings) feat(lang): translated using Weblate (Croatian) Currently translated at 91.7% (1134 of 1236 strings) Co-authored-by: Bruno Ševčenko Co-authored-by: Hosted Weblate Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Hungarian) Currently translated at 91.3% (1133 of 1240 strings) feat(lang): translated using Weblate (Hungarian) Currently translated at 89.3% (1108 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Levente Szajkó Co-authored-by: Nandor Rusz Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Hebrew) Currently translated at 13.9% (172 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: osh Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/he/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Polish) Currently translated at 99.1% (1225 of 1236 strings) Co-authored-by: Eryk Michalak Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pl/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Italian) Currently translated at 92.8% (1148 of 1236 strings) Co-authored-by: Francesco Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Arabic) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Fhd-pro Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ar/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Dutch) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Dutch) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Hosted Weblate Co-authored-by: Kobe Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Spanish) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (French) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (French) Currently translated at 100.0% (1236 of 1236 strings) feat(lang): translated using Weblate (French) Currently translated at 99.9% (1235 of 1236 strings) feat(lang): translated using Weblate (French) Currently translated at 99.9% (1235 of 1236 strings) Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Hosted Weblate Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Swedish) Currently translated at 100.0% (1236 of 1236 strings) Co-authored-by: Hosted Weblate Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Finnish) Currently translated at 2.6% (33 of 1240 strings) feat(lang): added translation using Weblate (Finnish) Co-authored-by: Eero Konttaniemi Co-authored-by: Hosted Weblate Co-authored-by: sct Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fi/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Serbian) Currently translated at 50.8% (630 of 1240 strings) Co-authored-by: Hosted Weblate Co-authored-by: Milan Smudja Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Korean) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Developer J Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ko/ Translation: Overseerr/Overseerr Frontend * feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1240 of 1240 strings) feat(lang): translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1234 of 1234 strings) Co-authored-by: Haohao Zhang Co-authored-by: Hosted Weblate Co-authored-by: lkw123 Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hans/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: Cleiton Carvalho Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Co-authored-by: Anders Ecklon Co-authored-by: Kenneth Hansen Co-authored-by: BeardedWatermelon Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: sct Co-authored-by: Michael Michael Co-authored-by: Сергій Co-authored-by: dtalens Co-authored-by: Karel Krýda Co-authored-by: Smexhy Co-authored-by: Bruno Ševčenko Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Co-authored-by: Levente Szajkó Co-authored-by: osh Co-authored-by: Eryk Michalak Co-authored-by: Francesco Co-authored-by: Fhd-pro Co-authored-by: Kobe Co-authored-by: gallegonovato Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: Eero Konttaniemi Co-authored-by: Milan Smudja Co-authored-by: Developer J Co-authored-by: Haohao Zhang Co-authored-by: lkw123 * feat(lang): add lang config for Bulgarian, Finnish, Ukrainian, Indonesian, Slovak, Turkish and Maori (#3834) * fix: correct deeplinks on iPad (#3883) * feat(studios): add a24 to studios list (#3902) * docs: add demrich as a contributor for code (#3906) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(watchlist): Cache watchlist requests with matching E-Tags (#3901) * perf(watchlist): add E-Tag caching to Plex watchlist requests * refactor(watchlist): increase frequency of watchlist requests * fix: sync watchlist every 3 min instead of 3 sec * docs: add maxnatamo as a contributor for code (#3907) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(plex): refresh token schedule (#3875) * feat: refresh token schedule fix #3861 * fix(i18n): add i18n message * refactor(plextv): use randomUUID crypto instead custom function * docs: add DamsDev1 as a contributor for code (#3924) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * fix: correct icon showing on certain phones when not pulled (#3939) * feat: add support for requesting "Specials" for TV Shows (#3724) * feat: add support for requesting "Specials" for TV Shows This commit is responsible for adding support in Overseerr for requesting "Special" episodes for TV Shows. This request has become especially pertinent when you consider shows like "Doctor Who". These shows have Specials that are critical to understanding the plot of a TV show. fix #779 * chore(yarn.lock): undo inappropriate changes to yarn.lock I was informed by @sct in a comment on the #3724 PR that it was not appropriate to commit the changes that ended up being made to the yarn.lock file. This commit is responsible, then, for undoing the changes to the yarn.lock file that ended up being submitted. * refactor: change loose equality to strict equality I received a comment from OwsleyJr pointing out that we are using loose equality when we could alternatively just be using strict equality to increase the robustness of our code. This commit does exactly that by squashing out previous usages of loose equality in my commits and replacing them with strict equality * refactor: move 'Specials' string to a global message Owsley pointed out that we are redefining the 'Specials' string multiple times throughout this PR. Instead, we can just move it as a global message. This commit does exactly that. It squashes out and previous declarations of the 'Specials' string inside the src files, and moves it directly to the global messages file. * docs: add AhmedNSidd as a contributor for code (#3964) [skip ci] * docs: update README.md * docs: update .all-contributorsrc --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * feat(lang): Translations update from Hosted Weblate (#3835) * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate 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 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 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 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 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 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 Co-authored-by: Димитър Мазнеков (Topper) 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 Co-authored-by: Michael Michael 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 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 Co-authored-by: dtalens 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 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 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 Co-authored-by: Hosted Weblate 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 Co-authored-by: senza 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 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 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 Co-authored-by: Robin Van de Vyvere 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 Co-authored-by: Hosted Weblate Co-authored-by: gallegonovato 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 Co-authored-by: Nackophilz Co-authored-by: TayZ3r 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 Co-authored-by: Per Erik 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 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 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 Co-authored-by: W L 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 Co-authored-by: Hyun Lee Co-authored-by: cutiekeek 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 Co-authored-by: Rafael Souto 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 Co-authored-by: Marc Lerno Co-authored-by: dtalens 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 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 Co-authored-by: exentler 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) Co-authored-by: Michael Michael Co-authored-by: dtalens Co-authored-by: Dargo Co-authored-by: senza Co-authored-by: Robin Van de Vyvere Co-authored-by: Frostar Co-authored-by: gallegonovato Co-authored-by: Nackophilz Co-authored-by: TayZ3r Co-authored-by: Per Erik Co-authored-by: Oskari Lavinto Co-authored-by: W L Co-authored-by: Hyun Lee Co-authored-by: cutiekeek Co-authored-by: Rafael Souto Co-authored-by: Marc Lerno Co-authored-by: exentler * 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 * 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 Co-authored-by: Hosted Weblate 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 Co-authored-by: Rico 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 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 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 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 Co-authored-by: Hosted Weblate 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 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 Co-authored-by: Yaroslav Buzko 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 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 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 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 Co-authored-by: ugyes 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 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 Co-authored-by: Hosted Weblate 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 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 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 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 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 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 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 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 Co-authored-by: sct * Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Hosted Weblate 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 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 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 Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ Translation: Overseerr/Overseerr Frontend --------- Co-authored-by: Felipe Garcia Co-authored-by: Rico Co-authored-by: George L Co-authored-by: Yaroslav Buzko Co-authored-by: ugyes Co-authored-by: Alberto Giardino Co-authored-by: sct * 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 * 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 --------- Co-authored-by: Isaac M Co-authored-by: Joseph Risk Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Loetwiek <79059734+Loetwiek@users.noreply.github.com> Co-authored-by: Loetwiek Co-authored-by: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Co-authored-by: Fuochi Co-authored-by: Weblate (bot) Co-authored-by: Cleiton Carvalho Co-authored-by: Nandor Rusz Co-authored-by: Thomas Schöneberg Co-authored-by: Anders Ecklon Co-authored-by: Kenneth Hansen Co-authored-by: BeardedWatermelon Co-authored-by: SoundwaveUwU Co-authored-by: SoundwaveUwU Co-authored-by: Димитър Мазнеков (Topper) Co-authored-by: Кирилл Тюрин <1337soundwave1337@gmail.com> Co-authored-by: Don Cezar Co-authored-by: Dragos Co-authored-by: Eduard Oancea Co-authored-by: sct Co-authored-by: Michael Michael Co-authored-by: Сергій Co-authored-by: dtalens Co-authored-by: Karel Krýda Co-authored-by: Smexhy Co-authored-by: Bruno Ševčenko Co-authored-by: Milo Ivir Co-authored-by: Stjepan Co-authored-by: lpispek Co-authored-by: Levente Szajkó Co-authored-by: osh Co-authored-by: Eryk Michalak Co-authored-by: Francesco Co-authored-by: Fhd-pro Co-authored-by: Kobe Co-authored-by: gallegonovato Co-authored-by: Baptiste Co-authored-by: Dimitri Co-authored-by: Maxime Lafarie Co-authored-by: Miguel Co-authored-by: asurare Co-authored-by: Per Erik Co-authored-by: Shjosan Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: Eero Konttaniemi Co-authored-by: Milan Smudja Co-authored-by: Developer J Co-authored-by: Haohao Zhang Co-authored-by: lkw123 Co-authored-by: Jordan Jones Co-authored-by: Brandon Cohen Co-authored-by: David Emrich Co-authored-by: Max T. Kristiansen Co-authored-by: Damien Fajole <60252259+DamsDev1@users.noreply.github.com> Co-authored-by: Ahmed Siddiqui <36286128+AhmedNSidd@users.noreply.github.com> Co-authored-by: Dargo Co-authored-by: senza Co-authored-by: Robin Van de Vyvere Co-authored-by: Frostar Co-authored-by: Nackophilz Co-authored-by: TayZ3r Co-authored-by: Oskari Lavinto Co-authored-by: W L Co-authored-by: Hyun Lee Co-authored-by: cutiekeek Co-authored-by: Rafael Souto Co-authored-by: Marc Lerno Co-authored-by: exentler Co-authored-by: soup Co-authored-by: JackOXI <53652452+JackW6809@users.noreply.github.com> Co-authored-by: Stancu Florin Co-authored-by: Stancu Florin Co-authored-by: Brandon Cohen Co-authored-by: Felipe Garcia Co-authored-by: Rico Co-authored-by: George L Co-authored-by: Yaroslav Buzko Co-authored-by: ugyes Co-authored-by: Alberto Giardino Co-authored-by: Lukas Miklosko <44380311+lmiklosko@users.noreply.github.com> Co-authored-by: Ryan Cohen Co-authored-by: Andrew Kennedy Co-authored-by: Danshil Kokil Mungur --- .all-contributorsrc | 102 ++++- README.md | 2 + jellyseerr-api.yml | 84 ++++ package.json | 2 + pnpm-lock.yaml | 17 + server/api/servarr/radarr.ts | 3 +- server/api/servarr/sonarr.ts | 4 +- server/entity/UserPushSubscription.ts | 16 +- .../migration/1740717744278-UpdateWebPush.ts | 31 ++ server/routes/user/index.ts | 78 +++- src/components/Layout/MobileMenu/index.tsx | 4 +- src/components/ServiceWorkerSetup/index.tsx | 33 +- .../UserNotificationsWebPush.tsx | 136 ------- .../UserNotificationsWebPush/DeviceItem.tsx | 110 +++++ .../UserNotificationsWebPush/index.tsx | 378 ++++++++++++++++++ src/i18n/locale/en.json | 22 +- src/pages/_app.tsx | 8 +- src/pages/collection/[collectionId]/index.tsx | 6 +- src/pages/movie/[movieId]/index.tsx | 6 +- src/pages/tv/[tvId]/index.tsx | 4 +- 20 files changed, 861 insertions(+), 185 deletions(-) create mode 100644 server/migration/1740717744278-UpdateWebPush.ts delete mode 100644 src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx create mode 100644 src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem.tsx create mode 100644 src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/index.tsx diff --git a/.all-contributorsrc b/.all-contributorsrc index 4cbc6e2c1..b68f27caa 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -249,7 +249,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/854646?v=4", "profile": "http://www.piribisoft.com", "contributions": [ - "doc" + "doc", + "code" ] }, { @@ -711,6 +712,105 @@ "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" + ] } ] } diff --git a/README.md b/README.md index a275de0c6..02d8839e9 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,8 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon Ahmed Siddiqui
Ahmed Siddiqui

💻 JackOXI
JackOXI

💻 Stancu Florin
Stancu Florin

💻 + Lukas Miklosko
Lukas Miklosko

💻 + Gauthier
Gauthier

💻 diff --git a/jellyseerr-api.yml b/jellyseerr-api.yml index 00b095968..6954992d8 100644 --- a/jellyseerr-api.yml +++ b/jellyseerr-api.yml @@ -3965,6 +3965,8 @@ paths: type: string p256dh: type: string + userAgent: + type: string required: - endpoint - auth @@ -3972,6 +3974,88 @@ paths: responses: '204': description: Successfully registered push subscription + /user/{userId}/pushSubscriptions: + get: + summary: Get all web push notification settings for a user + description: | + Returns all web push notification settings for a user in a JSON object. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + responses: + '200': + description: User web push notification settings in JSON + content: + application/json: + schema: + type: object + properties: + endpoint: + type: string + p256dh: + type: string + auth: + type: string + userAgent: + type: string + /user/{userId}/pushSubscription/{key}: + get: + summary: Get web push notification settings for a user + description: | + Returns web push notification settings for a user in a JSON object. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + - in: path + name: key + required: true + schema: + type: string + responses: + '200': + description: User web push notification settings in JSON + content: + application/json: + schema: + type: object + properties: + endpoint: + type: string + p256dh: + type: string + auth: + type: string + userAgent: + type: string + delete: + summary: Delete user push subscription by key + description: Deletes the user push subscription with the provided key. + tags: + - users + parameters: + - in: path + name: userId + required: true + schema: + type: number + - in: path + name: key + required: true + schema: + type: string + responses: + '204': + description: Successfully removed user push subscription /user/{userId}: get: summary: Get user by ID diff --git a/package.json b/package.json index a0e8a0ef0..143777810 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@svgr/webpack": "6.5.1", "@tanem/react-nprogress": "5.0.30", "@types/wink-jaro-distance": "^2.0.2", + "@types/ua-parser-js": "^0.7.36", "ace-builds": "1.15.2", "bcrypt": "5.1.0", "bowser": "2.11.0", @@ -99,6 +100,7 @@ "tailwind-merge": "^2.6.0", "typeorm": "0.3.11", "undici": "^7.3.0", + "ua-parser-js": "^1.0.35", "web-push": "3.5.0", "wink-jaro-distance": "^2.0.0", "winston": "3.8.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61874f355..86ec16a6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@tanem/react-nprogress': specifier: 5.0.30 version: 5.0.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/ua-parser-js': + specifier: ^0.7.36 + version: 0.7.39 '@types/wink-jaro-distance': specifier: ^2.0.2 version: 2.0.2 @@ -206,6 +209,9 @@ importers: 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)) + ua-parser-js: + specifier: ^1.0.35 + version: 1.0.40 undici: specifier: ^7.3.0 version: 7.3.0 @@ -3412,6 +3418,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/ua-parser-js@0.7.39': + resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} + '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -9223,6 +9232,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-parser-js@1.0.40: + resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -13778,6 +13791,8 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/ua-parser-js@0.7.39': {} + '@types/unist@2.0.10': {} '@types/web-push@3.3.2': @@ -20672,6 +20687,8 @@ snapshots: typescript@5.5.2: {} + ua-parser-js@1.0.40: {} + uc.micro@2.1.0: optional: true diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index f3bf3faaf..35d24024f 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -28,6 +28,7 @@ export interface RadarrMovie { qualityProfileId: number; added: string; hasFile: boolean; + tags: number[]; } class RadarrAPI extends ServarrBase<{ movieId: number }> { @@ -104,7 +105,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { minimumAvailability: options.minimumAvailability, tmdbId: options.tmdbId, year: options.year, - tags: options.tags, + tags: Array.from(new Set([...movie.tags, ...options.tags])), rootFolderPath: options.rootFolderPath, monitored: options.monitored, addOptions: { diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 5590c9acb..0a9c27322 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -184,7 +184,9 @@ class SonarrAPI extends ServarrBase<{ // If the series already exists, we will simply just update it if (series.id) { series.monitored = options.monitored ?? series.monitored; - series.tags = options.tags ?? series.tags; + series.tags = options.tags + ? Array.from(new Set([...series.tags, ...options.tags])) + : series.tags; series.seasons = this.buildSeasonList(options.seasons, series.seasons); const newSeriesData = await this.put( diff --git a/server/entity/UserPushSubscription.ts b/server/entity/UserPushSubscription.ts index 6389ea0b8..f05dd0f2b 100644 --- a/server/entity/UserPushSubscription.ts +++ b/server/entity/UserPushSubscription.ts @@ -1,4 +1,10 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { + Column, + CreateDateColumn, + Entity, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; import { User } from './User'; @Entity() @@ -18,9 +24,15 @@ export class UserPushSubscription { @Column() public p256dh: string; - @Column({ unique: true }) + @Column() public auth: string; + @Column({ nullable: true }) + public userAgent: string; + + @CreateDateColumn({ nullable: true }) + public createdAt: Date; + constructor(init?: Partial) { Object.assign(this, init); } diff --git a/server/migration/1740717744278-UpdateWebPush.ts b/server/migration/1740717744278-UpdateWebPush.ts new file mode 100644 index 000000000..a6dcd0021 --- /dev/null +++ b/server/migration/1740717744278-UpdateWebPush.ts @@ -0,0 +1,31 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateWebPush1740717744278 implements MigrationInterface { + name = 'UpdateWebPush1740717744278'; + + public async up(queryRunner: QueryRunner): Promise { + 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 DEFAULT NULL, "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") SELECT "id", "endpoint", "p256dh", "auth", "userId" 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 { + 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") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + } +} diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 0c79e4f3e..028b26e62 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -184,13 +184,15 @@ router.post< endpoint: string; p256dh: string; auth: string; + userAgent: string; } >('/registerPushSubscription', async (req, res, next) => { try { const userPushSubRepository = getRepository(UserPushSubscription); const existingSubs = await userPushSubRepository.find({ - where: { auth: req.body.auth }, + relations: { user: true }, + where: { auth: req.body.auth, user: { id: req.user?.id } }, }); if (existingSubs.length > 0) { @@ -205,6 +207,7 @@ router.post< auth: req.body.auth, endpoint: req.body.endpoint, p256dh: req.body.p256dh, + userAgent: req.body.userAgent, user: req.user, }); @@ -219,6 +222,79 @@ router.post< } }); +router.get<{ userId: number }>( + '/:userId/pushSubscriptions', + async (req, res, next) => { + try { + const userPushSubRepository = getRepository(UserPushSubscription); + + const userPushSubs = await userPushSubRepository.find({ + relations: { user: true }, + where: { user: { id: req.params.userId } }, + }); + + return res.status(200).json(userPushSubs); + } catch (e) { + next({ status: 404, message: 'User subscriptions not found.' }); + } + } +); + +router.get<{ userId: number; key: string }>( + '/:userId/pushSubscription/:key', + async (req, res, next) => { + try { + const userPushSubRepository = getRepository(UserPushSubscription); + + const userPushSub = await userPushSubRepository.findOneOrFail({ + relations: { + user: true, + }, + where: { + user: { id: req.params.userId }, + p256dh: req.params.key, + }, + }); + + return res.status(200).json(userPushSub); + } catch (e) { + next({ status: 404, message: 'User subscription not found.' }); + } + } +); + +router.delete<{ userId: number; key: string }>( + '/:userId/pushSubscription/:key', + async (req, res, next) => { + try { + const userPushSubRepository = getRepository(UserPushSubscription); + + const userPushSub = await userPushSubRepository.findOneOrFail({ + relations: { + user: true, + }, + where: { + user: { id: req.params.userId }, + p256dh: req.params.key, + }, + }); + + await userPushSubRepository.remove(userPushSub); + return res.status(204).send(); + } catch (e) { + logger.error('Something went wrong deleting the user push subcription', { + label: 'API', + key: req.params.key, + errorMessage: e.message, + }); + return next({ + status: 500, + message: 'User push subcription not found', + }); + } + } +); + router.get<{ id: string }>('/:id', async (req, res, next) => { try { const userRepository = getRepository(User); diff --git a/src/components/Layout/MobileMenu/index.tsx b/src/components/Layout/MobileMenu/index.tsx index 52e84d3de..09cec4a01 100644 --- a/src/components/Layout/MobileMenu/index.tsx +++ b/src/components/Layout/MobileMenu/index.tsx @@ -255,7 +255,9 @@ const MobileMenu = ({ router.pathname.match(link.activeRegExp) ? 'border-indigo-600 from-indigo-700 to-purple-700' : 'border-indigo-500 from-indigo-600 to-purple-600' - } flex h-4 w-4 items-center justify-center !px-[9px] !py-[9px] text-[9px]`} + } flex ${ + pendingRequestsCount > 99 ? 'w-6' : 'w-4' + } h-4 items-center justify-center !px-[5px] !py-[7px] text-[8px]`} > {pendingRequestsCount > 99 ? '99+' diff --git a/src/components/ServiceWorkerSetup/index.tsx b/src/components/ServiceWorkerSetup/index.tsx index f9b42cd39..2e0313f4d 100644 --- a/src/components/ServiceWorkerSetup/index.tsx +++ b/src/components/ServiceWorkerSetup/index.tsx @@ -1,10 +1,9 @@ /* eslint-disable no-console */ -import useSettings from '@app/hooks/useSettings'; + import { useUser } from '@app/hooks/useUser'; import { useEffect } from 'react'; const ServiceWorkerSetup = () => { - const { currentSettings } = useSettings(); const { user } = useUser(); useEffect(() => { if ('serviceWorker' in navigator && user?.id) { @@ -15,40 +14,12 @@ const ServiceWorkerSetup = () => { '[SW] Registration successful, scope is:', registration.scope ); - - if (currentSettings.enablePushRegistration) { - const sub = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: currentSettings.vapidPublic, - }); - - const parsedSub = JSON.parse(JSON.stringify(sub)); - - if (parsedSub.keys.p256dh && parsedSub.keys.auth) { - const res = await fetch('/api/v1/user/registerPushSubscription', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - endpoint: parsedSub.endpoint, - p256dh: parsedSub.keys.p256dh, - auth: parsedSub.keys.auth, - }), - }); - if (!res.ok) throw new Error(); - } - } }) .catch(function (error) { console.log('[SW] Service worker registration failed, error:', error); }); } - }, [ - user, - currentSettings.vapidPublic, - currentSettings.enablePushRegistration, - ]); + }, [user]); return null; }; diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx deleted file mode 100644 index e338c9f0a..000000000 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import Button from '@app/components/Common/Button'; -import LoadingSpinner from '@app/components/Common/LoadingSpinner'; -import NotificationTypeSelector, { - ALL_NOTIFICATIONS, -} from '@app/components/NotificationTypeSelector'; -import { useUser } from '@app/hooks/useUser'; -import globalMessages from '@app/i18n/globalMessages'; -import defineMessages from '@app/utils/defineMessages'; -import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; -import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces'; -import { Form, Formik } from 'formik'; -import { useRouter } from 'next/router'; -import { useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR, { mutate } from 'swr'; - -const messages = defineMessages( - 'components.UserProfile.UserSettings.UserNotificationSettings', - { - webpushsettingssaved: 'Web push notification settings saved successfully!', - webpushsettingsfailed: 'Web push notification settings failed to save.', - } -); - -const UserWebPushSettings = () => { - const intl = useIntl(); - const { addToast } = useToasts(); - const router = useRouter(); - const { user } = useUser({ id: Number(router.query.userId) }); - const { - data, - error, - mutate: revalidate, - } = useSWR( - user ? `/api/v1/user/${user?.id}/settings/notifications` : null - ); - - if (!data && !error) { - return ; - } - - return ( - { - try { - const res = await fetch( - `/api/v1/user/${user?.id}/settings/notifications`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - pgpKey: data?.pgpKey, - discordId: data?.discordId, - pushbulletAccessToken: data?.pushbulletAccessToken, - pushoverApplicationToken: data?.pushoverApplicationToken, - pushoverUserKey: data?.pushoverUserKey, - telegramChatId: data?.telegramChatId, - telegramSendSilently: data?.telegramSendSilently, - notificationTypes: { - webpush: values.types, - }, - }), - } - ); - if (!res.ok) throw new Error(); - mutate('/api/v1/settings/public'); - addToast(intl.formatMessage(messages.webpushsettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast(intl.formatMessage(messages.webpushsettingsfailed), { - appearance: 'error', - autoDismiss: true, - }); - } finally { - revalidate(); - } - }} - > - {({ - errors, - touched, - isSubmitting, - isValid, - values, - setFieldValue, - setFieldTouched, - }) => { - return ( -
- { - setFieldValue('types', newTypes); - setFieldTouched('types'); - }} - error={ - errors.types && touched.types - ? (errors.types as string) - : undefined - } - /> -
-
- - - -
-
- - ); - }} -
- ); -}; - -export default UserWebPushSettings; diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem.tsx new file mode 100644 index 000000000..59da71093 --- /dev/null +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem.tsx @@ -0,0 +1,110 @@ +import ConfirmButton from '@app/components/Common/ConfirmButton'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { + ComputerDesktopIcon, + DevicePhoneMobileIcon, + TrashIcon, +} from '@heroicons/react/24/solid'; +import { useIntl } from 'react-intl'; +import { UAParser } from 'ua-parser-js'; + +interface DeviceItemProps { + disablePushNotifications: (p256dh: string) => void; + device: { + endpoint: string; + p256dh: string; + auth: string; + userAgent: string; + createdAt: Date; + }; +} + +const messages = defineMessages( + 'components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush', + { + operatingsystem: 'Operating System', + browser: 'Browser', + engine: 'Engine', + deletesubscription: 'Delete Subscription', + unknown: 'Unknown', + } +); + +const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => { + const intl = useIntl(); + + return ( +
+
+
+
+ {UAParser(device.userAgent).device.type === 'mobile' ? ( + + ) : ( + + )} +
+
+
+ {device.createdAt + ? intl.formatDate(device.createdAt, { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + : 'N/A'} +
+
+ {device.userAgent + ? UAParser(device.userAgent).device.model + : intl.formatMessage(messages.unknown)} +
+
+
+
+
+ + {intl.formatMessage(messages.operatingsystem)} + + + {device.userAgent ? UAParser(device.userAgent).os.name : 'N/A'} + +
+
+ + {intl.formatMessage(messages.browser)} + + + {device.userAgent + ? UAParser(device.userAgent).browser.name + : 'N/A'} + +
+
+ + {intl.formatMessage(messages.engine)} + + + {device.userAgent + ? UAParser(device.userAgent).engine.name + : 'N/A'} + +
+
+
+
+ disablePushNotifications(device.p256dh)} + confirmText={intl.formatMessage(globalMessages.areyousure)} + className="w-full" + > + + {intl.formatMessage(messages.deletesubscription)} + +
+
+ ); +}; + +export default DeviceItem; diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/index.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/index.tsx new file mode 100644 index 000000000..de438e3ad --- /dev/null +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/index.tsx @@ -0,0 +1,378 @@ +import Alert from '@app/components/Common/Alert'; +import Button from '@app/components/Common/Button'; +import LoadingSpinner from '@app/components/Common/LoadingSpinner'; +import NotificationTypeSelector, { + ALL_NOTIFICATIONS, +} from '@app/components/NotificationTypeSelector'; +import DeviceItem from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem'; +import useSettings from '@app/hooks/useSettings'; +import { useUser } from '@app/hooks/useUser'; +import globalMessages from '@app/i18n/globalMessages'; +import defineMessages from '@app/utils/defineMessages'; +import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; +import { + CloudArrowDownIcon, + CloudArrowUpIcon, +} from '@heroicons/react/24/solid'; +import type { UserPushSubscription } from '@server/entity/UserPushSubscription'; +import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces'; +import { Form, Formik } from 'formik'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR, { mutate } from 'swr'; + +const messages = defineMessages( + 'components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush', + { + webpushsettingssaved: 'Web push notification settings saved successfully!', + webpushsettingsfailed: 'Web push notification settings failed to save.', + enablewebpush: 'Enable web push', + disablewebpush: 'Disable web push', + managedevices: 'Manage Devices', + type: 'type', + created: 'Created', + device: 'Device', + subscriptiondeleted: 'Subscription deleted.', + subscriptiondeleteerror: + 'Something went wrong while deleting the user subscription.', + nodevicestoshow: 'You have no web push subscriptions to show.', + webpushhasbeenenabled: 'Web push has been enabled.', + webpushhasbeendisabled: 'Web push has been disabled.', + enablingwebpusherror: 'Something went wrong while enabling web push.', + disablingwebpusherror: 'Something went wrong while disabling web push.', + } +); + +const UserWebPushSettings = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const router = useRouter(); + const { user } = useUser({ id: Number(router.query.userId) }); + const { currentSettings } = useSettings(); + const [webPushEnabled, setWebPushEnabled] = useState(false); + const { + data, + error, + mutate: revalidate, + } = useSWR( + user ? `/api/v1/user/${user?.id}/settings/notifications` : null + ); + const { data: dataDevices, mutate: revalidateDevices } = useSWR< + { + endpoint: string; + p256dh: string; + auth: string; + userAgent: string; + createdAt: Date; + }[] + >(`/api/v1/user/${user?.id}/pushSubscriptions`, { revalidateOnMount: true }); + + // Subscribes to the push manager + // Will only add to the database if subscribing for the first time + const enablePushNotifications = () => { + if ('serviceWorker' in navigator && user?.id) { + navigator.serviceWorker + .getRegistration('/sw.js') + .then(async (registration) => { + if (currentSettings.enablePushRegistration) { + const sub = await registration?.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: currentSettings.vapidPublic, + }); + const parsedSub = JSON.parse(JSON.stringify(sub)); + + if (parsedSub.keys.p256dh && parsedSub.keys.auth) { + const res = await fetch('/api/v1/user/registerPushSubscription', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + endpoint: parsedSub.endpoint, + p256dh: parsedSub.keys.p256dh, + auth: parsedSub.keys.auth, + userAgent: navigator.userAgent, + }), + }); + if (!res.ok) { + throw new Error(res.statusText); + } + setWebPushEnabled(true); + addToast(intl.formatMessage(messages.webpushhasbeenenabled), { + appearance: 'success', + autoDismiss: true, + }); + } + } + }) + .catch(function () { + addToast(intl.formatMessage(messages.enablingwebpusherror), { + autoDismiss: true, + appearance: 'error', + }); + }) + .finally(function () { + revalidateDevices(); + }); + } + }; + + // Unsubscribes from the push manager + // Deletes/disables corresponding push subscription from database + const disablePushNotifications = async (p256dh?: string) => { + if ('serviceWorker' in navigator && user?.id) { + navigator.serviceWorker.getRegistration('/sw.js').then((registration) => { + registration?.pushManager + .getSubscription() + .then(async (subscription) => { + const parsedSub = JSON.parse(JSON.stringify(subscription)); + + const res = await fetch( + `/api/v1/user/${user?.id}/pushSubscription/${ + p256dh ? p256dh : parsedSub.keys.p256dh + }`, + { + method: 'DELETE', + } + ); + if (!res.ok) { + throw new Error(res.statusText); + } + if (subscription && (p256dh === parsedSub.keys.p256dh || !p256dh)) { + subscription.unsubscribe(); + setWebPushEnabled(false); + } + addToast( + intl.formatMessage( + p256dh + ? messages.subscriptiondeleted + : messages.webpushhasbeendisabled + ), + { + autoDismiss: true, + appearance: 'success', + } + ); + }) + .catch(function () { + addToast( + intl.formatMessage( + p256dh + ? messages.subscriptiondeleteerror + : messages.disablingwebpusherror + ), + { + autoDismiss: true, + appearance: 'error', + } + ); + }) + .finally(function () { + revalidateDevices(); + }); + }); + } + }; + + // Checks our current subscription on page load + // Will set the web push state to true if subscribed + useEffect(() => { + if ('serviceWorker' in navigator && user?.id) { + navigator.serviceWorker + .getRegistration('/sw.js') + .then(async (registration) => { + await registration?.pushManager + .getSubscription() + .then(async (subscription) => { + if (subscription) { + const parsedKey = JSON.parse(JSON.stringify(subscription)); + const response = await fetch( + `/api/v1/user/${user.id}/pushSubscription/${parsedKey.keys.p256dh}` + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const currentUserPushSub = { + data: (await response.json()) as UserPushSubscription, + }; + + if (currentUserPushSub.data.p256dh !== parsedKey.keys.p256dh) { + return; + } + setWebPushEnabled(true); + } else { + setWebPushEnabled(false); + } + }); + }) + .catch(function (error) { + setWebPushEnabled(false); + // eslint-disable-next-line no-console + console.log( + '[SW] Failure retrieving push manager subscription, error:', + error + ); + }); + } + }, [user?.id]); + + if (!data && !error) { + return ; + } + + return ( + <> + { + try { + const res = await fetch( + `/api/v1/user/${user?.id}/settings/notifications`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + pgpKey: data?.pgpKey, + discordId: data?.discordId, + pushbulletAccessToken: data?.pushbulletAccessToken, + pushoverApplicationToken: data?.pushoverApplicationToken, + pushoverUserKey: data?.pushoverUserKey, + telegramChatId: data?.telegramChatId, + telegramSendSilently: data?.telegramSendSilently, + notificationTypes: { + webpush: values.types, + }, + }), + } + ); + if (!res.ok) { + throw new Error(res.statusText); + } + mutate('/api/v1/settings/public'); + addToast(intl.formatMessage(messages.webpushsettingssaved), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.webpushsettingsfailed), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + revalidate(); + } + }} + > + {({ + errors, + touched, + isSubmitting, + isValid, + values, + setFieldValue, + setFieldTouched, + }) => { + return ( +
+ { + setFieldValue('types', newTypes); + setFieldTouched('types'); + }} + error={ + errors.types && touched.types + ? (errors.types as string) + : undefined + } + /> +
+
+ + + + + + +
+
+ + ); + }} +
+
+

+ {intl.formatMessage(messages.managedevices)} +

+
+ {dataDevices?.length ? ( + dataDevices + ?.sort((a, b) => { + const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0; + const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0; + return dateB - dateA; + }) + .map((device, index) => ( +
+ +
+ )) + ) : ( + <> + + + )} +
+
+ + ); +}; + +export default UserWebPushSettings; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 50df170b2..121f6882e 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1339,6 +1339,26 @@ "components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "You do not have permission to modify this user's linked accounts.", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "This account is already linked to a Plex user", "components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "Unable to connect to Plex using your credentials", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "Browser", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "Created", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "Delete Subscription", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "Device", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "Disable web push", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "Something went wrong while disabling web push.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "Enable web push", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "Something went wrong while enabling web push.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "Engine", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "Manage Devices", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "You have no web push subscriptions to show.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "Operating System", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "Subscription deleted.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "Something went wrong while deleting the user subscription.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "type", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "Unknown", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "Web push has been disabled.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "Web push has been enabled.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "Web push notification settings failed to save.", + "components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "Web push notification settings saved successfully!", "components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default", "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID", "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The multi-digit ID number associated with your user account", @@ -1378,8 +1398,6 @@ "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid chat ID", "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "The thread/topic ID must be a positive whole number", "components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push", - "components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingsfailed": "Web push notification settings failed to save.", - "components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingssaved": "Web push notification settings saved successfully!", "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password", "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password", "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password", diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 9e87cbdf0..1b29d41e8 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -242,7 +242,9 @@ CoreApp.getInitialProps = async (initialProps) => { if (ctx.res) { // Check if app is initialized and redirect if necessary const res = await fetch( - `http://localhost:${process.env.PORT || 5055}/api/v1/settings/public` + `http://${process.env.HOST || 'localhost'}:${ + process.env.PORT || 5055 + }/api/v1/settings/public` ); if (!res.ok) throw new Error(); currentSettings = await res.json(); @@ -260,7 +262,9 @@ CoreApp.getInitialProps = async (initialProps) => { try { // Attempt to get the user by running a request to the local api const res = await fetch( - `http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`, + `http://${process.env.HOST || 'localhost'}:${ + process.env.PORT || 5055 + }/api/v1/auth/me`, { headers: ctx.req && ctx.req.headers.cookie diff --git a/src/pages/collection/[collectionId]/index.tsx b/src/pages/collection/[collectionId]/index.tsx index b0c47b17c..da9c6bf03 100644 --- a/src/pages/collection/[collectionId]/index.tsx +++ b/src/pages/collection/[collectionId]/index.tsx @@ -14,9 +14,9 @@ export const getServerSideProps: GetServerSideProps< CollectionPageProps > = async (ctx) => { const res = await fetch( - `http://localhost:${process.env.PORT || 5055}/api/v1/collection/${ - ctx.query.collectionId - }`, + `http://${process.env.HOST || 'localhost'}:${ + process.env.PORT || 5055 + }/api/v1/collection/${ctx.query.collectionId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } diff --git a/src/pages/movie/[movieId]/index.tsx b/src/pages/movie/[movieId]/index.tsx index be0d2aa5a..cf2b11b9c 100644 --- a/src/pages/movie/[movieId]/index.tsx +++ b/src/pages/movie/[movieId]/index.tsx @@ -14,9 +14,9 @@ export const getServerSideProps: GetServerSideProps = async ( ctx ) => { const res = await fetch( - `http://localhost:${process.env.PORT || 5055}/api/v1/movie/${ - ctx.query.movieId - }`, + `http://${process.env.HOST || 'localhost'}:${ + process.env.PORT || 5055 + }/api/v1/movie/${ctx.query.movieId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie } diff --git a/src/pages/tv/[tvId]/index.tsx b/src/pages/tv/[tvId]/index.tsx index 3961b157a..36fba5fcc 100644 --- a/src/pages/tv/[tvId]/index.tsx +++ b/src/pages/tv/[tvId]/index.tsx @@ -14,7 +14,9 @@ export const getServerSideProps: GetServerSideProps = async ( ctx ) => { const res = await fetch( - `http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`, + `http://${process.env.HOST || 'localhost'}:${ + process.env.PORT || 5055 + }/api/v1/tv/${ctx.query.tvId}`, { headers: ctx.req?.headers?.cookie ? { cookie: ctx.req.headers.cookie }