Compare commits

..

359 Commits

Author SHA1 Message Date
semantic-release-bot
686fe6fdf8 chore(release): 1.18.0 2021-01-30 06:13:16 +00:00
sct
c80a1fa261 Merge branch 'develop' 2021-01-30 06:06:49 +00:00
sct
6f3f24a1d7 ci(release): fix release workflow formatting 2021-01-30 06:02:45 +00:00
Weblate (bot)
71875efb48 feat(lang): translations update from Weblate (#727)
* feat(lang): translated using Weblate (English)

Currently translated at 100.0% (605 of 605 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/en/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Hungarian)

Currently translated at 26.6% (160 of 600 strings)

feat(lang): translated using Weblate (Hungarian)

Currently translated at 23.5% (135 of 573 strings)

feat(lang): added translation using Weblate (Hungarian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mo Pi <mopihu@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/hu/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (600 of 600 strings)

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (573 of 573 strings)

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (551 of 551 strings)

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (545 of 545 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_PT/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (606 of 607 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (603 of 605 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (600 of 600 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (596 of 596 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (575 of 575 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (573 of 573 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 92.2% (512 of 555 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 86.7% (525 of 605 strings)

feat(lang): translated using Weblate (French)

Currently translated at 75.1% (448 of 596 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (573 of 573 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (555 of 555 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (551 of 551 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (551 of 551 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (545 of 545 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 90.9% (552 of 607 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 90.7% (549 of 605 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 90.2% (546 of 605 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 81.8% (488 of 596 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 80.7% (481 of 596 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 91.1% (497 of 545 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Swedish)

Currently translated at 60.5% (361 of 596 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Serbian)

Currently translated at 43.7% (261 of 596 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 90.7% (520 of 573 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 93.6% (520 of 555 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Spanish)

Currently translated at 97.4% (535 of 549 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 97.2% (530 of 545 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 98.8% (598 of 605 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (596 of 596 strings)

feat(lang): translated using Weblate (German)

Currently translated at 90.4% (520 of 575 strings)

feat(lang): translated using Weblate (German)

Currently translated at 92.8% (532 of 573 strings)

feat(lang): translated using Weblate (German)

Currently translated at 95.1% (528 of 555 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (551 of 551 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (545 of 545 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul Hagedorn <panzer1119@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 99.6% (603 of 605 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (600 of 600 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (575 of 575 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (573 of 573 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 91.9% (527 of 573 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (551 of 551 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (545 of 545 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Russian)

Currently translated at 15.3% (88 of 573 strings)

Co-authored-by: David <david@hmnd.io>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 53.1% (317 of 596 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: TheCatLady <doris.houng@gmail.com>
Co-authored-by: Mo Pi <mopihu@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul Hagedorn <panzer1119@gmail.com>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Co-authored-by: David <david@hmnd.io>
2021-01-30 14:59:44 +09:00
samwiseg0
2e724b5bd8 build(snap): Fix snap to pull from latest master [skip ci] (#783) 2021-01-30 14:53:19 +09:00
sct
136d874cba fix(sync): do not update series status if already available and no new seasons
fixes #777
2021-01-30 05:26:24 +00:00
TheCatLady
f1dd5e7e12 feat(ui): Move PROXY setting to UI (#782)
Users who have set PROXY=yes in their Docker containers will need to configure this setting in the UI and restart the container.
2021-01-30 10:46:51 +09:00
sct
67f8aef00d feat: add manual availability buttons to manage slideover
closes #672
2021-01-29 12:31:19 +00:00
sct
b5fd1d520c fix(notifications): only send a single notification when standard media becomes available
fixes #770
2021-01-29 08:13:50 +00:00
costaht
c43cd16eff Setting up Fail2ban with Overseerr (#739)
Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
Co-authored-by: Jakob Ankarhem <jakob.ankarhem@outlook.com>
2021-01-29 17:03:59 +09:00
sct
3812989a1c fix(sonarr-sync): correctly set series with no seasons to requested status
fixes #762
2021-01-28 10:35:42 +00:00
Jakob Ankarhem
1d7150c24e feat(media): add link to the item on plex (#735)
Co-authored-by: sct <sctsnipe@gmail.com>
2021-01-28 15:13:43 +09:00
sct
946bd2db5e fix(frontend): dont show external links unless slug is set 2021-01-28 00:47:36 +00:00
Jakob Ankarhem
0c6d4780c3 fix(radarr): return the updated data when updating radarr request (#765) 2021-01-28 08:04:31 +09:00
Jakob Ankarhem
45a277964b fix(radarr): fix request bug which made it unable to be added to radarr (#760) 2021-01-28 07:35:05 +09:00
sct
ec5fb83678 feat: Radarr & Sonarr Sync (#734) 2021-01-27 23:52:37 +09:00
sct
86efcd82c3 fix(frontend): fix server name position on plex settings page 2021-01-27 00:48:09 +00:00
TheCatLady
f05d4a0d0b fix(ui): Capitalization, punctuation, and grammar inconsistences & errors (#731) 2021-01-27 08:34:52 +09:00
Jakob Ankarhem
9342a40bbc fix(ip logging): add env var for proxy to fix ip logging on failed logins (#756) 2021-01-27 08:18:45 +09:00
Jakob Ankarhem
20ca3f2f5f feat(users): add editable usernames (#715) 2021-01-27 08:09:42 +09:00
Chris Pritchard
82ac76b054 feat: pre-populate server info from plex.tv API (#563)
Co-authored-by: sct <sctsnipe@gmail.com>
2021-01-25 16:34:21 +09:00
allcontributors[bot]
b3313a7f4d docs: add hmnd as a contributor (#740) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-25 14:12:02 +09:00
TheCatLady
6e2589178b feat(auth): Add optional CSRF protection (#697)
* fix(auth): Missing CSRF middleware
Resolves LGTM alert/error for query js/missing-token-validation
More info: https://lgtm.com/rules/1506064038914/
2021-01-25 11:27:57 +09:00
David
4b0241c3b3 feat: ability to edit user settings in bulk (#597) 2021-01-25 09:29:43 +09:00
sct
2f75c4c6ae fix(lang): add missing i18n strings for notification settings 2021-01-23 10:13:10 +00:00
sct
21db3676d1 feat(notifications): add option to send notifications for auto-approved requests
closes #267
2021-01-23 09:54:43 +00:00
sct
d0c830e80d fix: show recently added series even if they are not complete 2021-01-22 16:01:46 +00:00
Jakob Ankarhem
660ada0b20 fix(permissions): use default user permissions when creating a local user (#713) 2021-01-23 00:46:41 +09:00
Jakob Ankarhem
2d31ea940a feat(login): add request ip to the failed request log (#714) 2021-01-23 00:23:07 +09:00
allcontributors[bot]
3ebb7d087f docs: add chriscpritchard as a contributor (#722) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-22 23:12:38 +09:00
sct
5e352c201f fix(frontend): fixed mismatched rounded sizing on new login
also fixes log filename in temp log page

fixes #721
2021-01-22 14:08:10 +00:00
sct
e044146aa5 feat(lang): add support for Portuguese (Portugal) language 2021-01-22 14:02:02 +00:00
sct
083a74a686 feat(lang): update languages and fix merge conflict 2021-01-22 13:47:03 +00:00
Chris Pritchard
58dbdad2b2 docs(install/gentoo): updated gentoo install instructions [skip ci] (#701)
Includes the live ebuild (which installs the development version of the package)

Amended gentoo instructions now that repository is included in the list provided by gentoo

Co-authored-by: sct <ryan@sct.dev>
2021-01-22 22:32:02 +09:00
Tijuco
32f4916c4a feat(lang): translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (545 of 545 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
2021-01-22 12:23:27 +01:00
Kobe
6d0f7d4b50 feat(lang): translated using Weblate (Dutch)
Currently translated at 100.0% (545 of 545 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
2021-01-22 12:23:27 +01:00
Jakob Ankarhem
f90e43b01a refactor(login): redesign login page (#709) 2021-01-22 20:23:22 +09:00
Hosted Weblate
9bb11afc6b feat(lang): translated using Weblate (English)
Currently translated at 100.0% (534 of 534 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/en/
Translation: Overseerr/Overseerr Frontend
2021-01-22 03:30:37 +01:00
Hosted Weblate
059995e0ef feat(lang): translated using Weblate (Dutch)
Currently translated at 100.0% (536 of 536 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (534 of 534 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 99.8% (533 of 534 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 99.8% (533 of 534 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend
2021-01-22 03:30:37 +01:00
Hosted Weblate
c2a3e8ed52 feat(lang): translated using Weblate (German)
Currently translated at 99.8% (535 of 536 strings)

feat(lang): translated using Weblate (German)

Currently translated at 98.6% (529 of 536 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.8% (533 of 534 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (533 of 533 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: Paul Hagedorn <panzer1119@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend
2021-01-22 03:30:37 +01:00
Hosted Weblate
4f94d227fc feat(lang): translated using Weblate (Spanish)
Currently translated at 99.0% (531 of 536 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (534 of 534 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend
2021-01-22 03:30:37 +01:00
Hosted Weblate
9aa5c12164 feat(lang): translated using Weblate (Italian)
Currently translated at 98.5% (528 of 536 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 98.5% (528 of 536 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 92.7% (497 of 536 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 93.8% (501 of 534 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend
2021-01-22 03:30:37 +01:00
Hosted Weblate
4789583d66 feat(lang): translated using Weblate (French)
Currently translated at 99.8% (535 of 536 strings)

feat(lang): translated using Weblate (French)

Currently translated at 98.6% (529 of 536 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (534 of 534 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (534 of 534 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (534 of 534 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend
2021-01-22 03:30:37 +01:00
Hosted Weblate
98570c920e feat(lang): translated using Weblate (Portuguese (Brazil))
Currently translated at 99.4% (533 of 536 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (536 of 536 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (536 of 536 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 98.6% (529 of 536 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (534 of 534 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (534 of 534 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (534 of 534 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 92.8% (496 of 534 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 92.8% (496 of 534 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 89.7% (479 of 534 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend
2021-01-22 03:30:37 +01:00
Hosted Weblate
f001fb3b33 feat(lang): translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (536 of 536 strings)

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 52.0% (279 of 536 strings)

feat(lang): translated using Weblate (Portuguese (Portugal))

Currently translated at 0.0% (0 of 536 strings)

feat(lang): added translation using Weblate (Portuguese (Portugal))

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marcos <silvamarcos1919@outlook.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_PT/
Translation: Overseerr/Overseerr Frontend
2021-01-22 03:30:37 +01:00
samwiseg0
e1accbcef0 build(armhf): Add libatomic1 to snap build for armhf (#720) 2021-01-22 11:30:33 +09:00
sct
ae9a1b3e94 fix(frontend): add poster not found image to request card and request list item 2021-01-22 02:06:36 +00:00
sct
0b055458d0 fix(frontend): add poster not found image to tv details page 2021-01-22 02:03:41 +00:00
sct
be650aae37 build(deps): bump dependencies 2021-01-22 02:02:22 +00:00
sct
a42f3115d3 chore: add headwind as a recommended extension for vscode [skip ci] 2021-01-22 01:53:54 +00:00
Jakob Ankarhem
2da0da826a fix(requests): handle when tvdbid is null (#657)
Co-authored-by: sct <sctsnipe@gmail.com>
2021-01-22 10:49:17 +09:00
imgbot[bot]
a3fe4e6321 refactor: [ImgBot] Optimize images (#712)
*Total -- 3,427.86kb -> 3,268.29kb (4.66%)

/public/images/radarr_logo.png -- 10.34kb -> 8.13kb (21.37%)
/public/logo.png -- 77.09kb -> 64.06kb (16.91%)
/public/os_logo_square.png -- 47.18kb -> 40.52kb (14.11%)
/public/android-chrome-512x512.png -- 78.14kb -> 70.37kb (9.94%)
/public/apple-touch-icon.png -- 25.49kb -> 23.13kb (9.28%)
/public/android-chrome-192x192.png -- 24.60kb -> 22.43kb (8.81%)
/src/assets/services/imdb.svg -- 3.89kb -> 3.63kb (6.58%)
/public/preview.jpg -- 1,658.89kb -> 1,588.79kb (4.23%)
/public/images/rotate2.jpg -- 1,090.20kb -> 1,045.92kb (4.06%)
/public/images/rotate3.jpg -- 382.15kb -> 371.60kb (2.76%)
/src/assets/bolt.svg -- 0.25kb -> 0.24kb (2.72%)
/public/favicon-32x32.png -- 2.09kb -> 2.03kb (2.71%)
/src/assets/xcircle.svg -- 0.26kb -> 0.25kb (2.62%)
/src/assets/extlogos/pushover.svg -- 3.24kb -> 3.16kb (2.44%)
/src/assets/useradd.svg -- 0.28kb -> 0.28kb (2.41%)
/src/assets/download.svg -- 0.32kb -> 0.31kb (2.15%)
/src/assets/ellipsis.svg -- 0.29kb -> 0.28kb (2.05%)
/src/assets/extlogos/slack.svg -- 1.15kb -> 1.15kb (0.59%)
/src/assets/available.svg -- 0.73kb -> 0.73kb (0.53%)
/src/assets/extlogos/discord_white.svg -- 1.30kb -> 1.29kb (0.53%)
/src/assets/requested.svg -- 0.94kb -> 0.94kb (0.52%)
/src/assets/extlogos/telegram.svg -- 0.66kb -> 0.66kb (0.44%)
/src/assets/unavailable.svg -- 1.19kb -> 1.18kb (0.41%)
/src/assets/rt_fresh.svg -- 1.22kb -> 1.22kb (0.08%)
/src/assets/services/tmdb.svg -- 2.52kb -> 2.51kb (0.08%)
/src/assets/rt_rotten.svg -- 1.43kb -> 1.43kb (0.07%)
/src/assets/rt_aud_fresh.svg -- 2.60kb -> 2.60kb (0.04%)
/src/assets/services/rt.svg -- 9.41kb -> 9.41kb (0.01%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>

Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: sct <ryan@sct.dev>
2021-01-21 19:00:57 +09:00
sct
421f4c17f0 fix(api): prevent duplicate movie requests
fixes #705
2021-01-21 09:37:47 +00:00
Mārtiņš Možeiko
e97228899a fix: lookup movie by imdbid if tmdbid does not exits for plex movie agent (#711) 2021-01-21 13:27:24 +09:00
allcontributors[bot]
2bfab5e99c docs: add Tamberlox as a contributor (#702) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-20 20:39:57 +09:00
allcontributors[bot]
0483becbfa docs: add chriscpritchard as a contributor (#700) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-20 19:29:03 +09:00
Chris Pritchard
3a1f6d5706 fix(build): fix sqlite3 build error (#691)
updated package.json and yarn.lock to include a version of node-gyp that works with python3 instead
of python2 for node-sqlite3
2021-01-20 19:28:10 +09:00
sct
6c1742e94c feat(frontend): add option to hide all available items from discovery (#699) 2021-01-20 18:56:45 +09:00
semantic-release-bot
6167011ee5 chore(release): 1.17.2 2021-01-20 07:27:33 +00:00
sct
bc399d95a0 Merge branch 'develop' 2021-01-20 07:21:48 +00:00
sct
e1032ff5df fix(requests): allow declined season requests to be re-requested
fixes #690
2021-01-20 05:23:42 +00:00
samwiseg0
e80920faa1 Add skip ci to discord notifications (#696) 2021-01-20 11:13:24 +09:00
samwiseg0
f7c0769deb build(release): Add snap package to the release workflow [skip ci] (#692)
* build(discord): Always send discord notification

* build(snap): Remove trigger for tags

* build(release): Add snap build to release workflow

* Remove pull request if from discord snap
2021-01-20 10:40:15 +09:00
sct
78444a9e64 fix(requests): update requests to approved when parent media is set as available
fixes #688
2021-01-19 15:10:15 +00:00
semantic-release-bot
519dd17d37 chore(release): 1.17.1 2021-01-19 10:24:57 +00:00
sct
0761593411 Merge branch 'develop' 2021-01-19 10:18:51 +00:00
sct
8927c6d2e3 fix(frontend): show auto approval on series request modal only with correct permissions
fixes #687
2021-01-19 10:17:36 +00:00
semantic-release-bot
caa45415b1 chore(release): 1.17.0 2021-01-19 07:58:37 +00:00
sct
81a96db2e7 ci: fix release discord notification [skip ci] 2021-01-19 07:54:55 +00:00
sct
ed297d9d6c Merge branch 'develop' 2021-01-19 07:52:47 +00:00
sct
686c4f71bf feat(lang): add support for Chinese (Traditional) language 2021-01-19 02:17:14 +00:00
Weblate (bot)
801e76524d feat(lang): Translations update from Weblate (#604)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (512 of 512 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (489 of 489 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (472 of 472 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (454 of 454 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (513 of 513 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (512 of 512 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (512 of 512 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (489 of 489 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.1% (485 of 489 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 93.5% (497 of 531 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 94.5% (484 of 512 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 58.9% (302 of 512 strings)

feat(lang): translated using Weblate (Chinese (Traditional))

Currently translated at 56.2% (288 of 512 strings)

feat(lang): added translation using Weblate (Chinese (Traditional))

Co-authored-by: Doris Houng <doris.houng@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/zh_Hant/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Swedish)

Currently translated at 96.9% (474 of 489 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: sebstrgg <sebastian@wollter.nu>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 96.6% (496 of 513 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 88.6% (454 of 512 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 92.8% (454 of 489 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (454 of 454 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (531 of 531 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (454 of 454 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (531 of 531 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (513 of 513 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (513 of 513 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (512 of 512 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.6% (510 of 512 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (512 of 512 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (489 of 489 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (489 of 489 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (489 of 489 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (454 of 454 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul Hagedorn <panzer1119@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (533 of 533 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (531 of 531 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 99.8% (530 of 531 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (513 of 513 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (512 of 512 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (512 of 512 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (489 of 489 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (489 of 489 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (454 of 454 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Russian)

Currently translated at 19.1% (87 of 454 strings)

Co-authored-by: David <david@hmnd.io>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (English)

Currently translated at 100.0% (512 of 512 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheCatLady <doris.houng@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/en/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Co-authored-by: Doris Houng <doris.houng@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: sebstrgg <sebastian@wollter.nu>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: Paul Hagedorn <panzer1119@gmail.com>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Co-authored-by: David <david@hmnd.io>
2021-01-19 11:12:05 +09:00
sct
a9eeda17a2 docs: update README with newer app screenshot [skip ci] 2021-01-19 02:04:19 +00:00
samwiseg0
f2b9125acd build(workflows): Update workflows to use ubuntu-20.04 (#684)
Added discord notifications to all build jobs
2021-01-18 20:17:24 -05:00
allcontributors[bot]
3b4892bcd7 docs: add TheCatLady as a contributor (#683) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-19 09:47:28 +09:00
TheCatLady
192cfd8a8e feat(api): /request/count endpoint (#682) 2021-01-19 09:33:30 +09:00
Roxedus
70229d12ab docs(reverse-proxy): Add note for bundled sample SWAG (#681) [skip ci]
Co-authored-by: samwiseg0 <2241731+samwiseg0@users.noreply.github.com>
2021-01-18 17:40:19 -05:00
sct
eb6fc8a190 fix(notifications): send media declined email
fixes #679
2021-01-18 22:11:28 +00:00
sct
2f97f61a6e feat(notifications): add notification for declined requests
closes #663
2021-01-18 06:34:20 +00:00
samwiseg0
4fe8172eb1 build(snapcraft): Fix arm build by using py2 (#671) 2021-01-18 14:31:44 +09:00
samwiseg0
c8a8de38cc build(snapcraft): Add snapcraft build yaml and workflow (#608) 2021-01-18 12:58:14 +09:00
sct
00944b1ec2 fix(frontend): do not show failed media status on request list for declined requests
fixes #664
2021-01-18 02:55:09 +00:00
sct
7db62ab824 fix(api): improve rottentomatoes rating matching for movies
fixes #494
2021-01-18 02:47:12 +00:00
sct
860d71ed69 fix(frontend): clarify which fields are required in radarr/sonarr modals
closes #575
2021-01-18 02:40:03 +00:00
sct
51d8fba916 fix(server): support absolute paths for CONFIG_DIRECTORY 2021-01-18 01:25:44 +00:00
sct
fa8f112c31 feat(server): add CONFIG_DIRECTORY env var to control config directory location 2021-01-18 01:15:08 +00:00
sct
b0ce0406bb chore: remove old unused logo.svg [skip ci] 2021-01-18 01:05:50 +00:00
sct
7fe80cd4ad build(deps): batch update dependencies 2021-01-18 01:00:12 +00:00
allcontributors[bot]
d8a402aed2 docs: add hirenshah as a contributor (#668) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-18 09:49:11 +09:00
hirenshah
03c690b00b docs: Update LE/NGINX to remove CSP [skip ci] 2021-01-17 12:13:38 -05:00
sct
bdb33722e6 feat(requests): Request Overrides & Request Editing (#653) 2021-01-17 22:52:50 +09:00
sct
d9919abb89 fix(lang): add missing see more i18n string for SeeMoreCard 2021-01-17 08:24:47 +00:00
sct
587e8db15e feat(frontend): add see more card to media sliders 2021-01-17 08:23:34 +00:00
sct
a77a2aa3eb fix(lang): change email auth user/pass strings to SMTP Username/Password 2021-01-17 06:38:40 +00:00
sct
5cfdfac37b refactor(frontend): move discovery sliders into reusable MediaSlider component 2021-01-15 04:14:05 +00:00
Jakob Ankarhem
b04d00ef50 fix(user edit): fix user edit not being able to be saved (#651)
Co-authored-by: sct <sctsnipe@gmail.com>
2021-01-15 12:05:58 +09:00
sct
23624bd144 fix(build): remove cross import from client to server for UserType 2021-01-14 13:08:48 +00:00
Jakob Ankarhem
492e19df40 feat(login): add local users functionality (#591) 2021-01-14 21:03:12 +09:00
sct
f17fa2a2db fix(notifications): correctly compare seasons before sending series notifications 2021-01-13 12:24:08 +00:00
sct
d688a96759 fix(plex-sync): store plex added date and sort recently added by it 2021-01-13 09:51:11 +00:00
sct
a262727078 fix(frontend): fix request button height 2021-01-12 23:39:39 +00:00
sct
29c5bc4097 feat(frontend): add template variable help button to custom webhook settings page 2021-01-12 13:22:07 +00:00
sct
3e476868b1 docs: notification agent docs added 2021-01-12 22:14:59 +09:00
sct
d9e0c90e76 fix(frontend): fix button styling on details page on small screen sizes 2021-01-12 12:25:57 +00:00
sct
a740b07f06 fix(plex-sync): improve plex sync error handling. add session id to fix stuck runs 2021-01-12 11:53:35 +00:00
sct
a7cc7c5975 feat(notifications): Webhook Notifications (#632) 2021-01-12 18:28:42 +09:00
sct
1aa0005b42 fix(frontend): show correct request status on request cards for 4k requests 2021-01-11 16:02:08 +00:00
sct
fc6f7ccea5 fix(notifications): only send one available notification for standard media 2021-01-11 15:45:28 +00:00
sct
e0d9f891e7 fix(requests): select the correct radarr/sonarr server when sending request to service 2021-01-11 15:32:59 +00:00
sct
b491be1b1e fix(frontend): request dropdown menu now properly shows up over collection button 2021-01-11 15:23:24 +00:00
sct
6b2df24a2e feat: 4K Requests (#559) 2021-01-11 23:42:33 +09:00
sct
79629645aa revert(deps): revert back to next@10.0.3 until sharp optional dependency bug is fixed
see: https://github.com/vercel/next.js/issues/20456
2021-01-08 14:13:35 +00:00
Mārtiņš Možeiko
0600ac7c3a feat: map AniDB IDs from Hama agent to tvdb/tmdb/imdb IDs (#538)
* feat: map AniDB IDs from Hama agent to tvdb/tmdb/imdb IDs

re #453

* refactor: removes sync job for AnimeList, load mapping on demand

* refactor: addressing review comments, using typescript types for xml parsing

* refactor: make sure sync job does not update create same tvshow/movie twice

Hama agent can have same tvdbid it for different library items - for example
when user stores different seasons for same tv show separately. This change
adds "AsyncLock" that guarantees code in callback runs for same id fully,
before running same callback next time.

* refactor: do not use season 0 tvdbid for tvshow from mapping file

* refactor: support multiple imdb mappings for same anidb entry

* refactor: add debug log for missing tvdb entries in tmdb lookups from anidb/hama agent
2021-01-08 09:46:00 +09:00
semantic-release-bot
c6486c3643 chore(release): 1.16.0 2021-01-07 09:58:09 +00:00
sct
f9695068a8 Merge branch 'develop' 2021-01-07 09:53:04 +00:00
Weblate (bot)
2bfe0f2bf6 feat(lang): Translations update from Weblate (#580)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (433 of 433 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (454 of 454 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (433 of 433 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (454 of 454 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (433 of 433 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (454 of 454 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (454 of 454 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (433 of 433 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul <panzer1119@gmail.com>
Co-authored-by: cyb3rgh05t <flammang.yves@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 98.2% (446 of 454 strings)

Co-authored-by: Jos Vennik <josvennik@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul <panzer1119@gmail.com>
Co-authored-by: cyb3rgh05t <flammang.yves@gmail.com>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
2021-01-07 18:50:51 +09:00
sct
493d82b6b0 fix(plex-sync): do not run plex sync if no admin exists 2021-01-06 00:55:53 +00:00
sct
e2c2172ac6 build(deps): batch update dependencies 2021-01-05 15:17:46 +00:00
Jakob Ankarhem
ee5d0181fc feat(notifications): add pushover integration (#574)
* feat(notifications): add pushover integration

* refactor(pushover): group i18n translations
2021-01-05 13:19:25 +09:00
sct
67f2b57f00 fix(frontend): apply same titlecard hover effect to personcard 2021-01-04 16:40:39 +00:00
sct
88810bf0a4 fix(frontend): use hardware acceleration for titlecard scale 2021-01-04 16:09:09 +00:00
sct
0ab4c3c36f fix(frontend): only animate titlecard when showDetail is true 2021-01-04 15:33:07 +00:00
sct
effc80977a fix(frontend): adjust titlecard badge styling
this commit aligns the badges, removes the border from the availability badge (replaces it with a
light shadow) and also adds a new animation to the titlecard
2021-01-04 15:21:46 +00:00
semantic-release-bot
fd90adba4d chore(release): 1.15.0 2021-01-04 12:01:25 +00:00
sct
970aa612fe Merge pull request #570 from sct/release/release-20210104
Release
2021-01-04 20:56:34 +09:00
sct
71169944dc Merge branch 'develop' into release/release-20210104 2021-01-04 11:54:45 +00:00
Weblate (bot)
b04eda6c8a feat(lang): translations update from Weblate (#495)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (418 of 418 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 93.7% (391 of 417 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 95.3% (390 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (422 of 422 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (418 of 418 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (409 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Swedish)

Currently translated at 95.1% (389 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jakob Ankarhem <jakob.ankarhem@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (422 of 422 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (418 of 418 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (409 of 409 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (409 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (409 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (422 of 422 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (418 of 418 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (409 of 409 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (409 of 409 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (409 of 409 strings)

feat(lang): translated using Weblate (German)

Currently translated at 98.7% (404 of 409 strings)

Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Paul <panzer1119@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (432 of 432 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 96.9% (419 of 432 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 79.8% (345 of 432 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 99.5% (407 of 409 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakob Ankarhem <jakob.ankarhem@outlook.com>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: Ben Wallner <ben.david.wallner@gmail.com>
Co-authored-by: Paul <panzer1119@gmail.com>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: Kobe <kobe.boecquaert@gmail.com>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
2021-01-04 20:41:22 +09:00
sct
5add44cfb0 feat(frontend): filter/sorting for request list
closes #431
2021-01-03 16:02:34 +00:00
sct
a524b9c4c8 fix(frontend): add i18n for request text on titlecard 2021-01-03 14:38:05 +00:00
sct
b5a3a7a89f fix(frontend): change titlecard to only have a request button 2021-01-03 14:03:14 +00:00
sct
67ac9e075f fix(frontend): fix sliders overflowing on firefox
fixes #566
2021-01-03 13:50:28 +00:00
sct
ce0687922a fix(frontend): disable pointer-events on titlecard badges 2021-01-02 15:38:36 +00:00
sct
ba106c447d fix(frontend): increase titlecard status badge size on larger screens 2021-01-02 15:24:25 +00:00
sct
8cbf39a9d1 fix(frontend): show movie/series badges always 2021-01-02 14:40:47 +00:00
sct
78b15cad18 refactor(frontend): titlecard design experiment 2021-01-02 14:06:41 +00:00
semantic-release-bot
d77f9eb44e chore(release): 1.14.1 2021-01-02 08:57:24 +00:00
sct
8c0903393c fix(holiday): remove special holiday slider 2021-01-02 08:51:37 +00:00
sct
22f2037ea6 fix(holiday): remove special holiday slider 2021-01-02 08:33:24 +00:00
samwiseg0
3e613a3e93 docs: Update Ubuntu install instructions [skip ci] (#558)
* docs: Update Ubuntu install instructions [skip ci]

* docs: fix updating spacing [skip ci]

Co-authored-by: sct <ryan@sct.dev>
2021-01-02 16:59:38 +09:00
sct
5c847026aa fix(api): return 202 when same seasons are requested again 2021-01-02 07:48:43 +00:00
Chris Pritchard
2f017fe2cb docs: Added installation instructions for gentoo (#556) [skip ci] 2021-01-01 21:32:53 -05:00
flying-sausages
7d95fd0f30 docs: Add swizzin install instructions (#552) [skip ci] 2021-01-01 17:15:43 -05:00
flying-sausages
28b1d8cf4c docs: Update Linux instructions #554 [skip ci] 2021-01-01 17:05:03 -05:00
allcontributors[bot]
9d6970db57 docs: add flying-sausages as a contributor (#551) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-02 01:37:23 +09:00
flying-sausages
f906711ee8 docs: Add native linux install instructions (#550) [skip ci]
* Add native instructions

* formatting

* Update docs/getting-started/installation.md

* Add untested upgrade instructions
2021-01-02 01:35:29 +09:00
sct
93b57a76f1 fix(frontend): handle currentLibrary possibly being null on first manual sync 2020-12-31 05:05:16 +00:00
Brandon Cohen
12db7a065a fix(frontend): full season request modal fits on a smaller mobile UI (#535)
* fix(frontend): full season request modal fits on a smaller mobile UI

* fix(frontend): added responsive variant and removed unnecessary padding

* fix(frontend): added in responsive design

* fix(frontend): applied the same spacing from the discovery page to the requests/users pages

* fix(frontend): revered change to keep tables edge to edge on mobile
2020-12-31 13:57:31 +09:00
sct
058fb65495 fix(frontend): update login/setup images 2020-12-29 11:25:55 +00:00
sct
721ed9a930 fix: correctly deal with tmdb id duplicates between movies/series
fixes #526
2020-12-29 10:26:37 +00:00
Danshil Mungur
5295f74915 docs: added reminder to check default user permissions (#527) [skip ci] 2020-12-28 19:17:29 -05:00
Danshil Mungur
7d6c6cb2a1 docs: removed duplicate page (#522) [skip ci]
* docs(reverse-proxy-examples): removed duplicate text in hint

* docs(extending-overseerr): removed duplicate page
2020-12-29 08:17:56 +09:00
Danshil Mungur
d53d61014f docs(reverse-proxy-examples): removed duplicate text in hint (#520) [skip ci] 2020-12-29 02:25:09 +09:00
sct
7c40d9131d docs: update introduction (#519)
* docs: update docs introduction

* docs: fix link in faq
2020-12-28 20:44:08 +09:00
sct
f124d732a2 fix(frontend): fix count of requests in request list 2020-12-28 11:27:24 +00:00
sct
8af6a1f566 feat(notifications): control notifcation types per agent
closes #513
2020-12-28 02:21:45 +00:00
sct
d00e470b55 fix: use new commit tag file for app version as well 2020-12-27 17:22:21 +00:00
sct
55f9e41f1b build: write commit tag to json file inside docker image 2020-12-27 17:08:25 +00:00
sct
343f466788 fix(frontend): search clear button now correctly triggers routing 2020-12-27 15:31:18 +00:00
sct
fae4818957 fix(frontend): set locale cookie expiration to be much longer 2020-12-27 14:47:39 +00:00
sct
d188f6ffad fix(frontend): combine duplicate credits on a persons detail page
fixes #504
2020-12-27 12:14:47 +00:00
sct
289864af1a fix(build): fixes build to include commit tag for app build step 2020-12-27 10:33:44 +00:00
sct
d2620281bf GitBook: [develop] 5 pages modified 2020-12-27 10:28:50 +00:00
sct
30ed82e155 docs: documentation update (#516) [skip ci]
* docs: testing docs with gitbook

* GitBook: [docs] 3 pages modified

* GitBook: [docs] one page modified

* docs: migrate more wiki docs

* docs: Update gmail. Add 4k FAQ

* GitBook: [docs] 6 pages modified

* docs: add media missing in overseerr to faq

* GitBook: [docs] one page modified

* GitBook: [docs] 5 pages modified

* GitBook: [docs] 2 pages modified

* GitBook: [docs] 5 pages modified

* GitBook: [docs] 5 pages modified

* GitBook: [docs] 5 pages modified

* GitBook: [docs] one page modified

* GitBook: [docs] 2 pages modified

Co-authored-by: samwiseg0 <2241731+samwiseg0@users.noreply.github.com>
Co-authored-by: samwiseg0 <phillipah@gmail.com>
2020-12-27 19:27:03 +09:00
sct
c034496f55 feat(frontend): add documentation link to about page
Also updates the README with new documentation links
2020-12-27 10:23:10 +00:00
sct
2dcda39d40 fix(frontend): add localized strings for status checker 2020-12-27 10:12:06 +00:00
sct
75a426437a feat: status checker to prompt users to reload their frontend when app version changes 2020-12-27 10:05:54 +00:00
Danshil Mungur
1438b08cf0 fix(frontend): center text in movie auto-approve modal on small screens (#510)
fix #507
2020-12-27 18:20:58 +09:00
Jakob Ankarhem
0185bb1a70 feat(email): add sendername to email notification (#506)
* feat(email): add sendername to email notification

* feat(email): add sendername to api + move field in form

Co-authored-by: Jakob Ankarhem <jakob.ankarhem@jetshop.se>
2020-12-27 00:54:23 +09:00
Jakob Ankarhem
c8d4d674f4 feat(frontend): add telegram integration (#491)
* feat(frontend): add telegram notification agent

* feat(telegram): add i18n keys for telegram

* style(telegram): change message formatting in notification

* feat(telegram): add short tutorial for telegram setup

* feat(telegram): add i18n keys for telegram tutorial

* style(telegram): correct grammar in infobox

Co-authored-by: sct <ryan@sct.dev>

* fix(telegram): redo i18n extraction

Co-authored-by: Jakob Ankarhem <jakob.ankarhem@jetshop.se>
Co-authored-by: sct <ryan@sct.dev>
2020-12-27 00:02:00 +09:00
Jakob Ankarhem
7434a26f76 feat(frontend): add clear-field-icon to search field (#498)
* feat(frontend): add button to clear search field

Clear input field button was not visible on all devices, this replaces native ones with an svg

* refactor(search): use tailwind css for button and change svg

* refactor(search): larger click area on reset button

Co-authored-by: Jakob Ankarhem <jakob.ankarhem@jetshop.se>
2020-12-26 23:43:22 +09:00
allcontributors[bot]
46af705014 docs: add danshilm as a contributor (#505) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-26 18:22:28 +09:00
Danshil Mungur
2b27a715b0 fix(docs): fix typo in build instructions (#503) 2020-12-26 18:21:46 +09:00
semantic-release-bot
2cdf755c20 chore(release): 1.14.0 2020-12-25 11:52:44 +00:00
sct
f99ab47c01 Merge branch 'develop' 2020-12-25 20:47:19 +09:00
Weblate (bot)
c8c74b0ae5 feat(lang): translations update from Weblate (#479)
* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (390 of 390 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (389 of 389 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (390 of 390 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (389 of 389 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (386 of 386 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (386 of 386 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (393 of 393 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (390 of 390 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (390 of 390 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (390 of 390 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (389 of 389 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (377 of 377 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul <panzer1119@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 100.0% (390 of 390 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 97.4% (376 of 386 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: Paul <panzer1119@gmail.com>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
2020-12-25 20:38:32 +09:00
sct
de3d288949 fix(frontend): fix missing styles for alert component
also adds relative release time to releases in About page
2020-12-25 20:27:19 +09:00
sct
6353cda582 fix(frontend): fix mobile dropdown in notifications settings 2020-12-25 20:01:35 +09:00
sct
1163e81adc feat(notifications): add slack notification agent
re #365
2020-12-25 19:02:04 +09:00
sct
5d06a34731 fix(frontend): remove vote permission for now
currently unsused so no point in showing it currnetly
2020-12-25 10:25:06 +09:00
sct
8f292d538b fix(frontend): update titlecard status badge to new requested colors 2020-12-25 10:10:27 +09:00
sct
4809257816 feat: add separate auto approve permissions for Movies/Series
closes #268
2020-12-25 09:58:29 +09:00
johnpyp
02969d5426 feat: simple failed request handling (#474)
When a movie or series is added with radarr or sonarr, if it fails, this changes the media state to
unknown and sends a notification to admins. Client side this will look like a failed state along
with a retry button that will delete the request and re-queue it.
2020-12-25 09:53:32 +09:00
sct
ed94a0f335 fix(frontend): status badge Unavailable renamed to Requested
closes #374
2020-12-24 23:42:12 +09:00
sct
395cbb2be6 fix(logs): rotate logs on a daily basis instead of incrementing log filename 2020-12-24 23:05:05 +09:00
sct
bd947409e6 fix(frontend): fix scaling titlecard content position 2020-12-24 21:46:03 +09:00
sct
06fc98b6b9 fix(frontend): add margin to ButtonWithDropdown component on movie/tv details page 2020-12-24 21:28:33 +09:00
sct
d7b1c28406 fix(frontend): improve flex header on movie/tv details page 2020-12-24 21:17:21 +09:00
allcontributors[bot]
3438045e51 docs: add ankarhem as a contributor (#487) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-24 21:01:08 +09:00
Jakob Ankarhem
4b7af86111 fix(frontend): correctly position title card hover section (#486)
f2ebba7 introduced an issue where the children of the animation would not be visible during the
animation

Co-authored-by: Jakob Ankarhem <jakob.ankarhem@jetshop.se>
2020-12-24 21:00:20 +09:00
allcontributors[bot]
dd22d158ff docs: add jayesh100 as a contributor (#485) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-24 20:42:35 +09:00
Jayesh
e88dc83aeb feat: YouTube Movie/TV Trailers (#454)
* feat: Get Youtube trailers from TMDB API and show on Movie/TV details page

* docs(overseerr-api.yml): remove youtube trailer URL (unused) from OAS
2020-12-24 20:41:32 +09:00
sct
329a814a8f fix: improve apple-touch-icon and android app icons 2020-12-24 19:25:56 +09:00
sct
a333a09582 feat: add collections (#484)
closes #418
2020-12-24 16:31:56 +09:00
Alex Zoitos
f2ebba7b1d fix(frontend): invalid dom-nesting title card fix (#482)
fix #476
2020-12-24 11:37:11 +09:00
sct
17518dbe7f fix(frontend): sort person detail credits by tmdb votes 2020-12-24 10:33:54 +09:00
allcontributors[bot]
2350f3ffd8 docs: add ankarhem as a contributor (#481) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-24 10:33:17 +09:00
Jakob Ankarhem
3dced2065f docs(contributing.md): improve structure of Getting started and add section about rebasing (#480)
Co-authored-by: Jakob Ankarhem <jakob.ankarhem@jetshop.se>
2020-12-24 10:32:45 +09:00
semantic-release-bot
7e4175ff21 chore(release): 1.13.0 2020-12-23 12:44:01 +00:00
sct
4157432aa7 Merge branch 'develop' 2020-12-23 21:39:00 +09:00
Weblate (bot)
941fe19904 feat(lang): translations update from Weblate (#410)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (364 of 365 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 81.8% (297 of 363 strings)

feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 73.0% (265 of 363 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (377 of 377 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (French)

Currently translated at 99.7% (368 of 369 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (366 of 366 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (365 of 365 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (363 of 363 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.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 97.8% (366 of 374 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (365 of 365 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (363 of 363 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: sebstrgg <sebastian@wollter.nu>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 99.7% (376 of 377 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 99.7% (368 of 369 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (366 of 366 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (365 of 365 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (363 of 363 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (363 of 363 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (366 of 366 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (377 of 377 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (374 of 374 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.7% (368 of 369 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (366 of 366 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (365 of 365 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (363 of 363 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 93.4% (341 of 365 strings)

Co-authored-by: BodingClockchian <joost_nl@live.nl>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 98.1% (367 of 374 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 100.0% (366 of 366 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 95.9% (351 of 366 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Co-authored-by: sebstrgg <sebastian@wollter.nu>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: BodingClockchian <joost_nl@live.nl>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
2020-12-23 21:38:11 +09:00
sct
2e0e4d5129 fix(frontend): filter out undefined backdrop paths for person details page 2020-12-23 17:18:00 +09:00
sct
9f5f920c23 fix(frontend): show backdrops instead of posters for new person detail design 2020-12-23 16:14:39 +09:00
sct
1fb7ea7258 feat(frontend): adjust person details design and add improved truncate 2020-12-23 16:02:55 +09:00
Brandon Cohen
3bbc716434 fix(frontend): added new Radarr v3 logo (#471) 2020-12-23 13:05:40 +09:00
sct
6898357b13 feat(email): option to allow self signed certificates 2020-12-23 12:18:40 +09:00
Brandon Cohen
a7db01fba4 feat(frontend): first air date added to TV details page (#470) 2020-12-23 12:15:05 +09:00
sct
0c8a180189 fix: clarify full sync runs every 24 hours 2020-12-23 01:40:46 +09:00
sct
0980fa54f9 fix(email): correctly log errors when emails fail to send 2020-12-23 01:34:29 +09:00
sct
32b4c99950 fix(api): correctly return firstAirDate for series in search endpoints
fixes #462
2020-12-23 01:13:29 +09:00
sct
22002ab4c7 feat(logs): rotate log files if they reach 20MB in size
closes #438
2020-12-23 00:58:33 +09:00
sct
659fa505f0 feat(notifications): include direct links to media in notifications
closes #437
2020-12-23 00:25:44 +09:00
sct
4205e32ae7 fix(plex-sync): match correct tmdb format for movies 2020-12-22 22:37:18 +09:00
sct
ffe9e19c3b feat(plex-sync): add support for hama guid's
re #453
2020-12-22 13:45:04 +09:00
Brandon Cohen
66ef72dd42 fix(frontend): approve and decline button (in manage panel) will now fit on mobile (#441) 2020-12-22 13:19:06 +09:00
samwiseg0
1056d28a26 docs: Update templates with new labels [skip ci] (#452)
* docs: Update bug template with new label [skip ci]

* docs: Update feature request template with new label [skip ci]
2020-12-22 12:50:09 +09:00
semantic-release-bot
741b2c4e9f chore(release): 1.12.1 2020-12-22 03:17:58 +00:00
sct
6dbe9ea814 Merge branch 'develop' 2020-12-22 12:12:25 +09:00
sct
69fd7a5511 fix(migration): fixes issue migrating away from the unique imdbId constraint 2020-12-22 12:10:57 +09:00
semantic-release-bot
96328ab5c6 chore(release): 1.12.0 2020-12-22 02:26:31 +00:00
sct
c31709681a Merge branch 'develop' 2020-12-22 11:21:19 +09:00
sct
4b505223b8 fix(logs): improve logging when adding to sonarr/radarr 2020-12-22 11:12:41 +09:00
sct
16e48c4919 build: remove yml from dockerignore [skip ci] 2020-12-22 09:46:22 +09:00
sct
604ba2a92f feat(frontend): add full crew page for movies/shows 2020-12-22 00:37:26 +09:00
sct
12127a7763 feat(frontend): add crew related movies/shows to person details page 2020-12-22 00:12:50 +09:00
allcontributors[bot]
9a4da25faa docs: add johnpyp as a contributor (#435) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-21 23:42:25 +09:00
johnpyp
7e8f361af7 feat: import users from plex (#428)
* feat: import users from plex

fix #281

* fix(frontend): re-enable delete user confirmation button after finished
2020-12-21 23:41:19 +09:00
allcontributors[bot]
08c22b03e0 docs: add doob187 as a contributor (#434) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-21 23:12:39 +09:00
doob187
3cee6b78a4 build: Update Docker ignore (#425)
Add some files to ignore.
Inside the Docker  ( latest dev Build )
```
CODE_OF_CONDUCT.md
CONTRIBUTING.md
Dockerfile
Dockerfile.local
LICENSE
babel.config.js
config
dist
docker-compose.yml
next-env.d.ts
next.config.js
node_modules
ormconfig.js
overseerr-api.yml
package.json
postcss.config.js
public
server
src
stylelint.config.js
tailwind.config.js
tsconfig.json
yarn.lock
```
2020-12-21 23:07:54 +09:00
sct
7083ddf181 revert(deps): revert react-use-clipboard to 1.0.2 2020-12-21 21:25:14 +09:00
sct
b092361c73 build(deps): more bundled dependency updates 2020-12-21 21:15:15 +09:00
sct
6f2a71c788 build(deps-dev): bundled dependency update 2020-12-21 21:08:46 +09:00
sct
22006e9dbd fix(api): prevent checking first admin account for plex server access 2020-12-21 20:51:54 +09:00
sct
e7ee85c29b feat: default user permissions added to settings
closes #388
2020-12-21 18:47:55 +09:00
sct
f35dae56a5 fix(api): fix cross-imported type crashing build 2020-12-21 18:14:49 +09:00
sct
5deb64a87f fix(frontend): allow more special characters in search input
fixes #430
2020-12-21 18:02:23 +09:00
sct
6753d9daaa feat(frontend): add prioritized crew under overview
closes #406
2020-12-21 17:37:38 +09:00
sct
4891298891 fix(frontend): adjust person card layout to deal with overflowing content
fixes #416
2020-12-21 15:45:55 +09:00
sct
ab9cef3624 fix: only run migrations in production 2020-12-21 14:38:32 +09:00
allcontributors[bot]
d4d79e438c docs: add danshilm as a contributor (#429) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-21 14:37:26 +09:00
Danshil Mungur
c6836e02c8 fix(frontend): add name, short_name and start_url to manifest (#424) 2020-12-21 14:36:11 +09:00
sct
0916b58594 fix(notifications): always update the media table when seasons become available 2020-12-21 01:40:23 +09:00
sct
44a305426f feat(notifications): added ability to send test notifications
closes #309
2020-12-20 21:41:25 +09:00
sct
764db94f1b fix(plex-sync): fixes processing movies using TMDB agent
fix #363
2020-12-20 11:00:48 +09:00
semantic-release-bot
0a3d7c5915 chore(release): 1.11.0 2020-12-20 01:18:39 +00:00
sct
912c63e562 Merge branch 'develop' 2020-12-20 10:12:59 +09:00
allcontributors[bot]
f6865ca0c1 docs: add sebstrgg as a contributor (#407) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-20 01:45:47 +09:00
sct
c9fe6cb0b7 feat(lang): add support for swedish language 2020-12-20 01:43:54 +09:00
Weblate (bot)
3778ad829c feat(lang): translations update from Weblate (#403)
* feat(lang): translated using Weblate (Swedish)

Currently translated at 100.0% (350 of 350 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 75.7% (265 of 350 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 75.7% (265 of 350 strings)

feat(lang): translated using Weblate (Swedish)

Currently translated at 72.5% (254 of 350 strings)

feat(lang): added translation using Weblate (Swedish)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: sebstrgg <sebastian@wollter.nu>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sv/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (350 of 350 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (350 of 350 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: sebstrgg <sebastian@wollter.nu>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
2020-12-20 01:40:56 +09:00
sct
b1786245c7 ci: change semantic-release/github to add releases to bottom [skip ci] 2020-12-20 01:34:50 +09:00
sct
b7f5739035 feat(frontend): releases added to about page
closes #303
2020-12-20 01:22:58 +09:00
sct
c128898206 feat(frontend): add support overseerr block to about page 2020-12-19 23:30:27 +09:00
sct
ff2ab29491 feat(frontend): add language picker to setup/login 2020-12-19 23:13:46 +09:00
sct
108dfc4afd feat(lang): add support for Italian, Portuguese (Brazil) and Serbian 2020-12-19 21:51:05 +09:00
allcontributors[bot]
6f2de8577e docs: add Shagon94 as a contributor (#401) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-19 21:31:51 +09:00
Weblate (bot)
1bd0e646e3 feat(lang): translations update from Weblate (#400)
* feat(lang): translated using Weblate (French)

Currently translated at 96.5% (338 of 350 strings)

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Serbian)

Currently translated at 100.0% (350 of 350 strings)

feat(lang): added translation using Weblate (Serbian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shagon94 <Shagon94@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/sr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 62.8% (220 of 350 strings)

Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (350 of 350 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 100.0% (350 of 350 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Spanish)

Currently translated at 93.7% (328 of 350 strings)

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 96.2% (337 of 350 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 95.7% (335 of 350 strings)

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Russian)

Currently translated at 23.4% (82 of 350 strings)

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 99.4% (348 of 350 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 99.4% (348 of 350 strings)

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (English)

Currently translated at 100.0% (350 of 350 strings)

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/en/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Shagon94 <Shagon94@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
2020-12-19 21:31:07 +09:00
semantic-release-bot
077513151e chore(release): 1.10.0 2020-12-19 04:54:22 +00:00
sct
191f39e8bd Merge branch 'develop' 2020-12-19 13:49:05 +09:00
sct
fb883e8043 Merge branch 'develop' of github.com:sct/overseerr into develop 2020-12-19 13:44:57 +09:00
allcontributors[bot]
4435468561 docs: add Panzer1119 as a contributor (#396) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-19 13:44:48 +09:00
allcontributors[bot]
e821d0a02b docs: add mazzetta86 as a contributor (#395) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-19 13:43:53 +09:00
Weblate (bot)
5f71fb7ee2 feat(lang): translations update from Weblate (#391)
* feat(lang): translated using Weblate (Portuguese (Brazil))

Currently translated at 17.4% (61 of 350 strings)

feat(lang): added translation using Weblate (Portuguese (Brazil))

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/pt_BR/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (350 of 350 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Italian)

Currently translated at 90.8% (318 of 350 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 90.8% (318 of 350 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 46.2% (162 of 350 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 27.4% (96 of 350 strings)

feat(lang): translated using Weblate (Italian)

Currently translated at 27.4% (96 of 350 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (350 of 350 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (350 of 350 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Paul <panzer1119@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Tijuco <linuxinmotion@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: Paul <panzer1119@gmail.com>
2020-12-19 13:42:19 +09:00
sct
a0c5608aa0 fix(frontend): try not to render broken rottentomatoes data 2020-12-19 13:39:48 +09:00
sct
816fec1a83 fix(email): fix link to Overseerr in email templates
fixes #392
2020-12-19 13:23:15 +09:00
sct
2a4dd52275 fix(frontend): hide Request More button if all current seasons are available
fixes #343
2020-12-19 13:16:58 +09:00
sct
3601d442db fix(frontend): adjust padding of search box so placeholder text fits on mobile
this only fixes English. Other languages placeholders may or may not fit into the space

fixes #393
2020-12-19 12:57:17 +09:00
sct
53bede692d feat: add missing tzdata package to image
re #394
2020-12-19 11:32:17 +09:00
sct
fe46895289 ci: add @semantic-release/github step to publish array 2020-12-19 03:09:42 +09:00
sct
a0024a0cbe feat(frontend): add external links to movie and tv detail pages 2020-12-19 02:46:32 +09:00
Weblate (bot)
8408e19568 feat(lang): translations update from Weblate (#380)
* feat(lang): translated using Weblate (Italian)

Currently translated at 2.0% (7 of 350 strings)

feat(lang): added translation using Weblate (Italian)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/it/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (340 of 340 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (340 of 340 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (340 of 340 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: Simone Chiavaccini <mazzetta86@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
2020-12-19 02:01:47 +09:00
sct
ed49e7c28e docs: add FUNDING.yml 2020-12-19 01:55:23 +09:00
Brandon Cohen
549567a7e9 fix(frontend): changed request block for slideover on mobile UI (#387) 2020-12-19 01:55:11 +09:00
sct
51f99a2e7b ci: add missing build-args back into ci workflow 2020-12-19 01:24:50 +09:00
semantic-release-bot
048b6c1d90 chore(release): 1.9.1 2020-12-18 16:08:58 +00:00
sct
a6a065ac0b Merge branch 'develop' 2020-12-19 01:03:48 +09:00
sct
5e5ba40505 fix: change default internal port to 5055 (#389) 2020-12-19 00:12:41 +09:00
semantic-release-bot
abb0b2d763 chore(release): 1.9.0 2020-12-18 12:05:35 +00:00
sct
d2668d3f49 Merge branch 'develop' 2020-12-18 21:00:48 +09:00
ecelebi29
36b2b4fba4 docs: fixed multiple typos, updated latest en.json strings (#371) [skip ci] 2020-12-18 20:45:31 +09:00
sct
6beac736ef feat: api key regeneration 2020-12-18 19:40:21 +09:00
sct
6933b661ca refactor(frontend): remove notification bell from user bar for now 2020-12-18 19:11:39 +09:00
sct
4b6ad8a387 feat(frontend): add studio/networks to movie/tv details
closes #370
2020-12-18 19:01:56 +09:00
sct
992a23222a ci: add @semantic-release/github to release plugins 2020-12-18 18:37:52 +09:00
sct
e75b592bb2 ci: fix image name for ghcr.io tags 2020-12-18 07:29:23 +00:00
sct
baa62b286d ci: push develop images to ghcr.io 2020-12-18 07:04:17 +00:00
sct
908f63557c feat(holiday): special seasonal slider added to discover :) 2020-12-18 06:14:41 +00:00
sct
f88c4a6d4a feat(api): add movie keyword search 2020-12-18 06:13:44 +00:00
sct
0972f40a4e feat: anime profile support (#384)
closes #266
2020-12-18 14:32:40 +09:00
allcontributors[bot]
1f0486eba2 docs: add mmozeiko as a contributor (#382) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-18 11:12:30 +09:00
Mārtiņš Možeiko
086183b563 feat: allow to listen server on specific host interface (#381)
fix #273
2020-12-18 11:11:20 +09:00
samwiseg0
df4ef1fbe5 docs: Remove the issue lock as it will not allow users to edit [skip ci] (#376) 2020-12-18 08:06:14 +09:00
sct
727fa06c18 feat(frontend): added user deletion to the user list
also includes small updates to the api to prevent administrators from being deleted, as well as
migrations to cascade deletions to requests the users made

fixes #348
2020-12-17 16:03:27 +00:00
semantic-release-bot
bee6eb288f chore(release): 1.8.0 2020-12-17 15:54:40 +00:00
sct
3473aed27e Merge branch 'develop' 2020-12-17 15:49:18 +00:00
semantic-release-bot
a9144a2167 chore(release): 1.7.0 2020-12-17 14:09:58 +00:00
sct
59822d6e42 Merge branch 'develop' 2020-12-17 14:05:24 +00:00
Weblate (bot)
ee84f74f8a feat(lang): translations update from Weblate (#336)
* feat(lang): translated using Weblate (Spanish)

Currently translated at 96.4% (323 of 335 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (German)

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (335 of 335 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 95.2% (319 of 335 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 95.2% (319 of 335 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 98.7% (319 of 323 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (323 of 323 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Shutruk <cokimaya007@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
2020-12-17 22:54:41 +09:00
sct
18925decaf fix(frontend): correctly show an unauthorized error when a user fails to login
fixes #322
2020-12-17 12:05:45 +00:00
sct
67146c33ef fix(plex-sync): bundle duplicate ratingKeys to speed up recently added sync
This includes a rewrite to move movie/series availability notifications into a subscriber to prevent
duplicate notifications for series

fix #360
2020-12-17 06:28:03 +00:00
sct
d5eb4d8d43 fix(email): do not pass auth object to transport if no auth data present
re #312
2020-12-17 04:06:45 +00:00
sct
fc12ab84d9 fix(frontend): clarify that radarr/sonnarr servers must be tested before profiles/folders appear
Also blocks "Add Server" or "Save" button until all required fields are entered

fixes #326 and #328
2020-12-17 03:28:49 +00:00
sct
0d088e085e feat(frontend): show alert when there are no default radarr/sonarr servers
fixes #344
2020-12-17 02:08:09 +00:00
sct
2fe53ec5a8 fix(frontend): make minimum availability required for Radarr servers
fixes #345
2020-12-17 01:42:21 +00:00
sct
ce0266f74e fix(frontend): add http/https prefix to hostname fields for plex/radarr/sonarr
fixes #357
2020-12-17 01:36:23 +00:00
sct
c21fa5b535 fix(frontend): fix tv shows failing to open when firstAirDate is undefined
fix #347
2020-12-17 01:14:12 +00:00
allcontributors[bot]
647d5efb09 docs: add ecelebi29 as a contributor (#362) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-17 10:07:40 +09:00
ecelebi29
bb89f679b6 docs(contributing.md, readme.md): fixed 2 small typos (#361) [skip ci] 2020-12-17 10:07:07 +09:00
allcontributors[bot]
3210ac29d0 docs: add ecelebi29 as a contributor (#359) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-17 09:20:28 +09:00
ecelebi29
e9c899ce41 fix(sonarr.ts, mediarequest.ts): add missing seasonFolder option (#358) 2020-12-17 09:18:18 +09:00
semantic-release-bot
0bd1bee1fa chore(release): 1.6.0 2020-12-16 05:56:57 +00:00
sct
46326f9d16 Merge branch 'develop' 2020-12-16 05:52:26 +00:00
allcontributors[bot]
ad18a40969 docs: add samwiseg0 as a contributor (#341) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-16 14:06:25 +09:00
samwiseg0
ae2ede8952 docs: add invalid template workflow (#340) 2020-12-16 13:56:52 +09:00
sct
675060bcdf fix(api): set plex libraries to disabled if the name changes
re #324
2020-12-16 04:07:59 +00:00
sct
f8f388db6d refactor(api): add better error context when plex items fail to sync 2020-12-16 03:57:03 +00:00
sct
2948f9360e feat: add version to startup logs 2020-12-16 03:54:32 +00:00
sct
df4ac8361f fix(frontend): adds a tip to plex setup to clarify that syncing runs in the background
fixes #325
2020-12-16 02:35:15 +00:00
sct
92a874e36d docs: update readme.md with emoji key for contributors 2020-12-16 02:07:49 +00:00
Brandon Cohen
6d7907e844 fix(frontend): close sidebar when clicking outside (#333) 2020-12-16 10:58:41 +09:00
sct
d9788c4aa9 feat(frontend): temporary logs page to clear up confusion about it 404ing
This commit also disables the "Run Now" buttons under Jobs until they actually work

re #272
2020-12-16 01:35:26 +00:00
Brandon Cohen
db0a5c44f6 fix(frontend): aligned movie and tv details (#331) 2020-12-16 09:44:58 +09:00
allcontributors[bot]
c545ec16e7 docs: add kieron as a contributor (#329) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-16 09:29:17 +09:00
Kieron Boswell
961d110720 fix: spelling mistake on the word 'requested' fixed (#319) 2020-12-16 09:28:09 +09:00
sct
e8776fd336 refactor(frontend): titlecard behavior changed to allow clicking anywhere to go through to title
mobile behavior remains mostly the same, except after the first click, a second click anywhere else
will go through to the title.
2020-12-15 15:49:59 +00:00
allcontributors[bot]
7c08809501 docs: add krystiancharubin as a contributor (#311) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-16 00:43:31 +09:00
Krystian Charubin
7fd43c9529 style: dark splash screen (#310) 2020-12-16 00:42:27 +09:00
sct
33f8831e88 fix(api): accept the api key to perform actions on the api with X-API-Key header 2020-12-15 14:13:41 +00:00
sct
20b119c1ac docs: update bug report issue template to include app version [skip ci] 2020-12-15 11:55:24 +00:00
sct
6cd20491d2 feat(lang): add support for Spanish language 2020-12-15 11:46:43 +00:00
allcontributors[bot]
cb8a50bd66 docs: add Shutruk as a contributor (#304) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-15 20:40:15 +09:00
Weblate (bot)
fddbb3cdfe feat(lang): Translations update from Weblate (#291)
* feat(lang): translated using Weblate (Spanish)

Currently translated at 100.0% (321 of 321 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 56.0% (180 of 321 strings)

feat(lang): translated using Weblate (Spanish)

Currently translated at 52.6% (169 of 321 strings)

feat(lang): added translation using Weblate (Spanish)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: TheRaZex <therazex@protonmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/es/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (310 of 310 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (314 of 314 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (310 of 310 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Japanese)

Currently translated at 100.0% (321 of 321 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (310 of 310 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: TheRaZex <therazex@protonmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
2020-12-15 20:39:14 +09:00
sct
01c179f762 fix(api): filter out libraries that do not have any metadata agent or are not movie/show 2020-12-15 10:24:39 +00:00
sct
e08fa35548 fix(api): only run recently added sync on enabled libraries
fixes #259
2020-12-15 10:23:19 +00:00
sct
d96d65b8af refactor(api): increased plex sync speed and changed full sync to run every 24 hours
With this change, plex sync will now process 20 items at a time every 4 seconds. (About double its
previous speed). We will see how much faster we can push this in the future.
2020-12-15 09:16:35 +00:00
semantic-release-bot
54b96cc451 chore(release): 1.5.0 2020-12-15 09:05:37 +00:00
sct
7d2a187865 Merge branch 'develop' 2020-12-15 09:00:44 +00:00
sct
900827be97 feat(lang): add i18n strings for new about page 2020-12-15 08:08:01 +00:00
sct
b9faa6486b fix(api): require package.json directly so typescript doesnt compile it into dist folder 2020-12-15 07:56:53 +00:00
sct
3f2a04c881 feat: about page initial version
Adding something to the about page for now, including the app version to better know what versions
of the app people are running
2020-12-15 07:27:30 +00:00
sct
ff618956b5 fix(frontend): only show alpha notice to admins 2020-12-15 05:15:45 +00:00
sct
b5988f9a5f fix(frontend): add validation for Radarr/Sonarr server name 2020-12-15 03:30:13 +00:00
sct
3ba09d07eb fix: add support for ssl when connecting to plex
fixes #275
2020-12-15 03:22:54 +00:00
Alex Zoitos
f998873fc5 fix(services): improve logging for when Radarr movie already exists (#285)
re #260
2020-12-15 11:11:48 +09:00
allcontributors[bot]
33b54c08b5 docs: add saltydk as a contributor (#286) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-15 11:05:19 +09:00
salty
34136ef617 build(dockerfile): Expose port 3000. (#278)
Allows for automated nginx reverse proxy setups to identify the port used in the container without having to bind the port to the host/localhost.
2020-12-15 10:57:01 +09:00
semantic-release-bot
17df5dd072 chore(release): 1.4.0 2020-12-15 00:53:05 +00:00
sct
4b48a36ad3 Merge branch 'develop' 2020-12-15 00:48:28 +00:00
allcontributors[bot]
2d3359a7e3 docs: add darknessgp as a contributor (#279) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-15 09:15:18 +09:00
darknessgp
6d08b10820 fix: changing parameter name to use correct 'port' #276 (#277)
Co-authored-by: Ryan Boyles <Ryan.Boyles@ahstech.com>
2020-12-15 09:14:11 +09:00
sct
78af1a3e6d fix(services): include radarr/sonarr baseUrl when adding media 2020-12-14 16:39:01 +00:00
semantic-release-bot
16bd3e9fd5 chore(release): 1.3.2 2020-12-14 13:11:15 +00:00
sct
6360c82898 Merge branch 'develop' 2020-12-14 13:06:22 +00:00
allcontributors[bot]
67a8f7fbfe docs: add jvennik as a contributor (#253) [skip ci]
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2020-12-14 19:08:45 +09:00
Weblate (bot)
e17c637483 feat(lang): Translations update from Weblate (#240)
* feat(lang): translated using Weblate (German)

Currently translated at 100.0% (306 of 306 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (310 of 310 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 100.0% (306 of 306 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 88.8% (272 of 306 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 72.5% (222 of 306 strings)

feat(lang): translated using Weblate (Dutch)

Currently translated at 69.9% (214 of 306 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nl/
Translation: Overseerr/Overseerr Frontend

* feat(lang): translated using Weblate (French)

Currently translated at 100.0% (306 of 306 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (306 of 306 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (306 of 306 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
Translation: Overseerr/Overseerr Frontend

Co-authored-by: K. Herbert <herbert.ka@mailo.com>
Co-authored-by: Jos Vennik <josvennik@gmail.com>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
2020-12-14 19:07:46 +09:00
sct
15013d6c5d fix(frontend): encode special characters in search input to prevent crashing router
re #252
2020-12-14 09:38:38 +00:00
sct
8cb05c413a fix(frontend): convert plex port to a number before posting to the api 2020-12-14 09:13:59 +00:00
sct
2098a2d3d2 fix(frontend): converts email smtp port to a number before posting to the api
- also adds toast notifications to both email/discord notifications pages to know when sucesses or
failures occur

re #251
2020-12-14 09:00:10 +00:00
sct
6c1ee830a1 fix(services): improve logging for adding movies to Radarr 2020-12-14 08:17:38 +00:00
sct
0658b7943e fix(services): radarr/sonarr will use the correct default server 2020-12-14 08:14:08 +00:00
sct
edbbccf3ae fix(plex sync): catch errors that occur during processMovie
this also removes the unique constraint on imdbId

re #244 #246 #250
2020-12-14 08:01:33 +00:00
273 changed files with 27338 additions and 4505 deletions

View File

@@ -70,6 +70,228 @@
"contributions": [
"doc"
]
},
{
"login": "jvennik",
"name": "jvennik",
"avatar_url": "https://avatars3.githubusercontent.com/u/6672637?v=4",
"profile": "https://github.com/jvennik",
"contributions": [
"translation"
]
},
{
"login": "darknessgp",
"name": "darknessgp",
"avatar_url": "https://avatars0.githubusercontent.com/u/1521243?v=4",
"profile": "https://github.com/darknessgp",
"contributions": [
"code"
]
},
{
"login": "saltydk",
"name": "salty",
"avatar_url": "https://avatars1.githubusercontent.com/u/6587950?v=4",
"profile": "https://github.com/saltydk",
"contributions": [
"infra"
]
},
{
"login": "Shutruk",
"name": "Shutruk",
"avatar_url": "https://avatars2.githubusercontent.com/u/9198633?v=4",
"profile": "https://github.com/Shutruk",
"contributions": [
"translation"
]
},
{
"login": "krystiancharubin",
"name": "Krystian Charubin",
"avatar_url": "https://avatars2.githubusercontent.com/u/17775600?v=4",
"profile": "https://github.com/krystiancharubin",
"contributions": [
"design"
]
},
{
"login": "kieron",
"name": "Kieron Boswell",
"avatar_url": "https://avatars2.githubusercontent.com/u/8655212?v=4",
"profile": "https://github.com/kieron",
"contributions": [
"code"
]
},
{
"login": "samwiseg0",
"name": "samwiseg0",
"avatar_url": "https://avatars1.githubusercontent.com/u/2241731?v=4",
"profile": "https://github.com/samwiseg0",
"contributions": [
"question",
"infra"
]
},
{
"login": "ecelebi29",
"name": "ecelebi29",
"avatar_url": "https://avatars2.githubusercontent.com/u/8337120?v=4",
"profile": "https://github.com/ecelebi29",
"contributions": [
"code",
"doc"
]
},
{
"login": "mmozeiko",
"name": "Mārtiņš Možeiko",
"avatar_url": "https://avatars3.githubusercontent.com/u/1665010?v=4",
"profile": "https://github.com/mmozeiko",
"contributions": [
"code"
]
},
{
"login": "mazzetta86",
"name": "mazzetta86",
"avatar_url": "https://avatars2.githubusercontent.com/u/45591560?v=4",
"profile": "https://github.com/mazzetta86",
"contributions": [
"translation"
]
},
{
"login": "Panzer1119",
"name": "Paul Hagedorn",
"avatar_url": "https://avatars1.githubusercontent.com/u/23016343?v=4",
"profile": "https://github.com/Panzer1119",
"contributions": [
"translation"
]
},
{
"login": "Shagon94",
"name": "Shagon94",
"avatar_url": "https://avatars3.githubusercontent.com/u/9140783?v=4",
"profile": "https://github.com/Shagon94",
"contributions": [
"translation"
]
},
{
"login": "sebstrgg",
"name": "sebstrgg",
"avatar_url": "https://avatars3.githubusercontent.com/u/27026694?v=4",
"profile": "https://github.com/sebstrgg",
"contributions": [
"translation"
]
},
{
"login": "danshilm",
"name": "Danshil Mungur",
"avatar_url": "https://avatars2.githubusercontent.com/u/20923978?v=4",
"profile": "https://github.com/danshilm",
"contributions": [
"code",
"doc"
]
},
{
"login": "doob187",
"name": "doob187",
"avatar_url": "https://avatars1.githubusercontent.com/u/60312740?v=4",
"profile": "https://github.com/doob187",
"contributions": [
"infra"
]
},
{
"login": "johnpyp",
"name": "johnpyp",
"avatar_url": "https://avatars2.githubusercontent.com/u/20625636?v=4",
"profile": "https://github.com/johnpyp",
"contributions": [
"code"
]
},
{
"login": "ankarhem",
"name": "Jakob Ankarhem",
"avatar_url": "https://avatars1.githubusercontent.com/u/14110063?v=4",
"profile": "https://github.com/ankarhem",
"contributions": [
"doc",
"code"
]
},
{
"login": "jayesh100",
"name": "Jayesh",
"avatar_url": "https://avatars1.githubusercontent.com/u/8022175?v=4",
"profile": "https://github.com/jayesh100",
"contributions": [
"code"
]
},
{
"login": "flying-sausages",
"name": "flying-sausages",
"avatar_url": "https://avatars1.githubusercontent.com/u/23618693?v=4",
"profile": "https://github.com/flying-sausages",
"contributions": [
"doc"
]
},
{
"login": "hirenshah",
"name": "hirenshah",
"avatar_url": "https://avatars2.githubusercontent.com/u/418112?v=4",
"profile": "https://github.com/hirenshah",
"contributions": [
"doc"
]
},
{
"login": "TheCatLady",
"name": "TheCatLady",
"avatar_url": "https://avatars0.githubusercontent.com/u/52870424?v=4",
"profile": "https://github.com/TheCatLady",
"contributions": [
"code",
"translation"
]
},
{
"login": "chriscpritchard",
"name": "Chris Pritchard",
"avatar_url": "https://avatars1.githubusercontent.com/u/1839074?v=4",
"profile": "https://github.com/chriscpritchard",
"contributions": [
"code",
"doc"
]
},
{
"login": "Tamberlox",
"name": "Tamberlox",
"avatar_url": "https://avatars3.githubusercontent.com/u/56069014?v=4",
"profile": "https://github.com/Tamberlox",
"contributions": [
"translation"
]
},
{
"login": "hmnd",
"name": "David",
"avatar_url": "https://avatars.githubusercontent.com/u/12853597?v=4",
"profile": "https://hmnd.io",
"contributions": [
"code"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",

View File

@@ -4,7 +4,12 @@ node_modules
.gitconfig
.gitignore
.github
.all-contributorsrc
.editorconfig
.prettierignore
**/README.md
**/.vscode
config/db/db.sqlite3
config/db/logs/overseerr.log
Dockerfil**
**.md

View File

@@ -32,6 +32,7 @@ module.exports = {
'formatjs/no-offset': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
'jsx-a11y/no-onchange': 'off',
},
overrides: [
{

5
.gitbook.yaml Normal file
View File

@@ -0,0 +1,5 @@
root: ./docs
structure:
readme: README.md
summary: SUMMARY.md

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [sct]
patreon: overseerr

View File

@@ -2,15 +2,17 @@
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
labels: 'awaiting-triage, type:bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Are you on latest or develop branch?**
Please fill in which docker image you are currently using.
**What version of Overseerr are you running?**
Please fill in the version you are currently running.
You can find it under: Settings -> About -> Version
**To Reproduce**
Steps to reproduce the behavior:

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
labels: 'awaiting-triage, type:enhancement'
assignees: ''
---

View File

@@ -10,7 +10,8 @@ on:
jobs:
test:
runs-on: ubuntu-18.04
name: Lint & Test Build
runs-on: ubuntu-20.04
container: node:12.18-alpine
steps:
- name: checkout
@@ -24,17 +25,64 @@ jobs:
- name: build
run: yarn build
build_and_push:
name: Build and push Docker image to Docker Hub
name: Build & Publish to Docker Hub
needs: test
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, 'skip ci')
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-20.04
steps:
- name: checkout
- name: Checkout
uses: actions/checkout@v2
- name: Build and push to Docker Hub
uses: docker/build-push-action@v1
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: sctx/overseerr
tags: develop
password: ${{ secrets.DOCKER_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
build-args: |
COMMIT_TAG=${{ github.sha }}
tags: |
sctx/overseerr:develop
sctx/overseerr:${{ github.sha }}
ghcr.io/sct/overseerr:develop
ghcr.io/sct/overseerr:${{ github.sha }}
discord:
name: Send Discord Notification
needs: build_and_push
if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-20.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v1
- name: Combine Job Status
id: status
run: |
failures=(neutral, skipped, timed_out, action_required)
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
echo ::set-output name=status::failure
else
echo ::set-output name=status::$WORKFLOW_CONCLUSION
fi
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ steps.status.outputs.status }}
title: ${{ github.workflow }}
nofail: true

19
.github/workflows/invalid_template.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: 'Invalid Template'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-20.04
steps:
- uses: dessant/support-requests@v2
with:
github-token: ${{ github.token }}
support-label: 'invalid:template-incomplete'
issue-comment: >
:wave: @{issue-author}, please edit your issue and follow the template provided.
close-issue: false
lock-issue: false
issue-lock-reason: 'resolved'

View File

@@ -7,7 +7,8 @@ on:
jobs:
test:
runs-on: ubuntu-18.04
name: Lint & Test Build
runs-on: ubuntu-20.04
container: node:12.18-alpine
steps:
- name: checkout
@@ -23,7 +24,7 @@ jobs:
semantic-release:
name: Tag and release latest version
needs: test
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -41,3 +42,84 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: npx semantic-release
build-snap:
name: Build Snap Package (${{ matrix.architecture }})
needs: semantic-release
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
architecture:
- amd64
- arm64
- armhf
steps:
- name: Checkout Code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Switch to master branch
run: git checkout master
- name: Prepare
id: prepare
run: |
git fetch --prune --unshallow --tags
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
echo ::set-output name=RELEASE::stable
else
echo ::set-output name=RELEASE::edge
fi
- name: Set Up QEMU
uses: docker/setup-qemu-action@v1
- name: Build Snap Package
uses: diddlesnaps/snapcraft-multiarch-action@v1
id: build
with:
architecture: ${{ matrix.architecture }}
- name: Upload Snap Package
uses: actions/upload-artifact@v2
with:
name: overseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1
with:
snap: ${{ steps.build.outputs.snap }}
- name: Publish Snap Package
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAP_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: ${{ steps.prepare.outputs.RELEASE }}
discord:
name: Send Discord Notification
needs: semantic-release
if: always()
runs-on: ubuntu-20.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v1
- name: Combine Job Status
id: status
run: |
failures=(neutral, skipped, timed_out, action_required)
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
echo ::set-output name=status::failure
else
echo ::set-output name=status::$WORKFLOW_CONCLUSION
fi
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ steps.status.outputs.status }}
title: ${{ github.workflow }}
nofail: true

100
.github/workflows/snap.yaml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: Publish Snap
on:
push:
branches: [develop]
jobs:
test:
name: Lint & Test Build
runs-on: ubuntu-20.04
if: "!contains(github.event.head_commit.message, '[skip ci]')"
container: node:12.18-alpine
steps:
- name: checkout
uses: actions/checkout@v2
- name: install dependencies
env:
HUSKY_SKIP_INSTALL: 1
run: yarn
- name: lint
run: yarn lint
- name: build
run: yarn build
build-snap:
name: Build Snap Package (${{ matrix.architecture }})
needs: test
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
architecture:
- amd64
- arm64
- armhf
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Prepare
id: prepare
run: |
git fetch --prune --unshallow --tags
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
echo ::set-output name=RELEASE::stable
else
echo ::set-output name=RELEASE::edge
fi
- name: Set Up QEMU
uses: docker/setup-qemu-action@v1
- name: Build Snap Package
uses: diddlesnaps/snapcraft-multiarch-action@v1
id: build
with:
architecture: ${{ matrix.architecture }}
- name: Upload Snap Package
uses: actions/upload-artifact@v2
with:
name: overseerr-snap-package-${{ matrix.architecture }}
path: ${{ steps.build.outputs.snap }}
- name: Review Snap Package
uses: diddlesnaps/snapcraft-review-tools-action@v1
with:
snap: ${{ steps.build.outputs.snap }}
- name: Publish Snap Package
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAP_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: ${{ steps.prepare.outputs.RELEASE }}
discord:
name: Send Discord Notification
needs: build-snap
if: always() && !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ubuntu-20.04
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v1
- name: Combine Job Status
id: status
run: |
failures=(neutral, skipped, timed_out, action_required)
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
echo ::set-output name=status::failure
else
echo ::set-output name=status::$WORKFLOW_CONCLUSION
fi
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ steps.status.outputs.status }}
title: ${{ github.workflow }}
nofail: true

View File

@@ -6,7 +6,7 @@ on:
jobs:
support:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: dessant/support-requests@v2
with:

11
.gitignore vendored
View File

@@ -32,14 +32,21 @@ yarn-error.log*
.vercel
# database
config/db/db.sqlite3
config/db/*.sqlite3
config/settings.json
# logs
config/logs/*.log
config/logs/*.log*
config/logs/*.json
# anidb mapping file
config/anime-list.xml
# dist files
dist
# sqlite journal
config/db/db.sqlite3-journal
# VS Code
.vscode/launch.json

View File

@@ -19,6 +19,9 @@
"stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss"
"bradlc.vscode-tailwindcss",
// https://marketplace.visualstudio.com/items?itemName=heybourn.headwind
"heybourn.headwind"
]
}

View File

@@ -1,3 +1,412 @@
# [1.18.0](https://github.com/sct/overseerr/compare/v1.17.2...v1.18.0) (2021-01-30)
### Bug Fixes
* **api:** prevent duplicate movie requests ([421f4c1](https://github.com/sct/overseerr/commit/421f4c17f0f206bbe7bfcbf2819014b8c7f55b6a)), closes [#705](https://github.com/sct/overseerr/issues/705)
* **build:** fix sqlite3 build error ([#691](https://github.com/sct/overseerr/issues/691)) ([3a1f6d5](https://github.com/sct/overseerr/commit/3a1f6d5706c8fc100e88425f3d89a26a0325af79))
* **frontend:** add poster not found image to request card and request list item ([ae9a1b3](https://github.com/sct/overseerr/commit/ae9a1b3e940ac2abf6e842d91f458daab3dd0f0d))
* **frontend:** add poster not found image to tv details page ([0b05545](https://github.com/sct/overseerr/commit/0b055458d0ddbfd4c87ebf9b0562f161fa3445a3))
* **frontend:** dont show external links unless slug is set ([946bd2d](https://github.com/sct/overseerr/commit/946bd2db5ecde0748b2e9bc5edfe7ca6000ec3d5))
* **frontend:** fix server name position on plex settings page ([86efcd8](https://github.com/sct/overseerr/commit/86efcd82c34ad6490f2899ebf6f84cdd1bffc498))
* **frontend:** fixed mismatched rounded sizing on new login ([5e352c2](https://github.com/sct/overseerr/commit/5e352c201fc2f731ca5f713ecb6901527ef354da)), closes [#721](https://github.com/sct/overseerr/issues/721)
* **ip logging:** add env var for proxy to fix ip logging on failed logins ([#756](https://github.com/sct/overseerr/issues/756)) ([9342a40](https://github.com/sct/overseerr/commit/9342a40bbc03f7fdda23e3876b3a4a81ea8532c0))
* **lang:** add missing i18n strings for notification settings ([2f75c4c](https://github.com/sct/overseerr/commit/2f75c4c6aed42a15bb47d3652272de8f852ec79f))
* **notifications:** only send a single notification when standard media becomes available ([b5fd1d5](https://github.com/sct/overseerr/commit/b5fd1d520cd2a7be6e6356a25129e93af1caf542)), closes [#770](https://github.com/sct/overseerr/issues/770)
* **permissions:** use default user permissions when creating a local user ([#713](https://github.com/sct/overseerr/issues/713)) ([660ada0](https://github.com/sct/overseerr/commit/660ada0b2025eb2c06d9054fd0a7b5a632af6af2))
* **radarr:** fix request bug which made it unable to be added to radarr ([#760](https://github.com/sct/overseerr/issues/760)) ([45a2779](https://github.com/sct/overseerr/commit/45a277964b0c39346d7216873812e0ebe505cb79))
* **radarr:** return the updated data when updating radarr request ([#765](https://github.com/sct/overseerr/issues/765)) ([0c6d478](https://github.com/sct/overseerr/commit/0c6d4780c355ffe1a951268fb6949491d435bbf1))
* **requests:** handle when tvdbid is null ([#657](https://github.com/sct/overseerr/issues/657)) ([2da0da8](https://github.com/sct/overseerr/commit/2da0da826ae1d73467bc8a671fda7cc5ca1f14c9))
* **sonarr-sync:** correctly set series with no seasons to requested status ([3812989](https://github.com/sct/overseerr/commit/3812989a1ce1e07d4af09149008043a6e2e94060)), closes [#762](https://github.com/sct/overseerr/issues/762)
* **sync:** do not update series status if already available and no new seasons ([136d874](https://github.com/sct/overseerr/commit/136d874cba37babf9c0670844b002871710e6d99)), closes [#777](https://github.com/sct/overseerr/issues/777)
* **ui:** Capitalization, punctuation, and grammar inconsistences & errors ([#731](https://github.com/sct/overseerr/issues/731)) ([f05d4a0](https://github.com/sct/overseerr/commit/f05d4a0d0b42905fcaee49b2471bb1f4ee77fffe))
* lookup movie by imdbid if tmdbid does not exits for plex movie agent ([#711](https://github.com/sct/overseerr/issues/711)) ([e972288](https://github.com/sct/overseerr/commit/e97228899a5936b2525c8060abfa14b5ce31658d))
* show recently added series even if they are not complete ([d0c830e](https://github.com/sct/overseerr/commit/d0c830e80d389f9e0f48a9b83659331f54630d03))
### Features
* **lang:** translated using Weblate (Dutch) ([059995e](https://github.com/sct/overseerr/commit/059995e0ef3370a3192bd386fa6875ca0f58690a))
* **lang:** translated using Weblate (French) ([4789583](https://github.com/sct/overseerr/commit/4789583d66305ac7b3d393659b2f3604c0acc576))
* **lang:** translations update from Weblate ([#727](https://github.com/sct/overseerr/issues/727)) ([71875ef](https://github.com/sct/overseerr/commit/71875efb48246dbb0139ad15a4261a5661fcfe17))
* **lang:** update languages and fix merge conflict ([083a74a](https://github.com/sct/overseerr/commit/083a74a686d202cce5775bf9752caaa9a626cf45))
* **ui:** Move PROXY setting to UI ([#782](https://github.com/sct/overseerr/issues/782)) ([f1dd5e7](https://github.com/sct/overseerr/commit/f1dd5e7e12c1f602449c4769173dbce71e3569d0))
* add manual availability buttons to manage slideover ([67f8aef](https://github.com/sct/overseerr/commit/67f8aef00d98c834b60cb6152ccd5cb7b5709d12)), closes [#672](https://github.com/sct/overseerr/issues/672)
* **media:** add link to the item on plex ([#735](https://github.com/sct/overseerr/issues/735)) ([1d7150c](https://github.com/sct/overseerr/commit/1d7150c24ec5ad347093889bfceab61b664900d5))
* Radarr & Sonarr Sync ([#734](https://github.com/sct/overseerr/issues/734)) ([ec5fb83](https://github.com/sct/overseerr/commit/ec5fb836785855eb4846fd33b49faeb94c40506a))
* **frontend:** add option to hide all available items from discovery ([#699](https://github.com/sct/overseerr/issues/699)) ([6c1742e](https://github.com/sct/overseerr/commit/6c1742e94ccfc6c13cf1d25fd9e893ee1f431aae))
* **lang:** add support for Portuguese (Portugal) language ([e044146](https://github.com/sct/overseerr/commit/e044146aa55109a1eccfde9650b26beb0d5ec9a6))
* **lang:** translated using Weblate (Dutch) ([6d0f7d4](https://github.com/sct/overseerr/commit/6d0f7d4b50370c420c1017f32d48313074543743))
* **lang:** translated using Weblate (Italian) ([9aa5c12](https://github.com/sct/overseerr/commit/9aa5c121644518c1fbb308a487c26d8998bb5a36))
* **lang:** translated using Weblate (Portuguese (Portugal)) ([f001fb3](https://github.com/sct/overseerr/commit/f001fb3b33d4fb749acb70c45b8a55a5bbef570c))
* **lang:** translated using Weblate (Spanish) ([4f94d22](https://github.com/sct/overseerr/commit/4f94d227fc3096bcb8a1e5cf12fe9222d6c6b711))
* **login:** add request ip to the failed request log ([#714](https://github.com/sct/overseerr/issues/714)) ([2d31ea9](https://github.com/sct/overseerr/commit/2d31ea940ac0a1a84d2150743798b41ff6490317))
* **users:** add editable usernames ([#715](https://github.com/sct/overseerr/issues/715)) ([20ca3f2](https://github.com/sct/overseerr/commit/20ca3f2f5fcf4a9eb0d6a8be671bb4fb1f5e6178))
* pre-populate server info from plex.tv API ([#563](https://github.com/sct/overseerr/issues/563)) ([82ac76b](https://github.com/sct/overseerr/commit/82ac76b0540ba1133cb5384744d2499c2488a4e8))
* **auth:** Add optional CSRF protection ([#697](https://github.com/sct/overseerr/issues/697)) ([6e25891](https://github.com/sct/overseerr/commit/6e2589178b99f8f32f0ded9a7cfd9921c33e9b60))
* ability to edit user settings in bulk ([#597](https://github.com/sct/overseerr/issues/597)) ([4b0241c](https://github.com/sct/overseerr/commit/4b0241c3b34d4229f928c21defb10a1c051264d1))
* **lang:** translated using Weblate (English) ([9bb11af](https://github.com/sct/overseerr/commit/9bb11afc6b4a109ae1e14d41c9fe2b71f19c470a))
* **lang:** translated using Weblate (German) ([c2a3e8e](https://github.com/sct/overseerr/commit/c2a3e8ed5243925dce991ec7995ae831702dbc7b))
* **lang:** translated using Weblate (Portuguese (Brazil)) ([32f4916](https://github.com/sct/overseerr/commit/32f4916c4a926097f31ed472aee031536b847bb7))
* **lang:** translated using Weblate (Portuguese (Brazil)) ([98570c9](https://github.com/sct/overseerr/commit/98570c920e4904a594bb7464161b985094958f84))
* **notifications:** add option to send notifications for auto-approved requests ([21db367](https://github.com/sct/overseerr/commit/21db3676d1464b63384b04c0c2926cb2a6252e9b)), closes [#267](https://github.com/sct/overseerr/issues/267)
## [1.17.2](https://github.com/sct/overseerr/compare/v1.17.1...v1.17.2) (2021-01-20)
### Bug Fixes
* **requests:** allow declined season requests to be re-requested ([e1032ff](https://github.com/sct/overseerr/commit/e1032ff5dfac4a8c9d4da9cf2788c19822343ad9)), closes [#690](https://github.com/sct/overseerr/issues/690)
* **requests:** update requests to approved when parent media is set as available ([78444a9](https://github.com/sct/overseerr/commit/78444a9e643829823162389dee60cca70da56bff)), closes [#688](https://github.com/sct/overseerr/issues/688)
## [1.17.1](https://github.com/sct/overseerr/compare/v1.17.0...v1.17.1) (2021-01-19)
### Bug Fixes
* **frontend:** show auto approval on series request modal only with correct permissions ([8927c6d](https://github.com/sct/overseerr/commit/8927c6d2e39dbda2b1121095a7273f5cab1c9b74)), closes [#687](https://github.com/sct/overseerr/issues/687)
# [1.17.0](https://github.com/sct/overseerr/compare/v1.16.0...v1.17.0) (2021-01-19)
### Bug Fixes
* **api:** improve rottentomatoes rating matching for movies ([7db62ab](https://github.com/sct/overseerr/commit/7db62ab824eefc42e6db16e42d52f4266b136f82)), closes [#494](https://github.com/sct/overseerr/issues/494)
* **build:** remove cross import from client to server for UserType ([23624bd](https://github.com/sct/overseerr/commit/23624bd144af5df4c31995b68ce48105b95b20f6))
* **frontend:** clarify which fields are required in radarr/sonarr modals ([860d71e](https://github.com/sct/overseerr/commit/860d71ed69a69a1a3f74b79290ef471e04f57a6b)), closes [#575](https://github.com/sct/overseerr/issues/575)
* **frontend:** do not show failed media status on request list for declined requests ([00944b1](https://github.com/sct/overseerr/commit/00944b1ec2db8ddc5742448f6448f7364c473a98)), closes [#664](https://github.com/sct/overseerr/issues/664)
* **frontend:** fix button styling on details page on small screen sizes ([d9e0c90](https://github.com/sct/overseerr/commit/d9e0c90e76d80aef0c67318e00e997804805f46e))
* **frontend:** fix request button height ([a262727](https://github.com/sct/overseerr/commit/a2627270784bdef8644875fa5c5a7349a0b7fd81))
* **frontend:** request dropdown menu now properly shows up over collection button ([b491be1](https://github.com/sct/overseerr/commit/b491be1b1e7f6aa588274230e695e4c5302b961e))
* **frontend:** show correct request status on request cards for 4k requests ([1aa0005](https://github.com/sct/overseerr/commit/1aa0005b4298fc1af9c1d0bf1f357738c0fa2673))
* **lang:** add missing see more i18n string for SeeMoreCard ([d9919ab](https://github.com/sct/overseerr/commit/d9919abb8998d28558ddec35b8e60ab2af75d5b7))
* **lang:** change email auth user/pass strings to SMTP Username/Password ([a77a2aa](https://github.com/sct/overseerr/commit/a77a2aa3ebb1be353d534db5b07647ac26c60e15))
* **notifications:** correctly compare seasons before sending series notifications ([f17fa2a](https://github.com/sct/overseerr/commit/f17fa2a2db8144bac89936f588627e8dd37bf54a))
* **notifications:** only send one available notification for standard media ([fc6f7cc](https://github.com/sct/overseerr/commit/fc6f7ccea586165a30022b6d5554911c66ece6df))
* **notifications:** send media declined email ([eb6fc8a](https://github.com/sct/overseerr/commit/eb6fc8a19099469794d471db0b48a258c2866633)), closes [#679](https://github.com/sct/overseerr/issues/679)
* **plex-sync:** improve plex sync error handling. add session id to fix stuck runs ([a740b07](https://github.com/sct/overseerr/commit/a740b07f06f892b72a651b928af28ce71cb495ee))
* **plex-sync:** store plex added date and sort recently added by it ([d688a96](https://github.com/sct/overseerr/commit/d688a967596afcba9799b8133089bebb5add27cf))
* **requests:** select the correct radarr/sonarr server when sending request to service ([e0d9f89](https://github.com/sct/overseerr/commit/e0d9f891e797c3839f976b75a871903b6f2e55f1))
* **server:** support absolute paths for CONFIG_DIRECTORY ([51d8fba](https://github.com/sct/overseerr/commit/51d8fba9162b9e148a35ced69e7e035438c8b0f1))
* **user edit:** fix user edit not being able to be saved ([#651](https://github.com/sct/overseerr/issues/651)) ([b04d00e](https://github.com/sct/overseerr/commit/b04d00ef509d6f13c1f9677b3f318331782c0086))
### Features
* **api:** /request/count endpoint ([#682](https://github.com/sct/overseerr/issues/682)) ([192cfd8](https://github.com/sct/overseerr/commit/192cfd8a8ea9ab942d5bb265d42050917a2f5a04))
* **frontend:** add see more card to media sliders ([587e8db](https://github.com/sct/overseerr/commit/587e8db15e9c19b4c58406e3e4215d8bf87d8762))
* **frontend:** add template variable help button to custom webhook settings page ([29c5bc4](https://github.com/sct/overseerr/commit/29c5bc40975e7ab0a2e08bb77294f164f0c60769))
* **lang:** add support for Chinese (Traditional) language ([686c4f7](https://github.com/sct/overseerr/commit/686c4f71bf930625af082ac5e14dc5f79f5c42eb))
* **lang:** Translations update from Weblate ([#604](https://github.com/sct/overseerr/issues/604)) ([801e765](https://github.com/sct/overseerr/commit/801e76524d6ea0887249f1630402e9c3a3430b44))
* **login:** add local users functionality ([#591](https://github.com/sct/overseerr/issues/591)) ([492e19d](https://github.com/sct/overseerr/commit/492e19df4014e67dc6a2de5903a33c25e13fcf45))
* **notifications:** add notification for declined requests ([2f97f61](https://github.com/sct/overseerr/commit/2f97f61a6e8846975774aa16950a39ada2b1a016)), closes [#663](https://github.com/sct/overseerr/issues/663)
* **notifications:** Webhook Notifications ([#632](https://github.com/sct/overseerr/issues/632)) ([a7cc7c5](https://github.com/sct/overseerr/commit/a7cc7c59753dd9649b2ec37eb9d46fe4fa8e1e1c))
* **requests:** Request Overrides & Request Editing ([#653](https://github.com/sct/overseerr/issues/653)) ([bdb3372](https://github.com/sct/overseerr/commit/bdb33722e6df09dd6d8caa36b104b61c6b8dc00d))
* **server:** add CONFIG_DIRECTORY env var to control config directory location ([fa8f112](https://github.com/sct/overseerr/commit/fa8f112c31ccb5ee6244f776bc97e76d81958539))
* 4K Requests ([#559](https://github.com/sct/overseerr/issues/559)) ([6b2df24](https://github.com/sct/overseerr/commit/6b2df24a2e8f96dd2277a814d7e02015d1f80cdc))
* map AniDB IDs from Hama agent to tvdb/tmdb/imdb IDs ([#538](https://github.com/sct/overseerr/issues/538)) ([0600ac7](https://github.com/sct/overseerr/commit/0600ac7c3a1bc0cdd906634d5f77ea3e99b10e94)), closes [#453](https://github.com/sct/overseerr/issues/453)
### Reverts
* **deps:** revert back to next@10.0.3 until sharp optional dependency bug is fixed ([7962964](https://github.com/sct/overseerr/commit/79629645aacc1a042919834da79bff0c1f69c9d6))
# [1.16.0](https://github.com/sct/overseerr/compare/v1.15.0...v1.16.0) (2021-01-07)
### Bug Fixes
* **frontend:** adjust titlecard badge styling ([effc809](https://github.com/sct/overseerr/commit/effc80977a4ed732092254248f82363e52233171))
* **frontend:** apply same titlecard hover effect to personcard ([67f2b57](https://github.com/sct/overseerr/commit/67f2b57f00216ded3b34965629d6fdd2f16bc25f))
* **frontend:** only animate titlecard when showDetail is true ([0ab4c3c](https://github.com/sct/overseerr/commit/0ab4c3c36fe2c1ded142b6931111516f7f990a41))
* **frontend:** use hardware acceleration for titlecard scale ([88810bf](https://github.com/sct/overseerr/commit/88810bf0a4ef74299f6541b60fa91cea3610f99c))
* **plex-sync:** do not run plex sync if no admin exists ([493d82b](https://github.com/sct/overseerr/commit/493d82b6b066d77609cf66e005fd1f1472b8e011))
### Features
* **lang:** translations update from Weblate ([#495](https://github.com/sct/overseerr/issues/495)) ([b04eda6](https://github.com/sct/overseerr/commit/b04eda6c8a3bfcaa2a14b8a29612fdf690c9fba0))
* **lang:** Translations update from Weblate ([#580](https://github.com/sct/overseerr/issues/580)) ([2bfe0f2](https://github.com/sct/overseerr/commit/2bfe0f2bf66956763ab26d5c54f26e6c456f59f7))
* **notifications:** add pushover integration ([#574](https://github.com/sct/overseerr/issues/574)) ([ee5d018](https://github.com/sct/overseerr/commit/ee5d0181fc9a673b27aefd1d09b0a78c3d2e4f55))
# [1.15.0](https://github.com/sct/overseerr/compare/v1.14.1...v1.15.0) (2021-01-04)
### Bug Fixes
* **api:** return 202 when same seasons are requested again ([5c84702](https://github.com/sct/overseerr/commit/5c847026aad79fcac4d020786ded9f867696c226))
* **build:** fixes build to include commit tag for app build step ([289864a](https://github.com/sct/overseerr/commit/289864af1a995ce04834bf8a220cc238e1954d19))
* **docs:** fix typo in build instructions ([#503](https://github.com/sct/overseerr/issues/503)) ([2b27a71](https://github.com/sct/overseerr/commit/2b27a715b07c27200ba1e5e9623629a34389276d))
* **frontend:** add i18n for request text on titlecard ([a524b9c](https://github.com/sct/overseerr/commit/a524b9c4c8968f6823d33eb270dc26069fe4a725))
* **frontend:** add localized strings for status checker ([2dcda39](https://github.com/sct/overseerr/commit/2dcda39d40d820419e098bd6f1101eb820e5b42d))
* **frontend:** center text in movie auto-approve modal on small screens ([#510](https://github.com/sct/overseerr/issues/510)) ([1438b08](https://github.com/sct/overseerr/commit/1438b08cf0b358d79c6688c64be99f1718ec2d23)), closes [#507](https://github.com/sct/overseerr/issues/507)
* **frontend:** change titlecard to only have a request button ([b5a3a7a](https://github.com/sct/overseerr/commit/b5a3a7a89fcaf86dd794dc419711677b53646577))
* **frontend:** combine duplicate credits on a persons detail page ([d188f6f](https://github.com/sct/overseerr/commit/d188f6ffadff1564c47d5f33138e35498bed29fd)), closes [#504](https://github.com/sct/overseerr/issues/504)
* **frontend:** disable pointer-events on titlecard badges ([ce06879](https://github.com/sct/overseerr/commit/ce0687922a94588b3492e8ddf2e84f54dd1a0d4e))
* **frontend:** fix count of requests in request list ([f124d73](https://github.com/sct/overseerr/commit/f124d732a2911abdccb5abc11471efe61cc20f7a))
* **frontend:** fix sliders overflowing on firefox ([67ac9e0](https://github.com/sct/overseerr/commit/67ac9e075f0ca1cfe7e4766d9168815d7ab600fa)), closes [#566](https://github.com/sct/overseerr/issues/566)
* **frontend:** full season request modal fits on a smaller mobile UI ([#535](https://github.com/sct/overseerr/issues/535)) ([12db7a0](https://github.com/sct/overseerr/commit/12db7a065ad566b47d46de4b949343290894f153))
* **frontend:** handle currentLibrary possibly being null on first manual sync ([93b57a7](https://github.com/sct/overseerr/commit/93b57a76f10a823615ca11ff59f523b67aa30fad))
* **frontend:** increase titlecard status badge size on larger screens ([ba106c4](https://github.com/sct/overseerr/commit/ba106c447d76db2f9ac70a60c5b38cc60ab554fe))
* **frontend:** search clear button now correctly triggers routing ([343f466](https://github.com/sct/overseerr/commit/343f466788abc308b91a414ef61bba816ac8875c))
* **frontend:** set locale cookie expiration to be much longer ([fae4818](https://github.com/sct/overseerr/commit/fae481895736eab81d52eb93788beb00669fb355))
* **frontend:** show movie/series badges always ([8cbf39a](https://github.com/sct/overseerr/commit/8cbf39a9d12eaee7720fa4721c350c1ef9dee856))
* **frontend:** update login/setup images ([058fb65](https://github.com/sct/overseerr/commit/058fb65495baa08a0bd4c9e0aef320c6fc7d017b))
* **holiday:** remove special holiday slider ([8c09033](https://github.com/sct/overseerr/commit/8c0903393cf2cb2a929ba70a8ab6ddcc4cba0574))
* correctly deal with tmdb id duplicates between movies/series ([721ed9a](https://github.com/sct/overseerr/commit/721ed9a93087a57ae749388bddcacf26022e3df6)), closes [#526](https://github.com/sct/overseerr/issues/526)
* use new commit tag file for app version as well ([d00e470](https://github.com/sct/overseerr/commit/d00e470b55327489b49d770144b7cfdb24045be6))
### Features
* **email:** add sendername to email notification ([#506](https://github.com/sct/overseerr/issues/506)) ([0185bb1](https://github.com/sct/overseerr/commit/0185bb1a7084c1faeb61fb1c63e34e26732711c8))
* **frontend:** add clear-field-icon to search field ([#498](https://github.com/sct/overseerr/issues/498)) ([7434a26](https://github.com/sct/overseerr/commit/7434a26f76b5e9f74918f3e1a34443d20ecfcbe4))
* **frontend:** add documentation link to about page ([c034496](https://github.com/sct/overseerr/commit/c034496f557a031aed35cd28dc7221d8cdf36643))
* **frontend:** add telegram integration ([#491](https://github.com/sct/overseerr/issues/491)) ([c8d4d67](https://github.com/sct/overseerr/commit/c8d4d674f412082ad9e9da09abd79660365cf728))
* **frontend:** filter/sorting for request list ([5add44c](https://github.com/sct/overseerr/commit/5add44cfb0379aa6fed7c3b867230292feacc684)), closes [#431](https://github.com/sct/overseerr/issues/431)
* **notifications:** control notifcation types per agent ([8af6a1f](https://github.com/sct/overseerr/commit/8af6a1f566769c583af7dd9e18d162717835b7cc)), closes [#513](https://github.com/sct/overseerr/issues/513)
* status checker to prompt users to reload their frontend when app version changes ([75a4264](https://github.com/sct/overseerr/commit/75a426437a4182e21da13684066966dd5bf8fc5e))
## [1.14.1](https://github.com/sct/overseerr/compare/v1.14.0...v1.14.1) (2021-01-02)
### Bug Fixes
* **holiday:** remove special holiday slider ([22f2037](https://github.com/sct/overseerr/commit/22f2037ea6c5a0ba2ffa4d69f2b7cf42bdcf8575))
# [1.14.0](https://github.com/sct/overseerr/compare/v1.13.0...v1.14.0) (2020-12-25)
### Bug Fixes
* **frontend:** add margin to ButtonWithDropdown component on movie/tv details page ([06fc98b](https://github.com/sct/overseerr/commit/06fc98b6b958221fa180f57f702c348f15b31f1c))
* **frontend:** correctly position title card hover section ([#486](https://github.com/sct/overseerr/issues/486)) ([4b7af86](https://github.com/sct/overseerr/commit/4b7af86111a0300e1a137f23fa4ad1639fa55feb))
* **frontend:** fix missing styles for alert component ([de3d288](https://github.com/sct/overseerr/commit/de3d288949b60d3a3af889d69a62bea2bc799ed7))
* **frontend:** fix mobile dropdown in notifications settings ([6353cda](https://github.com/sct/overseerr/commit/6353cda5825f442dd539886c7b9ba437edf27ac4))
* **frontend:** fix scaling titlecard content position ([bd94740](https://github.com/sct/overseerr/commit/bd947409e6e8ff313011b77adc76ccd5f9112c78))
* **frontend:** improve flex header on movie/tv details page ([d7b1c28](https://github.com/sct/overseerr/commit/d7b1c2840690c144ebf29a360defcbd6fdb21354))
* **frontend:** invalid dom-nesting title card fix ([#482](https://github.com/sct/overseerr/issues/482)) ([f2ebba7](https://github.com/sct/overseerr/commit/f2ebba7b1df775d33d2af6abc3ee2c9de5f2e57a)), closes [#476](https://github.com/sct/overseerr/issues/476)
* **frontend:** remove vote permission for now ([5d06a34](https://github.com/sct/overseerr/commit/5d06a347311bd10c05d8f58068ca7104e265dcca))
* **frontend:** sort person detail credits by tmdb votes ([17518db](https://github.com/sct/overseerr/commit/17518dbe7f545100770a892d03d1f8508adc3650))
* **frontend:** status badge Unavailable renamed to Requested ([ed94a0f](https://github.com/sct/overseerr/commit/ed94a0f335c59de526dd812aea7616313fe002fd)), closes [#374](https://github.com/sct/overseerr/issues/374)
* **frontend:** update titlecard status badge to new requested colors ([8f292d5](https://github.com/sct/overseerr/commit/8f292d538b937ea133175089979ef02599f6fef4))
* **logs:** rotate logs on a daily basis instead of incrementing log filename ([395cbb2](https://github.com/sct/overseerr/commit/395cbb2be6c62f1d7573593e49a93615eaf22853))
* improve apple-touch-icon and android app icons ([329a814](https://github.com/sct/overseerr/commit/329a814a8fb791122266c0b04b05848c71d68ba1))
### Features
* **lang:** translations update from Weblate ([#479](https://github.com/sct/overseerr/issues/479)) ([c8c74b0](https://github.com/sct/overseerr/commit/c8c74b0ae54fcc524aa8b2edf5a5c5e5db6c1638))
* **notifications:** add slack notification agent ([1163e81](https://github.com/sct/overseerr/commit/1163e81adc7da1e8334155ebee5b4672a22143db)), closes [#365](https://github.com/sct/overseerr/issues/365)
* add collections ([#484](https://github.com/sct/overseerr/issues/484)) ([a333a09](https://github.com/sct/overseerr/commit/a333a095820ce3f10857026ba4770a2fffeed7cb)), closes [#418](https://github.com/sct/overseerr/issues/418)
* add separate auto approve permissions for Movies/Series ([4809257](https://github.com/sct/overseerr/commit/480925781691de456abc427fbbba161be11a3a8a)), closes [#268](https://github.com/sct/overseerr/issues/268)
* simple failed request handling ([#474](https://github.com/sct/overseerr/issues/474)) ([02969d5](https://github.com/sct/overseerr/commit/02969d5426245062a2f53475d83c4a8639632c9d))
* YouTube Movie/TV Trailers ([#454](https://github.com/sct/overseerr/issues/454)) ([e88dc83](https://github.com/sct/overseerr/commit/e88dc83aeba0475e3ad421d5ab130cea4fc9a806))
# [1.13.0](https://github.com/sct/overseerr/compare/v1.12.1...v1.13.0) (2020-12-23)
### Bug Fixes
* **api:** correctly return firstAirDate for series in search endpoints ([32b4c99](https://github.com/sct/overseerr/commit/32b4c99950659d9e1da2ffa93c22383c54d0d904)), closes [#462](https://github.com/sct/overseerr/issues/462)
* **email:** correctly log errors when emails fail to send ([0980fa5](https://github.com/sct/overseerr/commit/0980fa54f9fc3bdfae6c57fa5a20ce3b2a88a677))
* **frontend:** added new Radarr v3 logo ([#471](https://github.com/sct/overseerr/issues/471)) ([3bbc716](https://github.com/sct/overseerr/commit/3bbc716434dc04bfe6b55de9898eb2c0ecb03baa))
* **frontend:** approve and decline button (in manage panel) will now fit on mobile ([#441](https://github.com/sct/overseerr/issues/441)) ([66ef72d](https://github.com/sct/overseerr/commit/66ef72dd42912d83ea8f86aabb75fbee547f8de9))
* **frontend:** filter out undefined backdrop paths for person details page ([2e0e4d5](https://github.com/sct/overseerr/commit/2e0e4d5129ed4912415f61eb8d1da41e88ddcaff))
* **frontend:** show backdrops instead of posters for new person detail design ([9f5f920](https://github.com/sct/overseerr/commit/9f5f920c23007363aa7f53ebef0b61236d4f53ea))
* clarify full sync runs every 24 hours ([0c8a180](https://github.com/sct/overseerr/commit/0c8a180189b2610bab2fa977d458743d8a60343e))
* **plex-sync:** match correct tmdb format for movies ([4205e32](https://github.com/sct/overseerr/commit/4205e32ae71bc18c07209f1c82e6af1cb5f01335))
### Features
* **email:** option to allow self signed certificates ([6898357](https://github.com/sct/overseerr/commit/6898357b13a6aa53a55709ea95819c2b3df6784c))
* **frontend:** adjust person details design and add improved truncate ([1fb7ea7](https://github.com/sct/overseerr/commit/1fb7ea72589d2908ae80a2a688881d4eb3c050e5))
* **frontend:** first air date added to TV details page ([#470](https://github.com/sct/overseerr/issues/470)) ([a7db01f](https://github.com/sct/overseerr/commit/a7db01fba483ca633a6eb9d39eb085ab9939d4d2))
* **lang:** translations update from Weblate ([#410](https://github.com/sct/overseerr/issues/410)) ([941fe19](https://github.com/sct/overseerr/commit/941fe1990454439cf05b48ef92bd3493432f8ed8))
* **logs:** rotate log files if they reach 20MB in size ([22002ab](https://github.com/sct/overseerr/commit/22002ab4c76aace2bb202ac58da605b7a6f75d6d)), closes [#438](https://github.com/sct/overseerr/issues/438)
* **notifications:** include direct links to media in notifications ([659fa50](https://github.com/sct/overseerr/commit/659fa505f0db32262ad0041cddb4daea893e6d65)), closes [#437](https://github.com/sct/overseerr/issues/437)
* **plex-sync:** add support for hama guid's ([ffe9e19](https://github.com/sct/overseerr/commit/ffe9e19c3b99de6af1185900e292da641ff44320)), closes [#453](https://github.com/sct/overseerr/issues/453)
## [1.12.1](https://github.com/sct/overseerr/compare/v1.12.0...v1.12.1) (2020-12-22)
### Bug Fixes
* **migration:** fixes issue migrating away from the unique imdbId constraint ([69fd7a5](https://github.com/sct/overseerr/commit/69fd7a5511215674a5c22ba48627f221da900229))
# [1.12.0](https://github.com/sct/overseerr/compare/v1.11.0...v1.12.0) (2020-12-22)
### Bug Fixes
* **api:** fix cross-imported type crashing build ([f35dae5](https://github.com/sct/overseerr/commit/f35dae56a583a5545375318fa5be994ae1f2557f))
* **api:** prevent checking first admin account for plex server access ([22006e9](https://github.com/sct/overseerr/commit/22006e9dbde82609440f89bde9a40887b4742682))
* **frontend:** add name, short_name and start_url to manifest ([#424](https://github.com/sct/overseerr/issues/424)) ([c6836e0](https://github.com/sct/overseerr/commit/c6836e02c810e8adb12c3a4b110f9604cf5b7b81))
* **frontend:** adjust person card layout to deal with overflowing content ([4891298](https://github.com/sct/overseerr/commit/48912988915ae40606a900a6f1dd23fc25ed567f)), closes [#416](https://github.com/sct/overseerr/issues/416)
* **frontend:** allow more special characters in search input ([5deb64a](https://github.com/sct/overseerr/commit/5deb64a87fd70e97da27a025ad11fb8ace0e0b57)), closes [#430](https://github.com/sct/overseerr/issues/430)
* **logs:** improve logging when adding to sonarr/radarr ([4b50522](https://github.com/sct/overseerr/commit/4b505223b881a750007e3fbc7d4bcb9677d4d412))
* only run migrations in production ([ab9cef3](https://github.com/sct/overseerr/commit/ab9cef3624b5db1ec03507553a69d33b87857e29))
* **notifications:** always update the media table when seasons become available ([0916b58](https://github.com/sct/overseerr/commit/0916b58594a00db98c6701fdcaee4f3c3e08904e))
* **plex-sync:** fixes processing movies using TMDB agent ([764db94](https://github.com/sct/overseerr/commit/764db94f1bd7866309684d5bd56033b21cbc2e0c)), closes [#363](https://github.com/sct/overseerr/issues/363)
### Features
* **frontend:** add crew related movies/shows to person details page ([12127a7](https://github.com/sct/overseerr/commit/12127a77633f0e92ae88cbafd49581296f559c33))
* **frontend:** add full crew page for movies/shows ([604ba2a](https://github.com/sct/overseerr/commit/604ba2a92f1d59489e7fc6dfc011347f8595c123))
* default user permissions added to settings ([e7ee85c](https://github.com/sct/overseerr/commit/e7ee85c29b5d25c6bff58717eae5e62de4dcef0c)), closes [#388](https://github.com/sct/overseerr/issues/388)
* import users from plex ([#428](https://github.com/sct/overseerr/issues/428)) ([7e8f361](https://github.com/sct/overseerr/commit/7e8f361af711001cfc4dcc06a384b76f9846f90f)), closes [#281](https://github.com/sct/overseerr/issues/281)
* **frontend:** add prioritized crew under overview ([6753d9d](https://github.com/sct/overseerr/commit/6753d9daaafb18672f14fd86f2c1675dcec39b13)), closes [#406](https://github.com/sct/overseerr/issues/406)
* **notifications:** added ability to send test notifications ([44a3054](https://github.com/sct/overseerr/commit/44a305426f3e9829c167a4a73095d0d248641f47)), closes [#309](https://github.com/sct/overseerr/issues/309)
### Reverts
* **deps:** revert react-use-clipboard to 1.0.2 ([7083ddf](https://github.com/sct/overseerr/commit/7083ddf18121716e3442acab3506c395fdc351ac))
# [1.11.0](https://github.com/sct/overseerr/compare/v1.10.0...v1.11.0) (2020-12-20)
### Features
* **frontend:** add language picker to setup/login ([ff2ab29](https://github.com/sct/overseerr/commit/ff2ab29491a80c421525b9a394d6fbbf54914dc2))
* **frontend:** add support overseerr block to about page ([c128898](https://github.com/sct/overseerr/commit/c128898206d6cbb482de4d8dca53f70b87e4911a))
* **frontend:** releases added to about page ([b7f5739](https://github.com/sct/overseerr/commit/b7f573903500cc8a62e39afd787bc1da8c09d88b)), closes [#303](https://github.com/sct/overseerr/issues/303)
* **lang:** add support for Italian, Portuguese (Brazil) and Serbian ([108dfc4](https://github.com/sct/overseerr/commit/108dfc4afd31388cb6c9e07deccd168ade8b1574))
* **lang:** add support for swedish language ([c9fe6cb](https://github.com/sct/overseerr/commit/c9fe6cb0b7ea984d8e4e1cb3f284935c9da7cc2b))
* **lang:** translations update from Weblate ([#400](https://github.com/sct/overseerr/issues/400)) ([1bd0e64](https://github.com/sct/overseerr/commit/1bd0e646e313ddf77ef331e818e03401fbf64a72))
* **lang:** translations update from Weblate ([#403](https://github.com/sct/overseerr/issues/403)) ([3778ad8](https://github.com/sct/overseerr/commit/3778ad829c0897de178212b3bde4c0d3b5089161))
# [1.10.0](https://github.com/sct/overseerr/compare/v1.9.1...v1.10.0) (2020-12-19)
### Bug Fixes
* **email:** fix link to Overseerr in email templates ([816fec1](https://github.com/sct/overseerr/commit/816fec1a83a53edb3b65c3e5e7d0e6e1bd49726d)), closes [#392](https://github.com/sct/overseerr/issues/392)
* **frontend:** adjust padding of search box so placeholder text fits on mobile ([3601d44](https://github.com/sct/overseerr/commit/3601d442db32d3f98f7b050365c11ea8ef9bc4ae)), closes [#393](https://github.com/sct/overseerr/issues/393)
* **frontend:** changed request block for slideover on mobile UI ([#387](https://github.com/sct/overseerr/issues/387)) ([549567a](https://github.com/sct/overseerr/commit/549567a7e9db01933546d9970fc06f17218dfab1))
* **frontend:** hide Request More button if all current seasons are available ([2a4dd52](https://github.com/sct/overseerr/commit/2a4dd52275007e48f946c3b9e29f1d78da57bdaa)), closes [#343](https://github.com/sct/overseerr/issues/343)
* **frontend:** try not to render broken rottentomatoes data ([a0c5608](https://github.com/sct/overseerr/commit/a0c5608aa0b6c7a4294300589efa9a662163ce48))
### Features
* **lang:** translations update from Weblate ([#391](https://github.com/sct/overseerr/issues/391)) ([5f71fb7](https://github.com/sct/overseerr/commit/5f71fb7ee280714275d2ac045c472fcdddd5a2ea))
* add missing tzdata package to image ([53bede6](https://github.com/sct/overseerr/commit/53bede692d4f0e940dededa63015fe1908129914)), closes [#394](https://github.com/sct/overseerr/issues/394)
* **frontend:** add external links to movie and tv detail pages ([a0024a0](https://github.com/sct/overseerr/commit/a0024a0cbe717d78f53413bb78644c829f143c4d))
* **lang:** translations update from Weblate ([#380](https://github.com/sct/overseerr/issues/380)) ([8408e19](https://github.com/sct/overseerr/commit/8408e19568b2f239c57e11e2946c75f193d1c22e))
## [1.9.1](https://github.com/sct/overseerr/compare/v1.9.0...v1.9.1) (2020-12-18)
### Bug Fixes
* change default internal port to 5055 ([#389](https://github.com/sct/overseerr/issues/389)) ([5e5ba40](https://github.com/sct/overseerr/commit/5e5ba4050563f07bff367d2fb31ed7e7fca4291e))
# [1.9.0](https://github.com/sct/overseerr/compare/v1.8.0...v1.9.0) (2020-12-18)
### Features
* api key regeneration ([6beac73](https://github.com/sct/overseerr/commit/6beac736efcf7b9102e02e43b75d91a9a158cd22))
* **api:** add movie keyword search ([f88c4a6](https://github.com/sct/overseerr/commit/f88c4a6d4a49f8f3451ba6c85153677f33b7f5f6))
* **frontend:** add studio/networks to movie/tv details ([4b6ad8a](https://github.com/sct/overseerr/commit/4b6ad8a3871957db4192b603abf38404250cea5d)), closes [#370](https://github.com/sct/overseerr/issues/370)
* **frontend:** added user deletion to the user list ([727fa06](https://github.com/sct/overseerr/commit/727fa06c18febb2a97ca219cc6bf0277ff462acd)), closes [#348](https://github.com/sct/overseerr/issues/348)
* **holiday:** special seasonal slider added to discover :) ([908f635](https://github.com/sct/overseerr/commit/908f63557ca03a1da8b16809ffa2c3acd782d94e))
* allow to listen server on specific host interface ([#381](https://github.com/sct/overseerr/issues/381)) ([086183b](https://github.com/sct/overseerr/commit/086183b5636aa8d075d01fe59492c3eab0d1345b)), closes [#273](https://github.com/sct/overseerr/issues/273)
* anime profile support ([#384](https://github.com/sct/overseerr/issues/384)) ([0972f40](https://github.com/sct/overseerr/commit/0972f40a4e1fb3b5f02b07ae46b997d71aab9bfb)), closes [#266](https://github.com/sct/overseerr/issues/266)
# [1.8.0](https://github.com/sct/overseerr/compare/v1.7.0...v1.8.0) (2020-12-17)
### Features
* **lang:** translations update from Weblate ([#336](https://github.com/sct/overseerr/issues/336)) ([ee84f74](https://github.com/sct/overseerr/commit/ee84f74f8a3558875b41daa539f42d00b949898a))
# [1.7.0](https://github.com/sct/overseerr/compare/v1.6.0...v1.7.0) (2020-12-17)
### Bug Fixes
* **email:** do not pass auth object to transport if no auth data present ([d5eb4d8](https://github.com/sct/overseerr/commit/d5eb4d8d438a159266b2de66b6bcdd9440a0c8ef)), closes [#312](https://github.com/sct/overseerr/issues/312)
* **frontend:** add http/https prefix to hostname fields for plex/radarr/sonarr ([ce0266f](https://github.com/sct/overseerr/commit/ce0266f74ea3979b291ff962271a928682892788)), closes [#357](https://github.com/sct/overseerr/issues/357)
* **frontend:** clarify that radarr/sonnarr servers must be tested before profiles/folders appear ([fc12ab8](https://github.com/sct/overseerr/commit/fc12ab84d9482eb3a11f117f8cab6fd48a9401cd)), closes [#326](https://github.com/sct/overseerr/issues/326) [#328](https://github.com/sct/overseerr/issues/328)
* **frontend:** correctly show an unauthorized error when a user fails to login ([18925de](https://github.com/sct/overseerr/commit/18925decafdac518f52a354c594cc378d2529022)), closes [#322](https://github.com/sct/overseerr/issues/322)
* **frontend:** fix tv shows failing to open when firstAirDate is undefined ([c21fa5b](https://github.com/sct/overseerr/commit/c21fa5b5350abdd8e03c077fde7246fa398e176e)), closes [#347](https://github.com/sct/overseerr/issues/347)
* **frontend:** make minimum availability required for Radarr servers ([2fe53ec](https://github.com/sct/overseerr/commit/2fe53ec5a8534e75c7d0cef31a8b46065111e0a7)), closes [#345](https://github.com/sct/overseerr/issues/345)
* **plex-sync:** bundle duplicate ratingKeys to speed up recently added sync ([67146c3](https://github.com/sct/overseerr/commit/67146c33ef7f28d520ba2c50b32673d43f4525c8)), closes [#360](https://github.com/sct/overseerr/issues/360)
* **sonarr.ts, mediarequest.ts:** add missing seasonFolder option ([#358](https://github.com/sct/overseerr/issues/358)) ([e9c899c](https://github.com/sct/overseerr/commit/e9c899ce419d149dde2ad9a0f7d5a2f2545b3ebf))
### Features
* **frontend:** show alert when there are no default radarr/sonarr servers ([0d088e0](https://github.com/sct/overseerr/commit/0d088e085e68d39455fda21d1fd08ebcaef2c06b)), closes [#344](https://github.com/sct/overseerr/issues/344)
# [1.6.0](https://github.com/sct/overseerr/compare/v1.5.0...v1.6.0) (2020-12-16)
### Bug Fixes
* **api:** accept the api key to perform actions on the api with X-API-Key header ([33f8831](https://github.com/sct/overseerr/commit/33f8831e880dc7fd3f69d951246cada5c6c0ffe7))
* **api:** filter out libraries that do not have any metadata agent or are not movie/show ([01c179f](https://github.com/sct/overseerr/commit/01c179f762e686a1e5a3d4dab3a5bea53425b575))
* **api:** only run recently added sync on enabled libraries ([e08fa35](https://github.com/sct/overseerr/commit/e08fa35548bb8644afa8df3124e6f9cc3a2c8f4a)), closes [#259](https://github.com/sct/overseerr/issues/259)
* **api:** set plex libraries to disabled if the name changes ([675060b](https://github.com/sct/overseerr/commit/675060bcdf23acbfd4de2900a65f95e74f4966a5)), closes [#324](https://github.com/sct/overseerr/issues/324)
* **frontend:** adds a tip to plex setup to clarify that syncing runs in the background ([df4ac83](https://github.com/sct/overseerr/commit/df4ac8361f82971ee845f3be217408a9123a0bf3)), closes [#325](https://github.com/sct/overseerr/issues/325)
* **frontend:** aligned movie and tv details ([#331](https://github.com/sct/overseerr/issues/331)) ([db0a5c4](https://github.com/sct/overseerr/commit/db0a5c44f678e76eee7f5582381016306d1f46a2))
* **frontend:** close sidebar when clicking outside ([#333](https://github.com/sct/overseerr/issues/333)) ([6d7907e](https://github.com/sct/overseerr/commit/6d7907e844a909993d185759d660632f55aeaa35))
* spelling mistake on the word 'requested' fixed ([#319](https://github.com/sct/overseerr/issues/319)) ([961d110](https://github.com/sct/overseerr/commit/961d1107208069a6fc820a1ba97ffda7336677cb))
### Features
* add version to startup logs ([2948f93](https://github.com/sct/overseerr/commit/2948f9360eb484d1d6c0740a840135ca97e7240a))
* **frontend:** temporary logs page to clear up confusion about it 404ing ([d9788c4](https://github.com/sct/overseerr/commit/d9788c4aa9f87e2eda3f7e3f1adc985f16039552)), closes [#272](https://github.com/sct/overseerr/issues/272)
* **lang:** add support for Spanish language ([6cd2049](https://github.com/sct/overseerr/commit/6cd20491d2a0ceb995c4744eeb92a6e2f57a4893))
* **lang:** Translations update from Weblate ([#291](https://github.com/sct/overseerr/issues/291)) ([fddbb3c](https://github.com/sct/overseerr/commit/fddbb3cdfe3d50b2835c248556139c769dc2b805))
# [1.5.0](https://github.com/sct/overseerr/compare/v1.4.0...v1.5.0) (2020-12-15)
### Bug Fixes
* **api:** require package.json directly so typescript doesnt compile it into dist folder ([b9faa64](https://github.com/sct/overseerr/commit/b9faa6486b35aa865019aa8af9d307531054bc1d))
* **frontend:** add validation for Radarr/Sonarr server name ([b5988f9](https://github.com/sct/overseerr/commit/b5988f9a5ff274e97f208c2726abe76c22c858ee))
* **frontend:** only show alpha notice to admins ([ff61895](https://github.com/sct/overseerr/commit/ff618956b5d9cf933d867ea979b612c3d8a6f30b))
* add support for ssl when connecting to plex ([3ba09d0](https://github.com/sct/overseerr/commit/3ba09d07eb0367c41603cd55e7ff41c66fb641c4)), closes [#275](https://github.com/sct/overseerr/issues/275)
* **services:** improve logging for when Radarr movie already exists ([#285](https://github.com/sct/overseerr/issues/285)) ([f998873](https://github.com/sct/overseerr/commit/f998873fc5669a547901f2733c9c785d744d27ca)), closes [#260](https://github.com/sct/overseerr/issues/260)
### Features
* **lang:** add i18n strings for new about page ([900827b](https://github.com/sct/overseerr/commit/900827be97845688e4bea72a8c5d9611a3e9d069))
* about page initial version ([3f2a04c](https://github.com/sct/overseerr/commit/3f2a04c881bf06b73a952181fa463af84454b0dd))
# [1.4.0](https://github.com/sct/overseerr/compare/v1.3.2...v1.4.0) (2020-12-15)
### Bug Fixes
* changing parameter name to use correct 'port' [#276](https://github.com/sct/overseerr/issues/276) ([#277](https://github.com/sct/overseerr/issues/277)) ([6d08b10](https://github.com/sct/overseerr/commit/6d08b108200177ca3068c852e60a0df75ce2232a))
* **services:** include radarr/sonarr baseUrl when adding media ([78af1a3](https://github.com/sct/overseerr/commit/78af1a3e6d00a5645a05e7bf3cf56a59439b6cc9))
### Features
* **lang:** Translations update from Weblate ([#240](https://github.com/sct/overseerr/issues/240)) ([e17c637](https://github.com/sct/overseerr/commit/e17c63748362b6a480693e003ef5eec614dcec43))
## [1.3.2](https://github.com/sct/overseerr/compare/v1.3.1...v1.3.2) (2020-12-14)
### Bug Fixes
* **frontend:** convert plex port to a number before posting to the api ([8cb05c4](https://github.com/sct/overseerr/commit/8cb05c413a15a4b74e37ece5e24367d115995b32))
* **frontend:** converts email smtp port to a number before posting to the api ([2098a2d](https://github.com/sct/overseerr/commit/2098a2d3d2981fd2ae54392aec3ef81327f2858e)), closes [#251](https://github.com/sct/overseerr/issues/251)
* **frontend:** encode special characters in search input to prevent crashing router ([15013d6](https://github.com/sct/overseerr/commit/15013d6c5dbff15704c7c30d261d68a265e7f2d7)), closes [#252](https://github.com/sct/overseerr/issues/252)
* **plex sync:** catch errors that occur during processMovie ([edbbccf](https://github.com/sct/overseerr/commit/edbbccf3ae623430294f1a5c3fd2728dbd42e555)), closes [#244](https://github.com/sct/overseerr/issues/244) [#246](https://github.com/sct/overseerr/issues/246) [#250](https://github.com/sct/overseerr/issues/250)
* **services:** improve logging for adding movies to Radarr ([6c1ee83](https://github.com/sct/overseerr/commit/6c1ee830a183f89bb1fe96a181a7d61684e23b22))
* **services:** radarr/sonarr will use the correct default server ([0658b79](https://github.com/sct/overseerr/commit/0658b7943e1ab25816db9da34d4c9ea808d9203d))
## [1.3.1](https://github.com/sct/overseerr/compare/v1.3.0...v1.3.1) (2020-12-14)

View File

@@ -14,16 +14,46 @@ All help is welcome and greatly appreciated. If you would like to contribute to
### Getting Started
1. [Fork](https://help.github.com/articles/fork-a-repo/) the repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch `git checkout -b BRANCH_NAME`
```
git clone https://github.com/YOUR_USERNAME/overseerr.git
cd overseerr/
```
2. Add the remote upstream.
- Its recommended to name the branch something relevant to the feature or fix you are working on.
- An example of this would be `fix-title-cards` or `feature-new-system`.
- Bad examples would be `patch` or `bug`.
```
git remote add upstream https://github.com/sct/overseerr.git
```
3. Install dependencies `yarn`
4. `yarn dev` to build and watch for changes
3. Create a new branch
You can also run the development environment in [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
```
git checkout -b BRANCH_NAME develop
```
- Its recommended to name the branch something relevant to the feature or fix you are working on.
- An example of this would be `fix-title-cards` or `feature-new-system`.
- Bad examples would be `patch` or `bug`.
4. Run development environment
```
yarn
yarn dev
```
- Alternatively you can run using [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
5. Create your patch and run appropriate tests.
6. Follow the [guidelines](#contributing-code).
7. Should you need to update your fork you can do so by rebasing from upstream.
```
git fetch upstream
git rebase upstream/develop
git push origin BRANCH_NAME -f
```
### Contributing Code
@@ -33,7 +63,7 @@ You can also run the development environment in [Docker](https://www.docker.com/
- PRs with commits not following this standard will not be merged.
- Please make meaningful commits, or squash them
- Always rebase your commit to the latest `develop` branch. Do not merge develop into your branch.
- It is your responsbility to keep your branch up to date. It will not be merged unless its rebased off the latest develop branch.
- It is your responsibility to keep your branch up to date. It will not be merged unless its rebased off the latest develop branch.
- You can create a Draft pull request early to get feedback on your work.
- Your code must be formatted correctly or the tests will fail.
- We use Prettier to format our codebase. It should auto run with a git hook, but its recommended to have a Prettier extension installed in your editor and have it format on save.

View File

@@ -1,5 +1,8 @@
FROM node:12.18-alpine AS BUILD_IMAGE
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
COPY . /app
WORKDIR /app
@@ -12,6 +15,11 @@ RUN yarn cache clean
FROM node:12.18-alpine
ARG COMMIT_TAG
ENV COMMIT_TAG=${COMMIT_TAG}
RUN apk add tzdata
COPY . /app
WORKDIR /app
@@ -20,4 +28,8 @@ COPY --from=BUILD_IMAGE /app/dist ./dist
COPY --from=BUILD_IMAGE /app/.next ./.next
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
CMD yarn start
EXPOSE 5055

View File

@@ -16,7 +16,7 @@
<a href="https://lgtm.com/projects/g/sct/overseerr/context:javascript"><img alt="Language grade: JavaScript" src="https://img.shields.io/lgtm/grade/javascript/g/sct/overseerr.svg?logo=lgtm&logoWidth=18"/></a>
<img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-7-orange.svg"/></a>
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-31-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
</p>
@@ -36,15 +36,19 @@
- User profiles.
- User settings page (to give users the ability to modify their Overseerr experience to their liking).
- Version update notifications in-app.
- 4K requests (Includes multi-radarr/sonarr management for media)
- Local user system (for those who don't use Plex).
## Planned Features
- More notification types (Slack/Telegram/etc.).
- More notification types.
- Issues system. This will allow users to report issues with content on your media server.
- Local user system (for those who don't use Plex).
- Compatiblity APIs (to work with existing tools in your system).
- And a ton more! Check out our [issue tracker](https://github.com/sct/overseerr/issues) to see what features people have already requested.
## Getting Started
Check out our documentation for steps on how to install and run Overseerr:
https://docs.overseerr.dev/getting-started/installation
## Running Overseerr
@@ -54,7 +58,8 @@ Currently, Overseerr is only distributed through Docker images. If you have Dock
docker run -d \
-e LOG_LEVEL=info \
-e TZ=Asia/Tokyo \
-p 5055:3000 \
-e PROXY=<yes|no>
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
sctx/overseerr
@@ -66,17 +71,18 @@ After running Overseerr for the first time, configure it by visiting the web UI
## Preview
<img src="https://i.imgur.com/Mjbyruv.png">
<img src="./public/preview.jpg">
## Support
- Check out the [Overseerr Documentation](https://docs.overseerr.dev/) before asking for help. Your question might already be in the [FAQ](https://docs.overseerr.dev/support/faq).
- You can get support on [Discord](https://discord.gg/PkCWJSeCk7).
- You can ask questions in the Help category of our [GitHub Discussions](https://github.com/sct/overseerr/discussions).
- Bugs/Feature Requests can be opened via a [GitHub issue](https://github.com/sct/overseerr/issues).
## API Documentation
Full API documentation will soon be published automatically and available outside of running the app. Currently, you can access the API docs by running Overseerr locally and visiting http://localhost:3000/api-docs
Full API documentation will soon be published automatically and available outside of running the app. Currently, you can access the API docs by running Overseerr locally and visiting http://localhost:5055/api-docs
## Community
@@ -86,28 +92,62 @@ If you would like to chat with community members you can join the [Overseerr Dis
Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md) applies to all Overseerr community channels.
## Contributors
## Contributing
You can help build Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started.
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://sct.dev"><img src="https://avatars1.githubusercontent.com/u/234213?v=4" width="100px;" alt=""/><br /><sub><b>sct</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=sct" title="Code">💻</a> <a href="#design-sct" title="Design">🎨</a> <a href="#ideas-sct" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/azoitos"><img src="https://avatars2.githubusercontent.com/u/26529049?v=4" width="100px;" alt=""/><br /><sub><b>Alex Zoitos</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=azoitos" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/OwsleyJr"><img src="https://avatars3.githubusercontent.com/u/8635678?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Cohen</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Ahreluth"><img src="https://avatars2.githubusercontent.com/u/75682440?v=4" width="100px;" alt=""/><br /><sub><b>Ahreluth</b></sub></a><br /><a href="#translation-Ahreluth" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/KovalevArtem"><img src="https://avatars0.githubusercontent.com/u/36500228?v=4" width="100px;" alt=""/><br /><sub><b>KovalevArtem</b></sub></a><br /><a href="#translation-KovalevArtem" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/GiyomuWeb"><img src="https://avatars0.githubusercontent.com/u/62489209?v=4" width="100px;" alt=""/><br /><sub><b>GiyomuWeb</b></sub></a><br /><a href="#translation-GiyomuWeb" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/angrycuban13"><img src="https://avatars3.githubusercontent.com/u/39564898?v=4" width="100px;" alt=""/><br /><sub><b>Angry Cuban</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=angrycuban13" title="Documentation">📖</a></td>
<td align="center"><a href="https://sct.dev"><img src="https://avatars1.githubusercontent.com/u/234213?v=4?s=100" width="100px;" alt=""/><br /><sub><b>sct</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=sct" title="Code">💻</a> <a href="#design-sct" title="Design">🎨</a> <a href="#ideas-sct" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/azoitos"><img src="https://avatars2.githubusercontent.com/u/26529049?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Zoitos</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=azoitos" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/OwsleyJr"><img src="https://avatars3.githubusercontent.com/u/8635678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brandon Cohen</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Ahreluth"><img src="https://avatars2.githubusercontent.com/u/75682440?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ahreluth</b></sub></a><br /><a href="#translation-Ahreluth" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/KovalevArtem"><img src="https://avatars0.githubusercontent.com/u/36500228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>KovalevArtem</b></sub></a><br /><a href="#translation-KovalevArtem" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/GiyomuWeb"><img src="https://avatars0.githubusercontent.com/u/62489209?v=4?s=100" width="100px;" alt=""/><br /><sub><b>GiyomuWeb</b></sub></a><br /><a href="#translation-GiyomuWeb" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/angrycuban13"><img src="https://avatars3.githubusercontent.com/u/39564898?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Angry Cuban</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=angrycuban13" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/jvennik"><img src="https://avatars3.githubusercontent.com/u/6672637?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jvennik</b></sub></a><br /><a href="#translation-jvennik" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/darknessgp"><img src="https://avatars0.githubusercontent.com/u/1521243?v=4?s=100" width="100px;" alt=""/><br /><sub><b>darknessgp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=darknessgp" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/saltydk"><img src="https://avatars1.githubusercontent.com/u/6587950?v=4?s=100" width="100px;" alt=""/><br /><sub><b>salty</b></sub></a><br /><a href="#infra-saltydk" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/Shutruk"><img src="https://avatars2.githubusercontent.com/u/9198633?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Shutruk</b></sub></a><br /><a href="#translation-Shutruk" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/krystiancharubin"><img src="https://avatars2.githubusercontent.com/u/17775600?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Krystian Charubin</b></sub></a><br /><a href="#design-krystiancharubin" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/kieron"><img src="https://avatars2.githubusercontent.com/u/8655212?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kieron Boswell</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=kieron" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/samwiseg0"><img src="https://avatars1.githubusercontent.com/u/2241731?v=4?s=100" width="100px;" alt=""/><br /><sub><b>samwiseg0</b></sub></a><br /><a href="#question-samwiseg0" title="Answering Questions">💬</a> <a href="#infra-samwiseg0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/ecelebi29"><img src="https://avatars2.githubusercontent.com/u/8337120?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ecelebi29</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mmozeiko"><img src="https://avatars3.githubusercontent.com/u/1665010?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mārtiņš Možeiko</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=mmozeiko" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/mazzetta86"><img src="https://avatars2.githubusercontent.com/u/45591560?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mazzetta86</b></sub></a><br /><a href="#translation-mazzetta86" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Panzer1119"><img src="https://avatars1.githubusercontent.com/u/23016343?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Paul Hagedorn</b></sub></a><br /><a href="#translation-Panzer1119" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Shagon94"><img src="https://avatars3.githubusercontent.com/u/9140783?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Shagon94</b></sub></a><br /><a href="#translation-Shagon94" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/sebstrgg"><img src="https://avatars3.githubusercontent.com/u/27026694?v=4?s=100" width="100px;" alt=""/><br /><sub><b>sebstrgg</b></sub></a><br /><a href="#translation-sebstrgg" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/danshilm"><img src="https://avatars2.githubusercontent.com/u/20923978?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Danshil Mungur</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/doob187"><img src="https://avatars1.githubusercontent.com/u/60312740?v=4?s=100" width="100px;" alt=""/><br /><sub><b>doob187</b></sub></a><br /><a href="#infra-doob187" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/johnpyp"><img src="https://avatars2.githubusercontent.com/u/20625636?v=4?s=100" width="100px;" alt=""/><br /><sub><b>johnpyp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=johnpyp" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ankarhem"><img src="https://avatars1.githubusercontent.com/u/14110063?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jakob Ankarhem</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Documentation">📖</a> <a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jayesh100"><img src="https://avatars1.githubusercontent.com/u/8022175?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jayesh</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jayesh100" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/flying-sausages"><img src="https://avatars1.githubusercontent.com/u/23618693?v=4?s=100" width="100px;" alt=""/><br /><sub><b>flying-sausages</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=flying-sausages" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/hirenshah"><img src="https://avatars2.githubusercontent.com/u/418112?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hirenshah</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=hirenshah" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/TheCatLady"><img src="https://avatars0.githubusercontent.com/u/52870424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TheCatLady</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=TheCatLady" title="Code">💻</a> <a href="#translation-TheCatLady" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/chriscpritchard"><img src="https://avatars1.githubusercontent.com/u/1839074?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chris Pritchard</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=chriscpritchard" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=chriscpritchard" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Tamberlox"><img src="https://avatars3.githubusercontent.com/u/56069014?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tamberlox</b></sub></a><br /><a href="#translation-Tamberlox" title="Translation">🌍</a></td>
<td align="center"><a href="https://hmnd.io"><img src="https://avatars.githubusercontent.com/u/12853597?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=hmnd" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## Contributing
You can help build Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started.

View File

@@ -5,7 +5,7 @@ services:
context: .
dockerfile: Dockerfile.local
ports:
- 3000:3000
- 5055:5055
volumes:
- .:/app:rw,cached
- /app/node_modules

22
docs/README.md Normal file
View File

@@ -0,0 +1,22 @@
# Introduction
Welcome to the Overseerr Documentation.
## Features
- **Full Plex integration**. Login and manage user access with Plex.
- **Syncs to your Plex library** to show what titles you already have.
- **Integrates with Sonarr and Radarr**. With more services to come in the future.
- **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI.
- **Simple request management UI**. Don't dig through the app to approve recent requests.
- **Mobile-friendly design**, for when you need to approve requests on the go.
- Granular permission system.
- Localization into other languages.
## Motivation
The primary motivation for starting this project was to have an incredibly performant and easy to use application. There is a heavy focus on the user experience for both the server owner and the users. We feel requesting should be **effortless for the user**. Find the media you want, click request, and branch off efficiently into other titles that interest you, all in one seamless flow. For the server owner, Overseerr takes all the hassle out of approving your users' requests.
## We need your help!
Overseerr is an ambitious project. We have already poured a lot of work into this, with more coming. We need your valuable feedback and help with finding bugs. Also, being that this is an open-source project, anyone is welcome to contribute. Contribution includes building features, patching bugs, or even translating the application. You can find the contribution guide on our GitHub.

22
docs/SUMMARY.md Normal file
View File

@@ -0,0 +1,22 @@
# Table of contents
- [Introduction](README.md)
## Getting Started
- [Installation](getting-started/installation.md)
## Using Overseerr
- [Notifications](using-overseerr/notifications/README.md)
- [Custom Webhooks](using-overseerr/notifications/webhooks.md)
## Support
- [Frequently Asked Questions](support/faq.md)
- [Asking for Support](support/asking-for-support.md)
## Extending Overseerr
* [Reverse Proxy Examples](extending-overseerr/reverse-proxy-examples.md)
* [Fail2ban Filter](extending-overseerr/fail2ban.md)

View File

@@ -0,0 +1,14 @@
# Fail2ban Filter
{% hint style="warning" %}
If you are running Overseerr behind a reverse proxy, make sure that the **Enable Proxy Support** setting is **enabled**.
{% endhint %}
To use Fail2ban with Overseerr, create a new file named `overseerr.local` in your Fail2ban `filter.d` directory with the following filter definition:
```
[Definition]
failregex = .*\[info\]\[Auth\]\: Failed login attempt.*"ip":"<HOST>"
```
You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.

View File

@@ -0,0 +1,135 @@
# Reverse Proxy Examples
{% hint style="warning" %}
Base URLs cannot be configured in Overseerr. With this limitation, only subdomain configurations are supported.
{% endhint %}
## LE/SWAG
### Subdomain
A sample is bundled in SWAG. This page is still the only source of truth, so the sample is not guaranteed to be up to date. If you catch an inconsistency, report it to the linuxserver team, or do a pull-request against the proxy-confs repository to update the sample.
Rename the sample file `overseerr.subdomain.conf.sample` to `overseerr.subdomain.conf` in the `proxy-confs`folder, or create `overseerr.subdomain.conf` in the same folder with the example below.
Example Configuration:
```nginx
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name overseerr.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
location / {
include /config/nginx/proxy.conf;
resolver 127.0.0.11 valid=30s;
set $upstream_app overseerr;
set $upstream_port 5055;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
## Traefik \(v2\)
Add the labels to the Overseerr service in your `docker-compose` file. A basic example for a `docker-compose` file using Traefik can be found [here](https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/).
### Subdomain
Example Configuration:
```text
labels:
- "traefik.enable=true"
## HTTP Routers
- "traefik.http.routers.overseerr-rtr.entrypoints=https"
- "traefik.http.routers.overseerr-rtr.rule=Host(`overseerr.domain.com`)"
- "traefik.http.routers.overseerr-rtr.tls=true"
## HTTP Services
- "traefik.http.routers.overseerr-rtr.service=overseerr-svc"
- "traefik.http.services.overseerr-svc.loadbalancer.server.port=5055"
```
## LE/NGINX
### Subdomain
Take the configuration below and place it in `/etc/nginx/sites-available/overseerr.example.com.conf`.
Create a symlink to `/etc/nginx/sites-enabled`:
```text
sudo ln -s /etc/nginx/sites-available/overseerr.example.com.conf /etc/nginx/sites-enabled/overseerr.example.com.conf
```
Test the configuration:
```text
sudo nginx -t
```
Reload your configuration for NGINX:
```text
sudo systemctl reload nginx
```
Example Configuration:
```text
server {
listen 80;
server_name overseerr.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name overseerr.example.com;
ssl_certificate /etc/letsencrypt/live/overseerr.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/overseerr.example.com/privkey.pem;
proxy_set_header Referer $http_referer;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-Host $host:$remote_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Ssl on;
real_ip_header CF-Connecting-IP;
# Control the behavior of the Referer header (Referrer-Policy)
add_header Referrer-Policy "no-referrer";
# HTTP Strict Transport Security
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# Reduce XSS risks (Content-Security-Policy) - uncomment to use and add URLs whenever necessary
# add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://plex.tv; style-src 'self' 'unsafe-inline' https://rsms.me/inter/inter.css; script-src 'self'; img-src 'self' data: https://plex.tv https://assets.plex.tv https://gravatar.com https://i2.wp.com https://image.tmdb.org; font-src 'self' https://rsms.me/inter/font-files/" always;
# Prevent some categories of XSS attacks (X-XSS-Protection)
add_header X-XSS-Protection "1; mode=block" always;
# Provide clickjacking protection (X-Frame-Options)
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent Sniff Mimetype (X-Content-Type-Options)
add_header X-Content-Type-Options "nosniff" always;
# Tell crawling bots to not index the site
add_header X-Robots-Tag "noindex, nofollow" always;
access_log /var/log/nginx/overseerr.example.com-access.log;
error_log /var/log/nginx/overseerr.example.com-error.log;
location / {
proxy_pass http://127.0.0.1:5055;
}
}
```

View File

@@ -0,0 +1,183 @@
# Installation
{% hint style="danger" %}
Overseerr is currently under very heavy, rapid development and things are likely to break often. We need all the help we can get to find bugs and get them fixed to hit a more stable release. If you would like to help test the bleeding edge, please use the image **`sctx/overseerr:develop`** instead!
{% endhint %}
{% hint style="info" %}
After running Overseerr for the first time, configure it by visiting the web UI at `http://[address]:5055` and completing the setup steps.
{% endhint %}
## Docker
{% tabs %}
{% tab title="Basic" %}
```bash
docker run -d \
-e LOG_LEVEL=info \
-e TZ=Asia/Tokyo \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
sctx/overseerr
```
{% endtab %}
{% tab title="UID/GID" %}
```text
docker run -d \
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
-e LOG_LEVEL=info \
-e TZ=Asia/Tokyo \
-p 5055:5055 \
-v /path/to/appdata/config:/app/config \
--restart unless-stopped \
sctx/overseerr
```
{% endtab %}
{% tab title="Manual Update" %}
```text
# Stop the Overseerr container
docker stop overseerr
# Remove the Overseerr container
docker rm overseerr
# Pull the latest update
docker pull sctx/overseerr
# Run the Overseerr container with the same parameters as before
docker run -d ...
```
{% endtab %}
{% endtabs %}
{% hint style="info" %}
Use a 3rd party updating mechanism such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros) to keep Overseerr up-to-date automatically.
{% endhint %}
## Unraid
1. Ensure you have the **Community Applications** plugin installed.
2. Inside the **Communtiy Applications** app store, search for **Overseerr**.
3. Click the **Install Button**.
4. On the following **Add Container** screen, make changes to the **Host Port** and **Host Path 1**\(Appdata\) as needed.
5. Click apply and access "Overseerr" at your `<ServerIP:HostPort>` in a web browser.
## Windows
Please refer to the [docker for windows documentation](https://docs.docker.com/docker-for-windows/) for installation.
{% hint style="danger" %}
**WSL2 will need to be installed to prevent DB corruption! Please see** [**Docker Desktop WSL 2 backend**](https://docs.docker.com/docker-for-windows/wsl/) **on how to enable WSL2. The command below will only work with WSL2 installed!**
{% endhint %}
```bash
docker run -d -e LOG_LEVEL=info -e TZ=Asia/Tokyo -p 5055:5055 -v "/your/path/here:/app/config" --restart unless-stopped sctx/overseerr
```
{% hint style="info" %}
Docker on Windows works differently than it does on Linux; it uses a VM to run a stripped-down Linux and then runs docker within that. The volume mounts are exposed to the docker in this VM via SMB mounts. While this is fine for media, it is unacceptable for the `/app/config` directory because SMB does not support file locking. This will eventually corrupt your database which can lead to slow behavior and crashes. If you must run in docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host. It's worth noting that this warning also extends to other containers which use SQLite databases.
{% endhint %}
## Linux
{% hint style="info" %}
The [Overseerr snap](https://snapcraft.io/overseerr) is the only supported linux install method. Currently, the listening port cannot be changed. Port `5055` will need to be available on your host. To install snapd please refer to [Installing snapd](https://snapcraft.io/docs/installing-snapd).
{% endhint %}
**To install:**
```
sudo snap install overseerr
```
**Updating:**
Snap will keep Overseerr up-to-date automatically. You can force a refresh by using the following command.
```
sudo snap refresh
```
**To install the development build:**
```
sudo snap install overseerr --edge
```
{% hint style="danger" %}
This version can break any moment. Be prepared to troubleshoot any issues that arise!
{% endhint %}
## Third Party
{% tabs %}
{% tab title="Gentoo" %}
Portage overlay [GitHub Repository](https://github.com/chriscpritchard/overseerr-overlay).
This is now included in the list of [Gentoo repositories](https://overlays.gentoo.org/), so can be easily enabled with `eselect repository`
Efforts will be made to keep up to date with the latest releases, however, this cannot be guaranteed.
**To enable:**
To enable using `eselect repository`, run:
```bash
eselect repository enable overseerr-overlay
```
**To install:**
Once complete, you can just run:
```bash
emerge www-apps/overseerr
```
**To install the development build:**
A live ebuild (`=www-apps/overseerr-9999`) is also available. To use this, you will need to modify accept_keywords for this package:
```bash
emerge --autounmask --autounmask-write "=www-apps/overseerr-9999"
```
Once installed, you will not be notified of updates, so you can update with:
```bash
emerge @live-rebuild
```
or use `app-portage/smart-live-rebuild`
{% hint style="danger" %}
This version can break any moment. Be prepared to troubleshoot any issues that arise!
{% endhint %}
{% endtab %}
{% tab title="Swizzin" %}
The installation is not implemented via Docker, but barebones. The latest release version of Overseerr will be used.
Please see the [swizzin documentation](https://swizzin.ltd/applications/overseerr) for more information.
To install, run the following:
```bash
box install overseerr
```
To upgrade, run the following:
```bash
box upgrade overseerr
```
{% endtab %}
{% endtabs %}

View File

@@ -0,0 +1,34 @@
# Asking for Support
## Before asking for support, make sure you try these things first
* Make sure you have **updated** to the latest version.
* ["Have you tried turning it off and on again?"](https://www.youtube.com/watch?v=nn2FB1P_Mn8)
* **Analyzing** your logs, you just might find the solution yourself!
* **Search** the [Wiki](../), [Installation Guides](../getting-started/installation.md), and [FAQs](faq.md).
* If you have questions, feel free to ask them on [Discord](https://discord.gg/PkCWJSeCk7) \(Please review our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md)\). Please include a link to your logs. See [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) for more details.
## What should I include when asking for support?
When you contact support saying something like "it doesn't work" leaves little to go on to figure out what is wrong for you. When contacting support try to include information such as the following:
* What did you try to do? When you describe what you did to reach the state you are in we may notice something you did different from the instructions, or something that your unique setup requires in addition. Some examples of what to provide here:
* What command did you enter?
* What did you click on?
* What settings did you change?
* Provide a step-by-step list of what you tried.
* What do you see? We cannot see your screen so some of the following is necessary for us to know what is going on:
* Did something happen?
* Did something not happen?
* Are there any error messages showing?
* Screenshots can help us see what you are seeing
* The Overseerr logs show exactly what happened and are often critical for identifying issues \(see [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) below\).
## How can I share my logs?
First you will need to gather your logs from the install directory.
1. Collect the log file from `<Overseeerr-install-directory>/logs/overseerr.log`
2. Open the log file and **upload the text** by going to [gist.github.com](https://gist.github.com/) and creating a new secret Gist of the contents.
3. **Share the link** with support in [Discord](https://discord.gg/PkCWJSeCk7) by copying the URL of the page.

95
docs/support/faq.md Normal file
View File

@@ -0,0 +1,95 @@
# Frequently Asked Questions
{% hint style="info" %}
If you can't find a solution here, please ask on [Discord](https://discord.gg/PkCWJSeCk7). Please do not post questions on the GitHub issues tracker.
{% endhint %}
## General
### I receive 409 or 400 errors when requesting a movie or tv show!
**A:** Verify your are running radarr and sonarr v3. Overseerr was developed for v3 and is not currently backward compatible.
### How do I keep Overseerr up-to-date?
**A:** Use a 3rd party updating mechanism such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros) to keep Overseerr up-to-date automatically.
### How can I access Overseerr outside my home network?
**A:** The easy and least secure method is to forward an external port \(`5055`\) on your router to the internal port used by Overseerr \(default is TCP `5055`\). Visit [Port Forward](http://portforward.com/) for instructions for your particular router. You will then be able to access Overseerr via `http://EXTERNAL-IP-ADDRESS:5055`.
The more advanced and most preferred method \(and more secure if you use SSL\) is to set up a web server with NGINX/Apache, and use a reverse proxy to access Overseerr. You can lookup many guides on the internet to find out how to do this. There are several reverse proxy config examples located [here](../extending-overseerr/reverse-proxy-examples.md).
The most secure method, but also the most inconvenient, is to set up a VPN tunnel to your home server, then you can access Overseerr as if it is on a local network via `http://LOCAL-IP-ADDRESS:5055`.
### Overseerr is amazing! But it is not translated in my language yet! Can I help with translations?
**A:** You sure can! We are using Weblate for translations! Check it out [here](https://hosted.weblate.org/engage/overseerr/). If your language is not listed please open an [enhancement request in issues](https://github.com/sct/overseerr/issues/new/choose).
### Where can I find the changelog?
**A:** You can find the changelog in the **Settings -&gt; About** page in your instance. You can also find it on github [here](https://github.com/sct/overseerr/releases).
### Can I make 4K requests?
**A:** 4K requests are not supported just yet but they will be supported in the future!
### Some media is missing from Overseerr that I know is in Plex!
**A:** Overseerr supports the new Plex Movie, legacy Plex Movie, TheTVDB, and TMDb agents. Please verify that your library is using one of the agents previously listed. If you are changing agents, a full metadata refresh will need to be performed. Caution, this can take a long time depending on how many items you have in your movie library.
**Troubleshooting Steps:**
Check the Overseerr logs for media items that are missing. The logs will contain an error as to why that item could not be matched. One example might be `errorMessage":"SQLITE_CONSTRAINT: NOT NULL`. This means that the TMDb ID is missing from the Plex XML for that item.
1. Verify that you are using one of the agents mentioned above.
2. Refresh the metadata for just that item.
3. Run a full scan in Overseerr to see if that item is now matched properly.
4. If the item is now seen by Overseerr then repeat step 2 for each missing item. If you have a large amount of items missing then a full metadata refresh is recommended for that library.
5. Run a full scan on Overseerr after refreshing all unmatched items.
Perform these steps to verify the media item has a guid Overseerr can match.
1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**.
2. Verify that the media item has the same format of one of the examples below.
**Examples:**
1. TMDb agent `guid="com.plexapp.agents.themoviedb://1705"`
2. New Plex Movie agent `<Guid id="tmdb://464052"/>`
3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"`
4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"`
### Where can I find the logs?
**A:** The logs are located at `<Overseeerr-install-directory>/logs/overseerr.log`
## User Management
### Why can't I see all my Plex users?
**A:** Navigate to your **User List** in Overseerr and click **Import Users from Plex** button. Don't forget to check the default user permissions in the **Settings -&gt; General Settings** page beforehand.
### Can I create local users in Overseerr?
**A:** Not at this time. But it is a planned feature!
### Is is possible to set user roles in Overseerr?
**A:** Unfortunately, this is not possible yet. It is planned!
## Requests
### I approved a requested movie and radarr didn't search for it!
**A:** Check your minimum availability in radarr. If an added item does not meet the minimum availability, no search will be performed. Also verify that radarr did not search for it by checking the radarr logs. Lastly, verify the item was not already being monitored by radarr. Currently there is no state sync with radarr.
### Help! My request still shows "requested" even though it's in Plex!?!
**A:** See "[Some media is missing from Overseerr that I know is in Plex!](./faq.md#some-media-is-missing-from-overseerr-that-i-know-is-in-plex)" for troubleshooting steps.
## Notifications
### I am getting "Username and Password not accepted" when sending email notifications to gmail!
**A:** If you have 2-Step Verification enabled on your account you will need to create an app password. More details can be found [here](https://support.google.com/mail/answer/185833).

View File

@@ -0,0 +1,28 @@
# Notifications
Overseerr already supports a good number of notification agents, such as **Discord**, **Slack** and **Pushover**. New agents are always considered for development, if there is enough demand for it.
## Currently Supported Notification Agents
- Email
- Discord
- Slack
- Telegram
- Pushover
- [Webhooks](./webhooks.md)
## Setting Up Notifications
Configuring your notifications is _very simple_. First, you will need to visit the **Settings** page and click **Notifications** in the menu. This will present you with all of the currently available notification agents. Click on each one individually to configure them.
You must configure which type of notifications you want to send _per agent_. If no types are selected, you will not receive any notifications!
Some agents may have specific configuration gotchas that will be covered in each notification agents documentation page.
{% hint style="danger" %}
Currently, you will **not receive notifications** for any auto-approved requests. However, you will still receive a notification when the media becomes available.
{% endhint %}
## Requesting new agents
If we do not currently support a notification agent you would like, feel free to request it on our [GitHub Issues](https://github.com/sct/overseerr/issues). Make sure to search first to see if someone else already requested it!

View File

@@ -0,0 +1,56 @@
# Webhooks
Webhooks let you post a custom JSON payload to any endpoint you like. You can also set an authorization header for security purposes.
## Configuration
The following configuration options are available:
### Webhook URL (Required)
The URL you would like to post notifications to. Your JSON will be sent as the body of the request.
### Authorization Header
Custom authorization header. Anything entered for this will be sent as an `Authorization` header.
### Custom JSON Payload (Required)
Design your JSON payload as you see fit. JSON is validated before you can save or test. Overseerr provides several [template variables](./webhooks.md#template-variables) for use in the payload which will be replaced with actual values when the notifications are sent.
You can always reset back to the default custom payload setting by clicking the `Reset to Default JSON Payload` button under the editor.
## Template Variables
### Main
- `{{notification_type}}` The type of notification. (Ex. `MEDIA_PENDING` or `MEDIA_APPROVED`)
- `{{subject}}` The notification subject message. (For request notifications, this is the media title)
- `{{message}}` Notification message body. (For request notifications, this is the media's overview/synopsis)
- `{{image}}` Associated image with the request. (For request notifications, this is the media's poster)
### Notify User
These variables are usually the target user of the notification.
- `{{notifyuser_username}}` Target user's username.
- `{{notifyuser_email}}` Target user's email.
- `{{notifyuser_avatar}}` Target user's avatar.
### Media
These variables are only included in media related notifications, such as requests.
- `{{media_type}}` Media type. Either `movie` or `tv`.
- `{{media_tmdbid}}` Media's TMDb ID.
- `{{media_imdbid}}` Media's IMDb ID.
- `{{media_tvdbid}}` Media's TVDB ID.
- `{{media_status}}` Media's availability status. (Ex. `AVAILABLE` or `PENDING`)
- `{{media_status4k}}` Media's 4K availability status. (Ex. `AVAILABLE` or `PENDING`)
### Special Key Variables
These variables must be used as a key in the JSON Payload. (Ex, `"{{extra}}": []`).
- `{{extra}}` This will override the value of the property to be the pre-formatted "extra" array that can come along with certain notifications. Using this variable is _not required_.
- `{{media}}` This will override the value of the property to `null` if there is no media object passed along with the notification.

View File

@@ -1,4 +1,7 @@
module.exports = {
env: {
commitTag: process.env.COMMIT_TAG || 'local',
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/,

View File

@@ -1,10 +1,14 @@
const devConfig = {
type: 'sqlite',
database: 'config/db/db.sqlite3',
database: process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
: 'config/db/db.sqlite3',
synchronize: true,
migrationsRun: false,
logging: false,
entities: ['server/entity/**/*.ts'],
migrations: ['server/migration/**/*.ts'],
subscribers: ['server/subscriber/**/*.ts'],
cli: {
entitiesDir: 'server/entity',
migrationsDir: 'server/migration',
@@ -13,12 +17,15 @@ const devConfig = {
const prodConfig = {
type: 'sqlite',
database: 'config/db/db.sqlite3',
database: process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
: 'config/db/db.sqlite3',
synchronize: false,
logging: false,
entities: ['dist/entity/**/*.js'],
migrations: ['dist/migration/**/*.js'],
migrationsRun: true,
migrationsRun: false,
subscribers: ['dist/subscriber/**/*.js'],
cli: {
entitiesDir: 'dist/entity',
migrationsDir: 'dist/migration',

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "overseerr",
"version": "1.3.1",
"version": "1.18.0",
"private": true,
"scripts": {
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node --files --project server/tsconfig.json server/index.ts",
@@ -11,51 +11,63 @@
"start": "NODE_ENV=production node dist/index.js",
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault false './src/**/!(*.test).{ts,tsx}'",
"migration:generate": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:generate",
"migration:create": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:create",
"migration:run": "ts-node --project server/tsconfig.json ./node_modules/.bin/typeorm migration:run",
"format": "prettier --write ."
},
"license": "MIT",
"dependencies": {
"@supercharge/request-ip": "^1.1.2",
"@svgr/webpack": "^5.5.0",
"axios": "^0.21.0",
"ace-builds": "^1.4.12",
"axios": "^0.21.1",
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0",
"bowser": "^2.11.0",
"connect-typeorm": "^1.1.4",
"cookie-parser": "^1.4.5",
"email-templates": "^8.0.2",
"csurf": "^1.11.0",
"email-templates": "^8.0.3",
"express": "^4.17.1",
"express-openapi-validator": "^4.8.0",
"express-openapi-validator": "^4.10.8",
"express-session": "^1.17.1",
"formik": "^2.2.5",
"formik": "^2.2.6",
"gravatar-url": "^3.1.0",
"intl": "^1.2.5",
"lodash": "^4.17.20",
"next": "^10.0.3",
"next": "10.0.3",
"node-schedule": "^1.3.2",
"nodemailer": "^6.4.16",
"nookies": "^2.5.0",
"nodemailer": "^6.4.17",
"nookies": "^2.5.2",
"plex-api": "^5.3.1",
"pug": "^3.0.0",
"react": "17.0.1",
"react-ace": "^9.2.1",
"react-animate-height": "^2.0.23",
"react-dom": "17.0.1",
"react-intersection-observer": "^8.31.0",
"react-intl": "^5.10.6",
"react-intl": "^5.10.16",
"react-markdown": "^5.0.3",
"react-spring": "^8.0.27",
"react-toast-notifications": "^2.4.0",
"react-transition-group": "^4.4.1",
"react-use-clipboard": "1.0.2",
"react-truncate-markup": "^5.1.0",
"react-use-clipboard": "1.0.7",
"reflect-metadata": "^0.1.13",
"secure-random-password": "^0.2.2",
"sqlite3": "^5.0.0",
"swagger-ui-express": "^4.1.5",
"swr": "^0.3.9",
"typeorm": "^0.2.29",
"swagger-ui-express": "^4.1.6",
"swr": "^0.4.0",
"typeorm": "^0.2.30",
"uuid": "^8.3.2",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0",
"xml2js": "^0.4.23",
"yamljs": "^0.3.0",
"yup": "^0.32.8"
},
"devDependencies": {
"@babel/cli": "^7.12.8",
"@babel/cli": "^7.12.10",
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@semantic-release/changelog": "^5.0.1",
@@ -64,52 +76,58 @@
"@semantic-release/git": "^9.0.0",
"@tailwindcss/aspect-ratio": "^0.2.0",
"@tailwindcss/forms": "^0.2.1",
"@tailwindcss/typography": "^0.3.1",
"@tailwindcss/typography": "^0.4.0",
"@types/bcrypt": "^3.0.0",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/email-templates": "^7.1.0",
"@types/express": "^4.17.9",
"@types/csurf": "^1.11.0",
"@types/email-templates": "^8.0.0",
"@types/express": "^4.17.11",
"@types/express-session": "^1.17.0",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.11",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.22",
"@types/node-schedule": "^1.3.1",
"@types/nodemailer": "^6.4.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-toast-notifications": "^2.4.0",
"@types/react-transition-group": "^4.4.0",
"@types/secure-random-password": "^0.2.0",
"@types/swagger-ui-express": "^4.1.2",
"@types/uuid": "^8.3.0",
"@types/xml2js": "^0.4.7",
"@types/xml2js": "^0.4.8",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.10",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.9.1",
"@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"autoprefixer": "^9",
"babel-plugin-react-intl": "^8.2.21",
"babel-plugin-react-intl": "^8.2.25",
"babel-plugin-react-intl-auto": "^3.3.0",
"commitizen": "^4.2.2",
"commitizen": "^4.2.3",
"copyfiles": "^2.4.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.15.0",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-formatjs": "^2.9.10",
"eslint": "^7.18.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-formatjs": "^2.10.3",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.2.0",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"extract-react-intl-messages": "^4.1.1",
"husky": "^4.3.5",
"husky": "^4.3.8",
"lint-staged": "^10.5.3",
"nodemon": "^2.0.6",
"nodemon": "^2.0.7",
"postcss": "^7",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.2.1",
"semantic-release": "^17.3.0",
"semantic-release": "^17.3.6",
"semantic-release-docker": "^2.2.0",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"ts-node": "^9.1.1",
"typescript": "^4.1.2"
"typescript": "^4.1.3"
},
"resolutions": {
"sqlite3/node-gyp": "^5.1.0"
},
"config": {
"commitizen": {
@@ -167,10 +185,16 @@
[
"@semantic-release/exec",
{
"prepareCmd": "docker build -t sctx/overseerr ."
"prepareCmd": "docker build --build-arg COMMIT_TAG=$GITHUB_SHA -t sctx/overseerr ."
}
],
"semantic-release-docker"
"semantic-release-docker",
[
"@semantic-release/github",
{
"addReleases": "bottom"
}
]
],
"branches": [
"master"
@@ -180,7 +204,8 @@
{
"path": "semantic-release-docker",
"name": "sctx/overseerr"
}
},
"@semantic-release/github"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 407 KiB

BIN
public/images/rotate5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

BIN
public/images/rotate6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 40 KiB

BIN
public/preview.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

View File

@@ -1 +1,20 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{
"name": "Overseerr",
"short_name": "Overseerr",
"start_url": "./",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#2d3748",
"background_color": "#2d3748",
"display": "standalone"
}

223
server/api/animelist.ts Normal file
View File

@@ -0,0 +1,223 @@
import axios from 'axios';
import xml2js from 'xml2js';
import fs, { promises as fsp } from 'fs';
import path from 'path';
import logger from '../logger';
const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds
// originally at https://raw.githubusercontent.com/ScudLee/anime-lists/master/anime-list.xml
const MAPPING_URL =
'https://raw.githubusercontent.com/Anime-Lists/anime-lists/master/anime-list.xml';
const LOCAL_PATH = path.join(__dirname, '../../config/anime-list.xml');
const mappingRegexp = new RegExp(/;[0-9]+-([0-9]+)/g);
// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to TVDB/TMDb IDs
// https://github.com/Anime-Lists/anime-lists/
interface AnimeMapping {
$: {
anidbseason: string;
tvdbseason: string;
};
_: string;
}
interface Anime {
$: {
anidbid: number;
tvdbid?: string;
defaulttvdbseason?: string;
tmdbid?: number;
imdbid?: string;
};
'mapping-list'?: {
mapping: AnimeMapping[];
}[];
}
interface AnimeList {
'anime-list': {
anime: Anime[];
};
}
export interface AnidbItem {
tvdbId?: number;
tmdbId?: number;
imdbId?: string;
}
class AnimeListMapping {
private syncing = false;
private mapping: { [anidbId: number]: AnidbItem } = {};
// mapping file modification date when it was loaded
private mappingModified: Date | null = null;
// each episode in season 0 from TVDB can map to movie
private specials: { [tvdbId: number]: { [episode: number]: AnidbItem } } = {};
public isLoaded = () => Object.keys(this.mapping).length !== 0;
private loadFromFile = async () => {
logger.info('Loading mapping file', { label: 'Anime-List Sync' });
try {
const mappingStat = await fsp.stat(LOCAL_PATH);
const file = await fsp.readFile(LOCAL_PATH);
const xml = (await xml2js.parseStringPromise(file)) as AnimeList;
this.mapping = {};
this.specials = {};
for (const anime of xml['anime-list'].anime) {
// tvdbId can be nonnumber, like 'movie' string
let tvdbId: number | undefined;
if (anime.$.tvdbid && !isNaN(Number(anime.$.tvdbid))) {
tvdbId = Number(anime.$.tvdbid);
} else {
tvdbId = undefined;
}
let imdbIds: (string | undefined)[];
if (anime.$.imdbid) {
// if there are multiple imdb entries, then they map to different movies
imdbIds = anime.$.imdbid.split(',');
} else {
// in case there is no imdbid, that's ok as there will be tmdbid
imdbIds = [undefined];
}
const tmdbId = anime.$.tmdbid ? Number(anime.$.tmdbid) : undefined;
const anidbId = Number(anime.$.anidbid);
this.mapping[anidbId] = {
// for season 0 ignore tvdbid, because this must be movie/OVA
tvdbId: anime.$.defaulttvdbseason === '0' ? undefined : tvdbId,
tmdbId: tmdbId,
imdbId: imdbIds[0], // this is used for one AniDB -> one imdb movie mapping
};
if (tvdbId) {
const mappingList = anime['mapping-list'];
if (mappingList && mappingList.length != 0) {
let imdbIndex = 0;
for (const mapping of mappingList[0].mapping) {
const text = mapping._;
if (text && mapping.$.tvdbseason === '0') {
let matches;
while ((matches = mappingRegexp.exec(text)) !== null) {
const episode = Number(matches[1]);
if (!this.specials[tvdbId]) {
this.specials[tvdbId] = {};
}
// map next available imdbid to episode in s0
const imdbId =
imdbIndex > imdbIds.length ? undefined : imdbIds[imdbIndex];
if (tmdbId || imdbId) {
this.specials[tvdbId][episode] = {
tmdbId: tmdbId,
imdbId: imdbId,
};
imdbIndex++;
}
}
}
}
} else {
// some movies do not have mapping-list, so map episode 1,2,3,..to movies
// movies must have imdbId or tmdbId
const hasImdb = imdbIds.length > 1 || imdbIds[0] !== undefined;
if ((hasImdb || tmdbId) && anime.$.defaulttvdbseason === '0') {
if (!this.specials[tvdbId]) {
this.specials[tvdbId] = {};
}
// map each imdbid to episode in s0, episode index starts with 1
for (let idx = 0; idx < imdbIds.length; idx++) {
this.specials[tvdbId][idx + 1] = {
tmdbId: tmdbId,
imdbId: imdbIds[idx],
};
}
}
}
}
}
this.mappingModified = mappingStat.mtime;
logger.info(
`Loaded ${
Object.keys(this.mapping).length
} AniDB items from mapping file`,
{ label: 'Anime-List Sync' }
);
} catch (e) {
throw new Error(`Failed to load Anime-List mappings: ${e.message}`);
}
};
private downloadFile = async () => {
logger.info('Downloading latest mapping file', {
label: 'Anime-List Sync',
});
try {
const response = await axios.get(MAPPING_URL, {
responseType: 'stream',
});
await new Promise<void>((resolve) => {
const writer = fs.createWriteStream(LOCAL_PATH);
writer.on('finish', resolve);
response.data.pipe(writer);
});
} catch (e) {
throw new Error(`Failed to download Anime-List mapping: ${e.message}`);
}
};
public sync = async () => {
// make sure only one sync runs at a time
if (this.syncing) {
return;
}
this.syncing = true;
try {
// check if local file is not "expired" yet
if (fs.existsSync(LOCAL_PATH)) {
const now = new Date();
const stat = await fsp.stat(LOCAL_PATH);
if (now.getTime() - stat.mtime.getTime() < UPDATE_INTERVAL_MSEC) {
if (!this.isLoaded()) {
// no need to download, but make sure file is loaded
await this.loadFromFile();
} else if (
this.mappingModified &&
stat.mtime.getTime() > this.mappingModified.getTime()
) {
// if file has been modified externally since last load, reload it
await this.loadFromFile();
}
return;
}
}
await this.downloadFile();
await this.loadFromFile();
} finally {
this.syncing = false;
}
};
public getFromAnidbId = (anidbId: number): AnidbItem | undefined => {
return this.mapping[anidbId];
};
public getSpecialEpisode = (
tvdbId: number,
episode: number
): AnidbItem | undefined => {
const episodes = this.specials[tvdbId];
return episodes ? episodes[episode] : undefined;
};
}
const animeList = new AnimeListMapping();
export default animeList;

View File

@@ -1,13 +1,17 @@
import NodePlexAPI from 'plex-api';
import { getSettings } from '../lib/settings';
import { getSettings, PlexSettings } from '../lib/settings';
export interface PlexLibraryItem {
ratingKey: string;
parentRatingKey?: string;
grandparentRatingKey?: string;
title: string;
guid: string;
parentGuid?: string;
type: 'movie' | 'show' | 'season';
grandparentGuid?: string;
addedAt: number;
updatedAt: number;
type: 'movie' | 'show' | 'season' | 'episode';
}
interface PlexLibraryResponse {
@@ -20,6 +24,7 @@ export interface PlexLibrary {
type: 'show' | 'movie';
key: string;
title: string;
agent: string;
}
interface PlexLibrariesResponse {
@@ -45,6 +50,25 @@ export interface PlexMetadata {
parentIndex?: number;
leafCount: number;
viewedLeafCount: number;
addedAt: number;
updatedAt: number;
Media: Media[];
}
interface Media {
id: number;
duration: number;
bitrate: number;
width: number;
height: number;
aspectRatio: number;
audioChannels: number;
audioCodec: string;
videoCodec: string;
videoResolution: string;
container: string;
videoFrameRate: string;
videoProfile: string;
}
interface PlexMetadataResponse {
@@ -56,12 +80,26 @@ interface PlexMetadataResponse {
class PlexAPI {
private plexClient: NodePlexAPI;
constructor({ plexToken }: { plexToken?: string }) {
constructor({
plexToken,
plexSettings,
timeout,
}: {
plexToken?: string;
plexSettings?: PlexSettings;
timeout?: number;
}) {
const settings = getSettings();
let settingsPlex: PlexSettings | undefined;
plexSettings
? (settingsPlex = plexSettings)
: (settingsPlex = getSettings().plex);
this.plexClient = new NodePlexAPI({
hostname: settings.plex.ip,
post: settings.plex.port,
hostname: settingsPlex.ip,
port: settingsPlex.port,
https: settingsPlex.useSsl,
timeout: timeout,
token: plexToken,
authenticator: {
authenticate: (
@@ -86,6 +124,7 @@ class PlexAPI {
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public async getStatus() {
return await this.plexClient.query('/');
}
@@ -119,9 +158,17 @@ class PlexAPI {
return response.MediaContainer.Metadata[0];
}
public async getRecentlyAdded(): Promise<PlexLibraryItem[]> {
public async getChildrenMetadata(key: string): Promise<PlexMetadata[]> {
const response = await this.plexClient.query<PlexMetadataResponse>(
`/library/metadata/${key}/children`
);
return response.MediaContainer.Metadata;
}
public async getRecentlyAdded(id: string): Promise<PlexLibraryItem[]> {
const response = await this.plexClient.query<PlexLibraryResponse>(
'/library/recentlyAdded'
`/library/sections/${id}/recentlyAdded`
);
return response.MediaContainer.Metadata;

View File

@@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import xml2js from 'xml2js';
import { PlexDevice } from '../interfaces/api/plexInterfaces';
import { getSettings } from '../lib/settings';
import logger from '../logger';
@@ -29,6 +30,45 @@ interface PlexUser {
entitlements: string[];
}
interface ConnectionResponse {
$: {
protocol: string;
address: string;
port: string;
uri: string;
local: string;
};
}
interface DeviceResponse {
$: {
name: string;
product: string;
productVersion: string;
platform: string;
platformVersion: string;
device: string;
clientIdentifier: string;
createdAt: string;
lastSeenAt: string;
provides: string;
owned: string;
accessToken?: string;
publicAddress?: string;
httpsRequired?: string;
synced?: string;
relay?: string;
dnsRebindingProtection?: string;
natLoopbackSupported?: string;
publicAddressMatches?: string;
presence?: string;
ownerID?: string;
home?: string;
sourceTitle?: string;
};
Connection: ConnectionResponse[];
}
interface ServerResponse {
$: {
id: string;
@@ -56,6 +96,21 @@ interface FriendResponse {
};
}
interface UsersResponse {
MediaContainer: {
User: {
$: {
id: string;
title: string;
username: string;
email: string;
thumb: string;
};
Server: ServerResponse[];
}[];
};
}
class PlexTvAPI {
private authToken: string;
private axios: AxiosInstance;
@@ -72,6 +127,62 @@ class PlexTvAPI {
});
}
public async getDevices(): Promise<PlexDevice[]> {
try {
const devicesResp = await this.axios.get(
'/api/resources?includeHttps=1',
{
transformResponse: [],
responseType: 'text',
}
);
const parsedXml = await xml2js.parseStringPromise(
devicesResp.data as DeviceResponse
);
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
name: pxml.$.name,
product: pxml.$.product,
productVersion: pxml.$.productVersion,
platform: pxml.$?.platform,
platformVersion: pxml.$?.platformVersion,
device: pxml.$?.device,
clientIdentifier: pxml.$.clientIdentifier,
createdAt: new Date(parseInt(pxml.$?.createdAt, 10) * 1000),
lastSeenAt: new Date(parseInt(pxml.$?.lastSeenAt, 10) * 1000),
provides: pxml.$.provides.split(','),
owned: pxml.$.owned == '1' ? true : false,
accessToken: pxml.$?.accessToken,
publicAddress: pxml.$?.publicAddress,
publicAddressMatches:
pxml.$?.publicAddressMatches == '1' ? true : false,
httpsRequired: pxml.$?.httpsRequired == '1' ? true : false,
synced: pxml.$?.synced == '1' ? true : false,
relay: pxml.$?.relay == '1' ? true : false,
dnsRebindingProtection:
pxml.$?.dnsRebindingProtection == '1' ? true : false,
natLoopbackSupported:
pxml.$?.natLoopbackSupported == '1' ? true : false,
presence: pxml.$?.presence == '1' ? true : false,
ownerID: pxml.$?.ownerID,
home: pxml.$?.home == '1' ? true : false,
sourceTitle: pxml.$?.sourceTitle,
connection: pxml?.Connection?.map((conn: ConnectionResponse) => ({
protocol: conn.$.protocol,
address: conn.$.address,
port: parseInt(conn.$.port, 10),
uri: conn.$.uri,
local: conn.$.local == '1' ? true : false,
})),
}));
} catch (e) {
logger.error('Something went wrong getting the devices from plex.tv', {
label: 'Plex.tv API',
errorMessage: e.message,
});
throw new Error('Invalid auth token');
}
}
public async getUser(): Promise<PlexUser> {
try {
const account = await this.axios.get<PlexAccountResponse>(
@@ -81,7 +192,7 @@ class PlexTvAPI {
return account.data.user;
} catch (e) {
logger.error(
`Something went wrong getting the account from plex.tv: ${e.message}`,
`Something went wrong while getting the account from plex.tv: ${e.message}`,
{ label: 'Plex.tv API' }
);
throw new Error('Invalid auth token');
@@ -129,6 +240,18 @@ class PlexTvAPI {
return false;
}
}
public async getUsers(): Promise<UsersResponse> {
const response = await this.axios.get('/api/users', {
transformResponse: [],
responseType: 'text',
});
const parsedXml = (await xml2js.parseStringPromise(
response.data
)) as UsersResponse;
return parsedXml;
}
}
export default PlexTvAPI;

View File

@@ -1,4 +1,6 @@
import Axios, { AxiosInstance } from 'axios';
import { RadarrSettings } from '../lib/settings';
import logger from '../logger';
interface RadarrMovieOptions {
title: string;
@@ -12,12 +14,13 @@ interface RadarrMovieOptions {
searchNow?: boolean;
}
interface RadarrMovie {
export interface RadarrMovie {
id: number;
title: string;
isAvailable: boolean;
monitored: boolean;
tmdbId: number;
imdbId: string;
titleSlug: string;
folderName: string;
path: string;
@@ -28,7 +31,7 @@ interface RadarrMovie {
hasFile: boolean;
}
interface RadarrRootFolder {
export interface RadarrRootFolder {
id: number;
path: string;
freeSpace: number;
@@ -39,12 +42,44 @@ interface RadarrRootFolder {
}[];
}
interface RadarrProfile {
export interface RadarrProfile {
id: number;
name: string;
}
interface QueueItem {
movieId: number;
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: string;
trackedDownloadState: string;
downloadId: string;
protocol: string;
downloadClient: string;
indexer: string;
id: number;
}
interface QueueResponse {
page: number;
pageSize: number;
sortKey: string;
sortDirection: string;
totalRecords: number;
records: QueueItem[];
}
class RadarrAPI {
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
}
private axios: AxiosInstance;
constructor({ url, apiKey }: { url: string; apiKey: string }) {
this.axios = Axios.create({
@@ -75,10 +110,89 @@ class RadarrAPI {
}
};
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
try {
const response = await this.axios.get<RadarrMovie[]>('/movie/lookup', {
params: {
term: `tmdb:${id}`,
},
});
if (!response.data[0]) {
throw new Error('Movie not found');
}
return response.data[0];
} catch (e) {
logger.error('Error retrieving movie by TMDb ID', {
label: 'Radarr API',
message: e.message,
});
throw new Error('Movie not found');
}
}
public addMovie = async (
options: RadarrMovieOptions
): Promise<RadarrMovie> => {
try {
const movie = await this.getMovieByTmdbId(options.tmdbId);
if (movie.downloaded) {
logger.info(
'Title already exists and is available. Skipping add and returning success',
{
label: 'Radarr',
}
);
return movie;
}
// movie exists in radarr but is neither downloaded nor monitored
if (movie.id && !movie.monitored) {
const response = await this.axios.put<RadarrMovie>(`/movie`, {
...movie,
title: options.title,
qualityProfileId: options.qualityProfileId,
profileId: options.profileId,
titleSlug: options.tmdbId.toString(),
minimumAvailability: options.minimumAvailability,
tmdbId: options.tmdbId,
year: options.year,
rootFolderPath: options.rootFolderPath,
monitored: options.monitored,
addOptions: {
searchForMovie: options.searchNow,
},
});
if (response.data.monitored) {
logger.info(
'Found existing title in Radarr and set it to monitored. Returning success',
{ label: 'Radarr' }
);
logger.debug('Radarr update details', {
label: 'Radarr',
movie: response.data,
});
return response.data;
} else {
logger.error('Failed to update existing movie in Radarr', {
label: 'Radarr',
options,
});
throw new Error('Failed to update existing movie in Radarr');
}
}
if (movie.id) {
logger.info(
'Movie is already monitored in Radarr. Skipping add and returning success',
{ label: 'Radarr' }
);
return movie;
}
const response = await this.axios.post<RadarrMovie>(`/movie`, {
title: options.title,
qualityProfileId: options.qualityProfileId,
@@ -94,9 +208,31 @@ class RadarrAPI {
},
});
if (response.data.id) {
logger.info('Radarr accepted request', { label: 'Radarr' });
logger.debug('Radarr add details', {
label: 'Radarr',
movie: response.data,
});
} else {
logger.error('Failed to add movie to Radarr', {
label: 'Radarr',
options,
});
throw new Error('Failed to add movie to Radarr');
}
return response.data;
} catch (e) {
throw new Error(`[Radarr] Failed to add movie: ${e.message}`);
logger.error(
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
{
label: 'Radarr',
errorMessage: e.message,
options,
response: e?.response?.data,
}
);
throw new Error('Failed to add movie to Radarr');
}
};
@@ -119,6 +255,16 @@ class RadarrAPI {
throw new Error(`[Radarr] Failed to retrieve root folders: ${e.message}`);
}
};
public getQueue = async (): Promise<QueueItem[]> => {
try {
const response = await this.axios.get<QueueResponse>(`/queue`);
return response.data.records;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve queue: ${e.message}`);
}
};
}
export default RadarrAPI;

View File

@@ -92,7 +92,27 @@ class RottenTomatoes {
}
);
const movie = response.data.movies.find((movie) => movie.year === year);
// First, attempt to match exact name and year
let movie = response.data.movies.find(
(movie) => movie.year === year && movie.title === name
);
// If we don't find a movie, try to match partial name and year
if (!movie) {
movie = response.data.movies.find(
(movie) => movie.year === year && movie.title.includes(name)
);
}
// If we still dont find a movie, try to match just on year
if (!movie) {
movie = response.data.movies.find((movie) => movie.year === year);
}
// One last try, try exact name match only
if (!movie) {
movie = response.data.movies.find((movie) => movie.title === name);
}
if (!movie) {
return null;
@@ -116,7 +136,7 @@ class RottenTomatoes {
public async getTVRatings(
name: string,
year: number
year?: number
): Promise<RTRating | null> {
try {
const response = await this.axios.get<RTMultiSearchResponse>(
@@ -126,9 +146,13 @@ class RottenTomatoes {
}
);
const tvshow = response.data.tvSeries.find(
(series) => series.startYear === year
);
let tvshow: RTTvSearchResult | undefined = response.data.tvSeries[0];
if (year) {
tvshow = response.data.tvSeries.find(
(series) => series.startYear === year
);
}
if (!tvshow) {
return null;

View File

@@ -1,12 +1,21 @@
import Axios, { AxiosInstance } from 'axios';
import { SonarrSettings } from '../lib/settings';
import logger from '../logger';
interface SonarrSeason {
seasonNumber: number;
monitored: boolean;
statistics?: {
previousAiring?: string;
episodeFileCount: number;
episodeCount: number;
totalEpisodeCount: number;
sizeOnDisk: number;
percentOfEpisodes: number;
};
}
interface SonarrSeries {
export interface SonarrSeries {
title: string;
sortTitle: string;
seasonCount: number;
@@ -33,7 +42,7 @@ interface SonarrSeries {
tvMazeId: number;
firstAired: string;
lastInfoSync?: string;
seriesType: string;
seriesType: 'standard' | 'daily' | 'anime';
cleanTitle: string;
imdbId: string;
titleSlug: string;
@@ -55,6 +64,33 @@ interface SonarrSeries {
};
}
interface QueueItem {
seriesId: number;
episodeId: number;
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: string;
trackedDownloadState: string;
downloadId: string;
protocol: string;
downloadClient: string;
indexer: string;
id: number;
}
interface QueueResponse {
page: number;
pageSize: number;
sortKey: string;
sortDirection: string;
totalRecords: number;
records: QueueItem[];
}
interface SonarrProfile {
id: number;
name: string;
@@ -76,12 +112,20 @@ interface AddSeriesOptions {
title: string;
profileId: number;
seasons: number[];
seasonFolder: boolean;
rootFolderPath: string;
seriesType: SonarrSeries['seriesType'];
monitored?: boolean;
searchNow?: boolean;
}
class SonarrAPI {
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
sonarrSettings.hostname
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
}
private axios: AxiosInstance;
constructor({ url, apiKey }: { url: string; apiKey: string }) {
this.axios = Axios.create({
@@ -92,6 +136,38 @@ class SonarrAPI {
});
}
public async getSeries(): Promise<SonarrSeries[]> {
try {
const response = await this.axios.get<SonarrSeries[]>('/series');
return response.data;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve series: ${e.message}`);
}
}
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
try {
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
params: {
term: title,
},
});
if (!response.data[0]) {
throw new Error('No series found');
}
return response.data;
} catch (e) {
logger.error('Error retrieving series by series title', {
label: 'Sonarr API',
message: e.message,
});
throw new Error('No series found');
}
}
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
try {
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
@@ -124,7 +200,7 @@ class SonarrAPI {
series.addOptions = {
ignoreEpisodesWithFiles: true,
searchForMissingEpisodes: true,
searchForMissingEpisodes: options.searchNow,
};
const newSeriesResponse = await this.axios.put<SonarrSeries>(
@@ -132,6 +208,22 @@ class SonarrAPI {
series
);
if (newSeriesResponse.data.id) {
logger.info('Sonarr accepted request. Updated existing series', {
label: 'Sonarr',
});
logger.debug('Sonarr update details', {
label: 'Sonarr',
movie: newSeriesResponse.data,
});
} else {
logger.error('Failed to update series in Sonarr', {
label: 'Sonarr',
options,
});
throw new Error('Failed to update series in Sonarr');
}
return newSeriesResponse.data;
}
@@ -149,8 +241,10 @@ class SonarrAPI {
monitored: false,
}))
),
seasonFolder: options.seasonFolder,
monitored: options.monitored,
rootFolderPath: options.rootFolderPath,
seriesType: options.seriesType,
addOptions: {
ignoreEpisodesWithFiles: true,
searchForMissingEpisodes: options.searchNow,
@@ -158,12 +252,27 @@ class SonarrAPI {
} as Partial<SonarrSeries>
);
if (createdSeriesResponse.data.id) {
logger.info('Sonarr accepted request', { label: 'Sonarr' });
logger.debug('Sonarr add details', {
label: 'Sonarr',
movie: createdSeriesResponse.data,
});
} else {
logger.error('Failed to add movie to Sonarr', {
label: 'Sonarr',
options,
});
throw new Error('Failed to add series to Sonarr');
}
return createdSeriesResponse.data;
} catch (e) {
logger.error('Something went wrong adding a series to Sonarr', {
logger.error('Something went wrong while adding a series to Sonarr.', {
label: 'Sonarr API',
message: e.message,
errorMessage: e.message,
error: e,
response: e?.response?.data,
});
throw new Error('Failed to add series');
}
@@ -175,7 +284,7 @@ class SonarrAPI {
return response.data;
} catch (e) {
logger.error('Something went wrong retrieving Sonarr profiles', {
logger.error('Something went wrong while retrieving Sonarr profiles.', {
label: 'Sonarr API',
message: e.message,
});
@@ -189,10 +298,14 @@ class SonarrAPI {
return response.data;
} catch (e) {
logger.error('Something went wrong retrieving Sonarr root folders', {
label: 'Sonarr API',
message: e.message,
});
logger.error(
'Something went wrong while retrieving Sonarr root folders.',
{
label: 'Sonarr API',
message: e.message,
}
);
throw new Error('Failed to get root folders');
}
}
@@ -221,6 +334,16 @@ class SonarrAPI {
return newSeasons;
}
public getQueue = async (): Promise<QueueItem[]> => {
try {
const response = await this.axios.get<QueueResponse>(`/queue`);
return response.data.records;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve queue: ${e.message}`);
}
};
}
export default SonarrAPI;

View File

@@ -1,5 +1,7 @@
import axios, { AxiosInstance } from 'axios';
export const ANIME_KEYWORD_ID = 210024;
interface SearchOptions {
query: string;
page?: number;
@@ -69,7 +71,7 @@ export interface TmdbTvResult extends TmdbMediaResult {
name: string;
original_name: string;
origin_country: string[];
first_air_Date: string;
first_air_date: string;
}
export interface TmdbPersonResult {
@@ -188,7 +190,30 @@ export interface TmdbMovieDetails {
cast: TmdbCreditCast[];
crew: TmdbCreditCrew[];
};
belongs_to_collection?: {
id: number;
name: string;
poster_path?: string;
backdrop_path?: string;
};
external_ids: TmdbExternalIds;
videos: TmdbVideoResult;
}
export interface TmdbVideo {
id: string;
key: string;
name: string;
site: 'YouTube';
size: number;
type:
| 'Clip'
| 'Teaser'
| 'Trailer'
| 'Featurette'
| 'Opening Credits'
| 'Behind the Scenes'
| 'Bloopers';
}
export interface TmdbTvEpisodeResult {
@@ -258,6 +283,11 @@ export interface TmdbTvDetails {
name: string;
origin_country: string;
}[];
spoken_languages: {
english_name: string;
iso_639_1: string;
name: string;
}[];
seasons: TmdbTvSeasonResult[];
status: string;
type: string;
@@ -268,6 +298,19 @@ export interface TmdbTvDetails {
crew: TmdbCreditCrew[];
};
external_ids: TmdbExternalIds;
keywords: {
results: TmdbKeyword[];
};
videos: TmdbVideoResult;
}
export interface TmdbVideoResult {
results: TmdbVideo[];
}
export interface TmdbKeyword {
id: number;
name: string;
}
export interface TmdbPersonDetail {
@@ -329,6 +372,15 @@ export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
external_ids: TmdbExternalIds;
}
export interface TmdbCollection {
id: number;
name: string;
overview?: string;
poster_path?: string;
backdrop_path?: string;
parts: TmdbMovieResult[];
}
class TheMovieDb {
private apiKey = 'db55323b8d3e4154498498a75642b381';
private axios: AxiosInstance;
@@ -359,7 +411,12 @@ class TheMovieDb {
return response.data;
} catch (e) {
throw new Error(`[TMDB] Failed to search multi: ${e.message}`);
return {
page: 1,
results: [],
total_pages: 1,
total_results: 0,
};
}
};
@@ -418,7 +475,10 @@ class TheMovieDb {
const response = await this.axios.get<TmdbMovieDetails>(
`/movie/${movieId}`,
{
params: { language, append_to_response: 'credits,external_ids' },
params: {
language,
append_to_response: 'credits,external_ids,videos',
},
}
);
@@ -437,7 +497,10 @@ class TheMovieDb {
}): Promise<TmdbTvDetails> => {
try {
const response = await this.axios.get<TmdbTvDetails>(`/tv/${tvId}`, {
params: { language, append_to_response: 'credits,external_ids' },
params: {
language,
append_to_response: 'credits,external_ids,keywords,videos',
},
});
return response.data;
@@ -524,6 +587,32 @@ class TheMovieDb {
}
}
public async getMoviesByKeyword({
keywordId,
page = 1,
language = 'en-US',
}: {
keywordId: number;
page?: number;
language?: string;
}): Promise<TmdbSearchMovieResponse> {
try {
const response = await this.axios.get<TmdbSearchMovieResponse>(
`/keyword/${keywordId}/movies`,
{
params: {
page,
language,
},
}
);
return response.data;
} catch (e) {
throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
}
}
public async getTvRecommendations({
tvId,
page = 1,
@@ -809,14 +898,37 @@ class TheMovieDb {
}
throw new Error(
`[TMDB] Failed to find a tv show with the provided TVDB id: ${tvdbId}`
`[TMDB] Failed to find a TV show with the provided TVDB ID: ${tvdbId}`
);
} catch (e) {
throw new Error(
`[TMDB] Failed to get tv show by external tvdb ID: ${e.message}`
`[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`
);
}
}
public async getCollection({
collectionId,
language = 'en-US',
}: {
collectionId: number;
language?: string;
}): Promise<TmdbCollection> {
try {
const response = await this.axios.get<TmdbCollection>(
`/collection/${collectionId}`,
{
params: {
language,
},
}
);
return response.data;
} catch (e) {
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
}
}
}
export default TheMovieDb;

4
server/constants/user.ts Normal file
View File

@@ -0,0 +1,4 @@
export enum UserType {
PLEX = 1,
LOCAL = 2,
}

View File

@@ -8,14 +8,16 @@ import {
UpdateDateColumn,
getRepository,
In,
AfterUpdate,
AfterLoad,
} from 'typeorm';
import { MediaRequest } from './MediaRequest';
import { MediaStatus, MediaType } from '../constants/media';
import logger from '../logger';
import Season from './Season';
import notificationManager, { Notification } from '../lib/notifications';
import TheMovieDb from '../api/themoviedb';
import { getSettings } from '../lib/settings';
import RadarrAPI from '../api/radarr';
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
import SonarrAPI from '../api/sonarr';
@Entity()
class Media {
@@ -43,12 +45,15 @@ class Media {
}
}
public static async getMedia(id: number): Promise<Media | undefined> {
public static async getMedia(
id: number,
mediaType: MediaType
): Promise<Media | undefined> {
const mediaRepository = getRepository(Media);
try {
const media = await mediaRepository.findOne({
where: { tmdbId: id },
where: { tmdbId: id, mediaType },
relations: ['requests'],
});
@@ -65,7 +70,7 @@ class Media {
@Column({ type: 'varchar' })
public mediaType: MediaType;
@Column({ unique: true })
@Column()
@Index()
public tmdbId: number;
@@ -73,13 +78,16 @@ class Media {
@Index()
public tvdbId?: number;
@Column({ unique: true, nullable: true })
@Column({ nullable: true })
@Index()
public imdbId?: string;
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status: MediaStatus;
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status4k: MediaStatus;
@OneToMany(() => MediaRequest, (request) => request.media, { cascade: true })
public requests: MediaRequest[];
@@ -95,33 +103,174 @@ class Media {
@UpdateDateColumn()
public updatedAt: Date;
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
public lastSeasonChange: Date;
@Column({ type: 'datetime', nullable: true })
public mediaAddedAt: Date;
@Column({ nullable: true })
public serviceId?: number;
@Column({ nullable: true })
public serviceId4k?: number;
@Column({ nullable: true })
public externalServiceId?: number;
@Column({ nullable: true })
public externalServiceId4k?: number;
@Column({ nullable: true })
public externalServiceSlug?: string;
@Column({ nullable: true })
public externalServiceSlug4k?: string;
@Column({ nullable: true })
public ratingKey?: string;
@Column({ nullable: true })
public ratingKey4k?: string;
public serviceUrl?: string;
public serviceUrl4k?: string;
public downloadStatus?: DownloadingItem[] = [];
public downloadStatus4k?: DownloadingItem[] = [];
public plexUrl?: string;
public plexUrl4k?: string;
constructor(init?: Partial<Media>) {
Object.assign(this, init);
}
@AfterUpdate()
private async _notifyAvailable() {
if (this.status === MediaStatus.AVAILABLE) {
if (this.mediaType === MediaType.MOVIE) {
const requestRepository = getRepository(MediaRequest);
const relatedRequests = await requestRepository.find({
where: { media: this },
});
@AfterLoad()
public setPlexUrls(): void {
const machineId = getSettings().plex.machineId;
if (this.ratingKey) {
this.plexUrl = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey}`;
}
if (this.ratingKey4k) {
this.plexUrl4k = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`;
}
}
if (relatedRequests.length > 0) {
const tmdb = new TheMovieDb();
const movie = await tmdb.getMovie({ movieId: this.tmdbId });
@AfterLoad()
public setServiceUrl(): void {
if (this.mediaType === MediaType.MOVIE) {
if (this.serviceId !== null && this.externalServiceSlug !== null) {
const settings = getSettings();
const server = settings.radarr.find(
(radarr) => radarr.id === this.serviceId
);
relatedRequests.forEach((request) => {
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
notifyUser: request.requestedBy,
subject: movie.title,
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
});
});
if (server) {
this.serviceUrl = server.externalUrl
? `${server.externalUrl}/movie/${this.externalServiceSlug}`
: RadarrAPI.buildRadarrUrl(
server,
`/movie/${this.externalServiceSlug}`
);
}
}
if (this.serviceId4k !== null && this.externalServiceSlug4k !== null) {
const settings = getSettings();
const server = settings.radarr.find(
(radarr) => radarr.id === this.serviceId4k
);
if (server) {
this.serviceUrl4k = server.externalUrl
? `${server.externalUrl}/movie/${this.externalServiceSlug4k}`
: RadarrAPI.buildRadarrUrl(
server,
`/movie/${this.externalServiceSlug4k}`
);
}
}
}
if (this.mediaType === MediaType.TV) {
if (this.serviceId !== null && this.externalServiceSlug !== null) {
const settings = getSettings();
const server = settings.sonarr.find(
(sonarr) => sonarr.id === this.serviceId
);
if (server) {
this.serviceUrl = server.externalUrl
? `${server.externalUrl}/series/${this.externalServiceSlug}`
: SonarrAPI.buildSonarrUrl(
server,
`/series/${this.externalServiceSlug}`
);
}
}
if (this.serviceId4k !== null && this.externalServiceSlug4k !== null) {
const settings = getSettings();
const server = settings.sonarr.find(
(sonarr) => sonarr.id === this.serviceId4k
);
if (server) {
this.serviceUrl4k = server.externalUrl
? `${server.externalUrl}/series/${this.externalServiceSlug4k}`
: SonarrAPI.buildSonarrUrl(
server,
`/series/${this.externalServiceSlug4k}`
);
}
}
}
}
@AfterLoad()
public getDownloadingItem(): void {
if (this.mediaType === MediaType.MOVIE) {
if (
this.externalServiceId !== undefined &&
this.serviceId !== undefined
) {
this.downloadStatus = downloadTracker.getMovieProgress(
this.serviceId,
this.externalServiceId
);
}
if (
this.externalServiceId4k !== undefined &&
this.serviceId4k !== undefined
) {
this.downloadStatus4k = downloadTracker.getMovieProgress(
this.serviceId4k,
this.externalServiceId4k
);
}
}
if (this.mediaType === MediaType.TV) {
if (
this.externalServiceId !== undefined &&
this.serviceId !== undefined
) {
this.downloadStatus = downloadTracker.getSeriesProgress(
this.serviceId,
this.externalServiceId
);
}
if (
this.externalServiceId4k !== undefined &&
this.serviceId4k !== undefined
) {
this.downloadStatus4k = downloadTracker.getSeriesProgress(
this.serviceId4k,
this.externalServiceId4k
);
}
}
}
}

View File

@@ -15,11 +15,11 @@ import { User } from './User';
import Media from './Media';
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
import { getSettings } from '../lib/settings';
import TheMovieDb from '../api/themoviedb';
import TheMovieDb, { ANIME_KEYWORD_ID } from '../api/themoviedb';
import RadarrAPI from '../api/radarr';
import logger from '../logger';
import SeasonRequest from './SeasonRequest';
import SonarrAPI from '../api/sonarr';
import SonarrAPI, { SonarrSeries } from '../api/sonarr';
import notificationManager, { Notification } from '../lib/notifications';
@Entity()
@@ -36,10 +36,18 @@ export class MediaRequest {
})
public media: Media;
@ManyToOne(() => User, (user) => user.requests, { eager: true })
@ManyToOne(() => User, (user) => user.requests, {
eager: true,
onDelete: 'CASCADE',
})
public requestedBy: User;
@ManyToOne(() => User, { nullable: true, cascade: true, eager: true })
@ManyToOne(() => User, {
nullable: true,
cascade: true,
eager: true,
onDelete: 'SET NULL',
})
public modifiedBy?: User;
@CreateDateColumn()
@@ -57,12 +65,30 @@ export class MediaRequest {
})
public seasons: SeasonRequest[];
@Column({ default: false })
public is4k: boolean;
@Column({ nullable: true })
public serverId: number;
@Column({ nullable: true })
public profileId: number;
@Column({ nullable: true })
public rootFolder: string;
constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init);
}
@AfterUpdate()
@AfterInsert()
private async _notifyNewRequest() {
public async sendMedia(): Promise<void> {
await Promise.all([this.sendToRadarr(), this.sendToSonarr()]);
}
@AfterInsert()
public async notifyNewRequest(): Promise<void> {
if (this.status === MediaRequestStatus.PENDING) {
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
@@ -80,6 +106,7 @@ export class MediaRequest {
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: this.requestedBy,
media,
});
}
@@ -90,6 +117,7 @@ export class MediaRequest {
message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: this.requestedBy,
media,
extra: [
{
name: 'Seasons',
@@ -110,8 +138,11 @@ export class MediaRequest {
* auto approved content
*/
@AfterUpdate()
private async _notifyApproved() {
if (this.status === MediaRequestStatus.APPROVED) {
public async notifyApprovedOrDeclined(): Promise<void> {
if (
this.status === MediaRequestStatus.APPROVED ||
this.status === MediaRequestStatus.DECLINED
) {
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: this.media.id },
@@ -120,38 +151,71 @@ export class MediaRequest {
logger.error('No parent media!', { label: 'Media Request' });
return;
}
if (media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE) {
logger.warn(
'Media became available before request was approved. Approval notification will be skipped.',
{ label: 'Media Request' }
);
return;
}
const tmdb = new TheMovieDb();
if (this.media.mediaType === MediaType.MOVIE) {
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_APPROVED, {
subject: movie.title,
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: this.requestedBy,
});
notificationManager.sendNotification(
this.status === MediaRequestStatus.APPROVED
? Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED,
{
subject: movie.title,
message: movie.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
notifyUser: this.requestedBy,
media,
}
);
} else if (this.media.mediaType === MediaType.TV) {
const tv = await tmdb.getTvShow({ tvId: this.media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_APPROVED, {
subject: tv.name,
message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: this.requestedBy,
extra: [
{
name: 'Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
});
notificationManager.sendNotification(
this.status === MediaRequestStatus.APPROVED
? Notification.MEDIA_APPROVED
: Notification.MEDIA_DECLINED,
{
subject: tv.name,
message: tv.overview,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
notifyUser: this.requestedBy,
media,
extra: [
{
name: 'Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
}
);
}
}
}
@AfterInsert()
public async autoapprovalNotification(): Promise<void> {
const settings = getSettings().notifications;
if (
settings.autoapprovalEnabled &&
this.status === MediaRequestStatus.APPROVED
) {
this.notifyApprovedOrDeclined();
}
}
@AfterUpdate()
@AfterInsert()
private async _updateParentStatus() {
public async updateParentStatus(): Promise<void> {
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: this.media.id },
@@ -162,16 +226,30 @@ export class MediaRequest {
return;
}
const seasonRequestRepository = getRepository(SeasonRequest);
if (this.status === MediaRequestStatus.APPROVED) {
media.status = MediaStatus.PROCESSING;
if (
this.status === MediaRequestStatus.APPROVED &&
// Do not update the status if the item is already partially available or available
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
media[this.is4k ? 'status4k' : 'status'] !==
MediaStatus.PARTIALLY_AVAILABLE
) {
if (this.is4k) {
media.status4k = MediaStatus.PROCESSING;
} else {
media.status = MediaStatus.PROCESSING;
}
mediaRepository.save(media);
}
if (
this.media.mediaType === MediaType.MOVIE &&
media.mediaType === MediaType.MOVIE &&
this.status === MediaRequestStatus.DECLINED
) {
media.status = MediaStatus.UNKNOWN;
if (this.is4k) {
media.status4k = MediaStatus.UNKNOWN;
} else {
media.status = MediaStatus.UNKNOWN;
}
mediaRepository.save(media);
}
@@ -206,25 +284,37 @@ export class MediaRequest {
}
@AfterRemove()
private async _handleRemoveParentUpdate() {
public async handleRemoveParentUpdate(): Promise<void> {
const mediaRepository = getRepository(Media);
const fullMedia = await mediaRepository.findOneOrFail({
where: { id: this.media.id },
relations: ['requests'],
});
if (!fullMedia.requests || fullMedia.requests.length === 0) {
if (
!fullMedia.requests.some((request) => !request.is4k) &&
fullMedia.status !== MediaStatus.AVAILABLE
) {
fullMedia.status = MediaStatus.UNKNOWN;
mediaRepository.save(fullMedia);
}
if (
!fullMedia.requests.some((request) => request.is4k) &&
fullMedia.status4k !== MediaStatus.AVAILABLE
) {
fullMedia.status4k = MediaStatus.UNKNOWN;
}
mediaRepository.save(fullMedia);
}
@AfterUpdate()
@AfterInsert()
private async _sendToRadarr() {
public async sendToRadarr(): Promise<void> {
if (
this.status === MediaRequestStatus.APPROVED &&
this.type === MediaType.MOVIE
) {
try {
const mediaRepository = getRepository(Media);
const settings = getSettings();
if (settings.radarr.length === 0 && !settings.radarr[0]) {
logger.info(
@@ -234,40 +324,138 @@ export class MediaRequest {
return;
}
let radarrSettings = settings.radarr.find(
(radarr) => radarr.isDefault && radarr.is4k === this.is4k
);
if (
this.serverId !== null &&
this.serverId >= 0 &&
radarrSettings?.id !== this.serverId
) {
radarrSettings = settings.radarr.find(
(radarr) => radarr.id === this.serverId
);
logger.info(
`Request has an override server: ${radarrSettings?.name}`,
{ label: 'Media Request' }
);
}
if (!radarrSettings) {
logger.info(
`There is no default ${
this.is4k ? '4K ' : ''
}radarr configured. Did you set any of your Radarr servers as default?`,
{ label: 'Media Request' }
);
return;
}
let rootFolder = radarrSettings.activeDirectory;
let qualityProfile = radarrSettings.activeProfileId;
if (
this.rootFolder &&
this.rootFolder !== '' &&
this.rootFolder !== radarrSettings.activeDirectory
) {
rootFolder = this.rootFolder;
logger.info(`Request has an override root folder: ${rootFolder}`, {
label: 'Media Request',
});
}
if (
this.profileId &&
this.profileId !== radarrSettings.activeProfileId
) {
qualityProfile = this.profileId;
logger.info(`Request has an override profile id: ${qualityProfile}`, {
label: 'Media Request',
});
}
const tmdb = new TheMovieDb();
const radarrSettings = settings.radarr[0];
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}/api`,
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}/api`,
});
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
// Run this asynchronously so we don't wait for it on the UI side
radarr.addMovie({
profileId: radarrSettings.activeProfileId,
qualityProfileId: radarrSettings.activeProfileId,
rootFolderPath: radarrSettings.activeDirectory,
minimumAvailability: radarrSettings.minimumAvailability,
title: movie.title,
tmdbId: movie.id,
year: Number(movie.release_date.slice(0, 4)),
monitored: true,
searchNow: true,
const media = await mediaRepository.findOne({
where: { id: this.media.id },
});
if (!media) {
logger.error('Media not present');
return;
}
if (
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
) {
throw new Error('Media already available');
}
// Run this asynchronously so we don't wait for it on the UI side
radarr
.addMovie({
profileId: qualityProfile,
qualityProfileId: qualityProfile,
rootFolderPath: rootFolder,
minimumAvailability: radarrSettings.minimumAvailability,
title: movie.title,
tmdbId: movie.id,
year: Number(movie.release_date.slice(0, 4)),
monitored: true,
searchNow: !radarrSettings.preventSearch,
})
.then(async (radarrMovie) => {
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
radarrMovie.id;
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
radarrMovie.titleSlug;
media[this.is4k ? 'serviceId4k' : 'serviceId'] = radarrSettings?.id;
await mediaRepository.save(media);
})
.catch(async () => {
media.status = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
logger.warn(
'Newly added movie request failed to add to Radarr, marking as unknown',
{
label: 'Media Request',
}
);
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
});
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
subject: movie.title,
message: 'Movie failed to add to Radarr',
notifyUser: admin,
media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
});
});
logger.info('Sent request to Radarr', { label: 'Media Request' });
} catch (e) {
throw new Error(
`[MediaRequest] Request failed to send to radarr: ${e.message}`
);
const errorMessage = `Request failed to send to radarr: ${e.message}`;
logger.error('Request failed to send to Radarr', {
label: 'Media Request',
errorMessage,
});
throw new Error(errorMessage);
}
}
}
@AfterUpdate()
@AfterInsert()
private async _sendToSonarr() {
public async sendToSonarr(): Promise<void> {
if (
this.status === MediaRequestStatus.APPROVED &&
this.type === MediaType.TV
@@ -283,6 +471,34 @@ export class MediaRequest {
return;
}
let sonarrSettings = settings.sonarr.find(
(sonarr) => sonarr.isDefault && sonarr.is4k === this.is4k
);
if (
this.serverId !== null &&
this.serverId >= 0 &&
sonarrSettings?.id !== this.serverId
) {
sonarrSettings = settings.sonarr.find(
(sonarr) => sonarr.id === this.serverId
);
logger.info(
`Request has an override server: ${sonarrSettings?.name}`,
{ label: 'Media Request' }
);
}
if (!sonarrSettings) {
logger.info(
`There is no default ${
this.is4k ? '4K ' : ''
}sonarr configured. Did you set any of your Sonarr servers as default?`,
{ label: 'Media Request' }
);
return;
}
const media = await mediaRepository.findOne({
where: { id: this.media.id },
relations: ['requests'],
@@ -292,35 +508,133 @@ export class MediaRequest {
throw new Error('Media data is missing');
}
if (
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
) {
throw new Error('Media already available');
}
const tmdb = new TheMovieDb();
const sonarrSettings = settings.sonarr[0];
const sonarr = new SonarrAPI({
apiKey: sonarrSettings.apiKey,
url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${
sonarrSettings.hostname
}:${sonarrSettings.port}/api`,
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}/api`,
});
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
if (!series.external_ids.tvdb_id) {
if (!tvdbId) {
this.handleRemoveParentUpdate();
throw new Error('Series was missing tvdb id');
}
let seriesType: SonarrSeries['seriesType'] = 'standard';
// Change series type to anime if the anime keyword is present on tmdb
if (
series.keywords.results.some(
(keyword) => keyword.id === ANIME_KEYWORD_ID
)
) {
seriesType = 'anime';
}
let rootFolder =
seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
? sonarrSettings.activeAnimeDirectory
: sonarrSettings.activeDirectory;
let qualityProfile =
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId;
if (
this.rootFolder &&
this.rootFolder !== '' &&
this.rootFolder !== rootFolder
) {
rootFolder = this.rootFolder;
logger.info(`Request has an override root folder: ${rootFolder}`, {
label: 'Media Request',
});
}
if (this.profileId && this.profileId !== qualityProfile) {
qualityProfile = this.profileId;
logger.info(`Request has an override profile ID: ${qualityProfile}`, {
label: 'Media Request',
});
}
// Run this asynchronously so we don't wait for it on the UI side
sonarr.addSeries({
profileId: sonarrSettings.activeProfileId,
rootFolderPath: sonarrSettings.activeDirectory,
title: series.name,
tvdbid: series.external_ids.tvdb_id,
seasons: this.seasons.map((season) => season.seasonNumber),
monitored: true,
searchNow: true,
});
sonarr
.addSeries({
profileId: qualityProfile,
rootFolderPath: rootFolder,
title: series.name,
tvdbid: tvdbId,
seasons: this.seasons.map((season) => season.seasonNumber),
seasonFolder: sonarrSettings.enableSeasonFolders,
seriesType,
monitored: true,
searchNow: !sonarrSettings.preventSearch,
})
.then(async (sonarrSeries) => {
// We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({
where: { id: this.media.id },
relations: ['requests'],
});
if (!media) {
throw new Error('Media data is missing');
}
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
sonarrSeries.id;
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
sonarrSeries.titleSlug;
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
await mediaRepository.save(media);
})
.catch(async () => {
media.status = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
logger.warn(
'Newly added series request failed to add to Sonarr, marking as unknown',
{
label: 'Media Request',
}
);
const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({
order: { id: 'ASC' },
});
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
subject: series.name,
message: 'Series failed to add to Sonarr',
notifyUser: admin,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
media,
extra: [
{
name: 'Seasons',
value: this.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
});
});
logger.info('Sent request to Sonarr', { label: 'Media Request' });
} catch (e) {
throw new Error(
`[MediaRequest] Request failed to send to sonarr: ${e.message}`
);
const errorMessage = `Request failed to send to sonarr: ${e.message}`;
logger.error('Request failed to send to Sonarr', {
label: 'Media Request',
errorMessage,
});
throw new Error(errorMessage);
}
}
}

View File

@@ -5,15 +5,9 @@ import {
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
AfterInsert,
AfterUpdate,
getRepository,
} from 'typeorm';
import { MediaStatus } from '../constants/media';
import Media from './Media';
import logger from '../logger';
import TheMovieDb from '../api/themoviedb';
import notificationManager, { Notification } from '../lib/notifications';
@Entity()
class Season {
@@ -26,6 +20,9 @@ class Season {
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status: MediaStatus;
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
public status4k: MediaStatus;
@ManyToOne(() => Media, (media) => media.seasons, { onDelete: 'CASCADE' })
public media: Promise<Media>;
@@ -38,60 +35,6 @@ class Season {
constructor(init?: Partial<Season>) {
Object.assign(this, init);
}
@AfterInsert()
@AfterUpdate()
private async _sendSeasonAvailableNotification() {
if (this.status === MediaStatus.AVAILABLE) {
try {
const lazyMedia = await this.media;
const tmdb = new TheMovieDb();
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOneOrFail({
where: { id: lazyMedia.id },
relations: ['requests'],
});
const availableSeasons = media.seasons.map(
(season) => season.seasonNumber
);
const request = media.requests.find(
(request) =>
// Check if the season is complete AND it contains the current season that was just marked available
request.seasons.every((season) =>
availableSeasons.includes(season.seasonNumber)
) &&
request.seasons.some(
(season) => season.seasonNumber === this.seasonNumber
)
);
if (request) {
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
subject: tv.name,
message: tv.overview,
notifyUser: request.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
extra: [
{
name: 'Seasons',
value: request.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
});
}
} catch (e) {
logger.error('Something went wrong sending season available notice', {
label: 'Notifications',
message: e.message,
});
}
}
}
}
export default Season;

View File

@@ -6,9 +6,17 @@ import {
UpdateDateColumn,
OneToMany,
RelationCount,
AfterLoad,
} from 'typeorm';
import { Permission, hasPermission } from '../lib/permissions';
import { MediaRequest } from './MediaRequest';
import bcrypt from 'bcrypt';
import path from 'path';
import PreparedEmail from '../lib/email';
import logger from '../logger';
import { getSettings } from '../lib/settings';
import { default as generatePassword } from 'secure-random-password';
import { UserType } from '../constants/user';
@Entity()
export class User {
@@ -16,7 +24,9 @@ export class User {
return users.map((u) => u.filter());
}
static readonly filteredFields: string[] = ['plexToken'];
static readonly filteredFields: string[] = ['plexToken', 'password'];
public displayName: string;
@PrimaryGeneratedColumn()
public id: number;
@@ -24,11 +34,20 @@ export class User {
@Column({ unique: true })
public email: string;
@Column()
public username: string;
@Column({ nullable: true })
public plexUsername: string;
@Column({ select: false })
public plexId: number;
@Column({ nullable: true })
public username?: string;
@Column({ nullable: true, select: false })
public password?: string;
@Column({ type: 'integer', default: UserType.PLEX })
public userType: UserType;
@Column({ nullable: true, select: false })
public plexId?: number;
@Column({ nullable: true, select: false })
public plexToken?: string;
@@ -69,4 +88,52 @@ export class User {
public hasPermission(permissions: Permission | Permission[]): boolean {
return !!hasPermission(permissions, this.permissions);
}
public passwordMatch(password: string): Promise<boolean> {
return new Promise((resolve, reject) => {
if (this.password) {
resolve(bcrypt.compare(password, this.password));
} else {
return reject(false);
}
});
}
public async setPassword(password: string): Promise<void> {
const hashedPassword = await bcrypt.hash(password, 12);
this.password = hashedPassword;
}
public async resetPassword(): Promise<void> {
const password = generatePassword.randomPassword({ length: 16 });
this.setPassword(password);
const applicationUrl = getSettings().main.applicationUrl;
try {
logger.info(`Sending password email for ${this.email}`, {
label: 'User creation',
});
const email = new PreparedEmail();
await email.send({
template: path.join(__dirname, '../templates/email/password'),
message: {
to: this.email,
},
locals: {
password: password,
applicationUrl,
},
});
} catch (e) {
logger.error('Failed to send out password email', {
label: 'User creation',
message: e.message,
});
}
}
@AfterLoad()
public setDisplayName(): void {
this.displayName = this.username || this.plexUsername;
}
}

View File

@@ -5,6 +5,7 @@ import { createConnection, getRepository } from 'typeorm';
import routes from './routes';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import csurf from 'csurf';
import session, { Store } from 'express-session';
import { TypeormStore } from 'connect-typeorm/out';
import YAML from 'yamljs';
@@ -17,9 +18,16 @@ import { startJobs } from './job/schedule';
import notificationManager from './lib/notifications';
import DiscordAgent from './lib/notifications/agents/discord';
import EmailAgent from './lib/notifications/agents/email';
import TelegramAgent from './lib/notifications/agents/telegram';
import { getAppVersion } from './utils/appVersion';
import SlackAgent from './lib/notifications/agents/slack';
import PushoverAgent from './lib/notifications/agents/pushover';
import WebhookAgent from './lib/notifications/agents/webhook';
import { getClientIp } from '@supercharge/request-ip';
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
logger.info(`Starting Overseerr version ${getAppVersion()}`);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
@@ -27,22 +35,73 @@ const handle = app.getRequestHandler();
app
.prepare()
.then(async () => {
await createConnection();
const dbConnection = await createConnection();
// Run migrations in production
if (process.env.NODE_ENV === 'production') {
await dbConnection.query('PRAGMA foreign_keys=OFF');
await dbConnection.runMigrations();
await dbConnection.query('PRAGMA foreign_keys=ON');
}
// Load Settings
const settings = getSettings().load();
// Register Notification Agents
notificationManager.registerAgents([new DiscordAgent(), new EmailAgent()]);
notificationManager.registerAgents([
new DiscordAgent(),
new EmailAgent(),
new SlackAgent(),
new TelegramAgent(),
new PushoverAgent(),
new WebhookAgent(),
]);
// Start Jobs
startJobs();
const server = express();
if (settings.main.trustProxy) {
server.enable('trust proxy');
}
server.use(cookieParser());
server.use(bodyParser.json());
server.use(bodyParser.urlencoded({ extended: true }));
server.use((req, res, next) => {
try {
const descriptor = Object.getOwnPropertyDescriptor(req, 'ip');
if (descriptor?.writable === true) {
req.ip = getClientIp(req) ?? '';
}
} catch (e) {
logger.error('Failed to attach the ip to the request', {
label: 'Middleware',
message: e.message,
});
} finally {
next();
}
});
if (settings.main.csrfProtection) {
server.use(
csurf({
cookie: {
httpOnly: true,
sameSite: true,
secure: !dev,
},
})
);
server.use((req, res, next) => {
res.cookie('XSRF-TOKEN', req.csrfToken(), {
sameSite: true,
secure: !dev,
});
next();
});
}
// Setup sessions
// Set up sessions
const sessionRespository = getRepository(Session);
server.use(
'/api',
@@ -98,12 +157,21 @@ app
}
);
const port = Number(process.env.PORT) || 3000;
server.listen(port, () => {
logger.info(`Server ready on port ${port}`, {
label: 'SERVER',
const port = Number(process.env.PORT) || 5055;
const host = process.env.HOST;
if (host) {
server.listen(port, host, () => {
logger.info(`Server ready on ${host} port ${port}`, {
label: 'Server',
});
});
});
} else {
server.listen(port, () => {
logger.info(`Server ready on port ${port}`, {
label: 'Server',
});
});
}
})
.catch((err) => {
logger.error(err.stack);

View File

@@ -0,0 +1,45 @@
import { PlexSettings } from '../../lib/settings';
export interface PlexStatus {
settings: PlexSettings;
status: number;
message: string;
}
export interface PlexConnection {
protocol: string;
address: string;
port: number;
uri: string;
local: boolean;
status?: number;
message?: string;
host?: string;
}
export interface PlexDevice {
name: string;
product: string;
productVersion: string;
platform: string;
platformVersion: string;
device: string;
clientIdentifier: string;
createdAt: Date;
lastSeenAt: Date;
provides: string[];
owned: boolean;
accessToken?: string;
publicAddress?: string;
httpsRequired?: boolean;
synced?: boolean;
relay?: boolean;
dnsRebindingProtection?: boolean;
natLoopbackSupported?: boolean;
publicAddressMatches?: boolean;
presence?: boolean;
ownerID?: string;
home?: boolean;
sourceTitle?: string;
connection: PlexConnection[];
}

View File

@@ -0,0 +1,18 @@
import { RadarrProfile, RadarrRootFolder } from '../../api/radarr';
export interface ServiceCommonServer {
id: number;
name: string;
is4k: boolean;
isDefault: boolean;
activeProfileId: number;
activeDirectory: string;
activeAnimeProfileId?: number;
activeAnimeDirectory?: string;
}
export interface ServiceCommonServerWithDetails {
server: ServiceCommonServer;
profiles: RadarrProfile[];
rootFolders: Partial<RadarrRootFolder>[];
}

View File

@@ -0,0 +1,13 @@
export interface SettingsAboutResponse {
version: string;
totalRequests: number;
totalMediaItems: number;
tz?: string;
}
export interface PublicSettingsResponse {
initialized: boolean;
movie4kEnabled: boolean;
series4kEnabled: boolean;
hideAvailable: boolean;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,248 @@
import { uniqWith } from 'lodash';
import { getRepository } from 'typeorm';
import { v4 as uuid } from 'uuid';
import RadarrAPI, { RadarrMovie } from '../../api/radarr';
import { MediaStatus, MediaType } from '../../constants/media';
import Media from '../../entity/Media';
import { getSettings, RadarrSettings } from '../../lib/settings';
import logger from '../../logger';
const BUNDLE_SIZE = 50;
const UPDATE_RATE = 4 * 1000;
interface SyncStatus {
running: boolean;
progress: number;
total: number;
currentServer: RadarrSettings;
servers: RadarrSettings[];
}
class JobRadarrSync {
private running = false;
private progress = 0;
private enable4k = false;
private sessionId: string;
private servers: RadarrSettings[];
private currentServer: RadarrSettings;
private radarrApi: RadarrAPI;
private items: RadarrMovie[] = [];
public async run() {
const settings = getSettings();
const sessionId = uuid();
this.sessionId = sessionId;
this.log('Radarr sync starting', 'info', { sessionId });
try {
this.running = true;
// Remove any duplicate Radarr servers and assign them to the servers field
this.servers = uniqWith(settings.radarr, (radarrA, radarrB) => {
return (
radarrA.hostname === radarrB.hostname &&
radarrA.port === radarrB.port &&
radarrA.baseUrl === radarrB.baseUrl
);
});
this.enable4k = settings.radarr.some((radarr) => radarr.is4k);
if (this.enable4k) {
this.log(
'At least one 4K Radarr server was detected. 4K movie detection is now enabled.',
'info'
);
}
for (const server of this.servers) {
this.currentServer = server;
if (server.syncEnabled) {
this.log(
`Beginning to process Radarr server: ${server.name}`,
'info'
);
this.radarrApi = new RadarrAPI({
apiKey: server.apiKey,
url: RadarrAPI.buildRadarrUrl(server, '/api/v3'),
});
this.items = await this.radarrApi.getMovies();
await this.loop({ sessionId });
} else {
this.log(`Sync not enabled. Skipping Radarr server: ${server.name}`);
}
}
this.log('Radarr sync complete', 'info');
} catch (e) {
this.log('Something went wrong.', 'error', { errorMessage: e.message });
} finally {
// If a new scanning session hasnt started, set running back to false
if (this.sessionId === sessionId) {
this.running = false;
}
}
}
public status(): SyncStatus {
return {
running: this.running,
progress: this.progress,
total: this.items.length,
currentServer: this.currentServer,
servers: this.servers,
};
}
public cancel(): void {
this.running = false;
}
private async processRadarrMovie(radarrMovie: RadarrMovie) {
const mediaRepository = getRepository(Media);
const server4k = this.enable4k && this.currentServer.is4k;
const media = await mediaRepository.findOne({
where: { tmdbId: radarrMovie.tmdbId },
});
if (media) {
let isChanged = false;
if (media.status === MediaStatus.AVAILABLE) {
this.log(`Movie already available: ${radarrMovie.title}`);
} else {
media[server4k ? 'status4k' : 'status'] = radarrMovie.downloaded
? MediaStatus.AVAILABLE
: MediaStatus.PROCESSING;
this.log(
`Updated existing ${server4k ? '4K ' : ''}movie ${
radarrMovie.title
} to status ${MediaStatus[media[server4k ? 'status4k' : 'status']]}`
);
isChanged = true;
}
if (
media[server4k ? 'serviceId4k' : 'serviceId'] !== this.currentServer.id
) {
media[server4k ? 'serviceId4k' : 'serviceId'] = this.currentServer.id;
this.log(`Updated service ID for media entity: ${radarrMovie.title}`);
isChanged = true;
}
if (
media[server4k ? 'externalServiceId4k' : 'externalServiceId'] !==
radarrMovie.id
) {
media[server4k ? 'externalServiceId4k' : 'externalServiceId'] =
radarrMovie.id;
this.log(
`Updated external service ID for media entity: ${radarrMovie.title}`
);
isChanged = true;
}
if (
media[server4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] !==
radarrMovie.titleSlug
) {
media[server4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
radarrMovie.titleSlug;
this.log(
`Updated external service slug for media entity: ${radarrMovie.title}`
);
isChanged = true;
}
if (isChanged) {
await mediaRepository.save(media);
}
} else {
const newMedia = new Media({
tmdbId: radarrMovie.tmdbId,
imdbId: radarrMovie.imdbId,
mediaType: MediaType.MOVIE,
serviceId: !server4k ? this.currentServer.id : undefined,
serviceId4k: server4k ? this.currentServer.id : undefined,
externalServiceId: !server4k ? radarrMovie.id : undefined,
externalServiceId4k: server4k ? radarrMovie.id : undefined,
status:
!server4k && radarrMovie.downloaded
? MediaStatus.AVAILABLE
: !server4k
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN,
status4k:
server4k && radarrMovie.downloaded
? MediaStatus.AVAILABLE
: server4k
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN,
});
this.log(
`Added media for movie ${radarrMovie.title} and set status to ${
MediaStatus[newMedia[server4k ? 'status4k' : 'status']]
}`
);
await mediaRepository.save(newMedia);
}
}
private async processItems(items: RadarrMovie[]) {
await Promise.all(
items.map(async (radarrMovie) => {
await this.processRadarrMovie(radarrMovie);
})
);
}
private async loop({
start = 0,
end = BUNDLE_SIZE,
sessionId,
}: {
start?: number;
end?: number;
sessionId?: string;
} = {}) {
const slicedItems = this.items.slice(start, end);
if (!this.running) {
throw new Error('Sync was aborted.');
}
if (this.sessionId !== sessionId) {
throw new Error('New session was started. Old session aborted.');
}
if (start < this.items.length) {
this.progress = start;
await this.processItems(slicedItems);
await new Promise<void>((resolve, reject) =>
setTimeout(() => {
this.loop({
start: start + BUNDLE_SIZE,
end: end + BUNDLE_SIZE,
sessionId,
})
.then(() => resolve())
.catch((e) => reject(new Error(e.message)));
}, UPDATE_RATE)
);
}
}
private log(
message: string,
level: 'info' | 'error' | 'debug' | 'warn' = 'debug',
optional?: Record<string, unknown>
): void {
logger[level](message, { label: 'Radarr Sync', ...optional });
}
}
export const jobRadarrSync = new JobRadarrSync();

View File

@@ -1,10 +1,17 @@
import schedule from 'node-schedule';
import { jobPlexFullSync, jobPlexRecentSync } from './plexsync';
import logger from '../logger';
import { jobRadarrSync } from './radarrsync';
import { jobSonarrSync } from './sonarrsync';
import downloadTracker from '../lib/downloadtracker';
interface ScheduledJob {
id: string;
job: schedule.Job;
name: string;
type: 'process' | 'command';
running?: () => boolean;
cancelFn?: () => void;
}
export const scheduledJobs: ScheduledJob[] = [];
@@ -12,21 +19,80 @@ export const scheduledJobs: ScheduledJob[] = [];
export const startJobs = (): void => {
// Run recently added plex sync every 5 minutes
scheduledJobs.push({
id: 'plex-recently-added-sync',
name: 'Plex Recently Added Sync',
job: schedule.scheduleJob('0 */10 * * * *', () => {
type: 'process',
job: schedule.scheduleJob('0 */5 * * * *', () => {
logger.info('Starting scheduled job: Plex Recently Added Sync', {
label: 'Jobs',
});
jobPlexRecentSync.run();
}),
running: () => jobPlexRecentSync.status().running,
cancelFn: () => jobPlexRecentSync.cancel(),
});
// Run full plex sync every 6 hours
// Run full plex sync every 24 hours
scheduledJobs.push({
id: 'plex-full-sync',
name: 'Plex Full Library Sync',
job: schedule.scheduleJob('0 0 */6 * * *', () => {
type: 'process',
job: schedule.scheduleJob('0 0 3 * * *', () => {
logger.info('Starting scheduled job: Plex Full Sync', { label: 'Jobs' });
jobPlexFullSync.run();
}),
running: () => jobPlexFullSync.status().running,
cancelFn: () => jobPlexFullSync.cancel(),
});
// Run full radarr sync every 24 hours
scheduledJobs.push({
id: 'radarr-sync',
name: 'Radarr Sync',
type: 'process',
job: schedule.scheduleJob('0 0 4 * * *', () => {
logger.info('Starting scheduled job: Radarr Sync', { label: 'Jobs' });
jobRadarrSync.run();
}),
running: () => jobRadarrSync.status().running,
cancelFn: () => jobRadarrSync.cancel(),
});
// Run full sonarr sync every 24 hours
scheduledJobs.push({
id: 'sonarr-sync',
name: 'Sonarr Sync',
type: 'process',
job: schedule.scheduleJob('0 30 4 * * *', () => {
logger.info('Starting scheduled job: Sonarr Sync', { label: 'Jobs' });
jobSonarrSync.run();
}),
running: () => jobSonarrSync.status().running,
cancelFn: () => jobSonarrSync.cancel(),
});
// Run download sync
scheduledJobs.push({
id: 'download-sync',
name: 'Download Sync',
type: 'command',
job: schedule.scheduleJob('0 * * * * *', () => {
logger.debug('Starting scheduled job: Download Sync', { label: 'Jobs' });
downloadTracker.updateDownloads();
}),
});
// Reset download sync
scheduledJobs.push({
id: 'download-sync-reset',
name: 'Download Sync Reset',
type: 'command',
job: schedule.scheduleJob('0 0 1 * * *', () => {
logger.info('Starting scheduled job: Download Sync Reset', {
label: 'Jobs',
});
downloadTracker.resetDownloadTracker();
}),
});
logger.info('Scheduled jobs loaded', { label: 'Jobs' });

View File

@@ -0,0 +1,370 @@
import { uniqWith } from 'lodash';
import { getRepository } from 'typeorm';
import { v4 as uuid } from 'uuid';
import SonarrAPI, { SonarrSeries } from '../../api/sonarr';
import TheMovieDb, { TmdbTvDetails } from '../../api/themoviedb';
import { MediaStatus, MediaType } from '../../constants/media';
import Media from '../../entity/Media';
import Season from '../../entity/Season';
import { getSettings, SonarrSettings } from '../../lib/settings';
import logger from '../../logger';
const BUNDLE_SIZE = 50;
const UPDATE_RATE = 4 * 1000;
interface SyncStatus {
running: boolean;
progress: number;
total: number;
currentServer: SonarrSettings;
servers: SonarrSettings[];
}
class JobSonarrSync {
private running = false;
private progress = 0;
private enable4k = false;
private sessionId: string;
private servers: SonarrSettings[];
private currentServer: SonarrSettings;
private sonarrApi: SonarrAPI;
private items: SonarrSeries[] = [];
public async run() {
const settings = getSettings();
const sessionId = uuid();
this.sessionId = sessionId;
this.log('Sonarr sync starting', 'info', { sessionId });
try {
this.running = true;
// Remove any duplicate Sonarr servers and assign them to the servers field
this.servers = uniqWith(settings.sonarr, (sonarrA, sonarrB) => {
return (
sonarrA.hostname === sonarrB.hostname &&
sonarrA.port === sonarrB.port &&
sonarrA.baseUrl === sonarrB.baseUrl
);
});
this.enable4k = settings.sonarr.some((sonarr) => sonarr.is4k);
if (this.enable4k) {
this.log(
'At least one 4K Sonarr server was detected. 4K movie detection is now enabled.',
'info'
);
}
for (const server of this.servers) {
this.currentServer = server;
if (server.syncEnabled) {
this.log(
`Beginning to process Sonarr server: ${server.name}`,
'info'
);
this.sonarrApi = new SonarrAPI({
apiKey: server.apiKey,
url: SonarrAPI.buildSonarrUrl(server, '/api/v3'),
});
this.items = await this.sonarrApi.getSeries();
await this.loop({ sessionId });
} else {
this.log(`Sync not enabled. Skipping Sonarr server: ${server.name}`);
}
}
this.log('Sonarr sync complete', 'info');
} catch (e) {
this.log('Something went wrong.', 'error', { errorMessage: e.message });
} finally {
// If a new scanning session hasnt started, set running back to false
if (this.sessionId === sessionId) {
this.running = false;
}
}
}
public status(): SyncStatus {
return {
running: this.running,
progress: this.progress,
total: this.items.length,
currentServer: this.currentServer,
servers: this.servers,
};
}
public cancel(): void {
this.running = false;
}
private async processSonarrSeries(sonarrSeries: SonarrSeries) {
const mediaRepository = getRepository(Media);
const server4k = this.enable4k && this.currentServer.is4k;
const media = await mediaRepository.findOne({
where: { tvdbId: sonarrSeries.tvdbId },
});
const currentSeasonsAvailable = (media?.seasons ?? []).filter(
(season) =>
season[server4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
).length;
const newSeasons: Season[] = [];
for (const season of sonarrSeries.seasons) {
const existingSeason = media?.seasons.find(
(es) => es.seasonNumber === season.seasonNumber
);
// We are already tracking this season so we can work on it directly
if (existingSeason) {
if (
existingSeason[server4k ? 'status4k' : 'status'] !==
MediaStatus.AVAILABLE &&
season.statistics
) {
existingSeason[server4k ? 'status4k' : 'status'] =
season.statistics.episodeFileCount ===
season.statistics.totalEpisodeCount
? MediaStatus.AVAILABLE
: season.statistics.episodeFileCount > 0
? MediaStatus.PARTIALLY_AVAILABLE
: season.monitored
? MediaStatus.PROCESSING
: existingSeason[server4k ? 'status4k' : 'status'];
}
} else {
if (season.statistics && season.seasonNumber !== 0) {
const allEpisodes =
season.statistics.episodeFileCount ===
season.statistics.totalEpisodeCount;
newSeasons.push(
new Season({
seasonNumber: season.seasonNumber,
status:
!server4k && allEpisodes
? MediaStatus.AVAILABLE
: !server4k && season.statistics.episodeFileCount > 0
? MediaStatus.PARTIALLY_AVAILABLE
: !server4k && season.monitored
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN,
status4k:
server4k && allEpisodes
? MediaStatus.AVAILABLE
: server4k && season.statistics.episodeFileCount > 0
? MediaStatus.PARTIALLY_AVAILABLE
: !server4k && season.monitored
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN,
})
);
}
}
}
const filteredSeasons = sonarrSeries.seasons.filter(
(s) => s.seasonNumber !== 0
);
const isAllSeasons =
(media?.seasons ?? []).filter(
(s) => s[server4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
).length +
newSeasons.filter(
(s) => s[server4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
).length >=
filteredSeasons.length && filteredSeasons.length > 0;
if (media) {
media.seasons = [...media.seasons, ...newSeasons];
const newSeasonsAvailable = (media?.seasons ?? []).filter(
(season) =>
season[server4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
).length;
if (newSeasonsAvailable > currentSeasonsAvailable) {
this.log(
`Detected ${newSeasonsAvailable - currentSeasonsAvailable} new ${
server4k ? '4K ' : ''
}season(s) for ${sonarrSeries.title}`,
'debug'
);
media.lastSeasonChange = new Date();
}
if (
media[server4k ? 'serviceId4k' : 'serviceId'] !== this.currentServer.id
) {
media[server4k ? 'serviceId4k' : 'serviceId'] = this.currentServer.id;
this.log(`Updated service ID for media entity: ${sonarrSeries.title}`);
}
if (
media[server4k ? 'externalServiceId4k' : 'externalServiceId'] !==
sonarrSeries.id
) {
media[server4k ? 'externalServiceId4k' : 'externalServiceId'] =
sonarrSeries.id;
this.log(
`Updated external service ID for media entity: ${sonarrSeries.title}`
);
}
if (
media[server4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] !==
sonarrSeries.titleSlug
) {
media[server4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
sonarrSeries.titleSlug;
this.log(
`Updated external service slug for media entity: ${sonarrSeries.title}`
);
}
// If the show is already available, and there are no new seasons, dont adjust
// the status
const shouldStayAvailable =
media.status === MediaStatus.AVAILABLE &&
newSeasons.filter(
(season) =>
season[server4k ? 'status4k' : 'status'] !== MediaStatus.UNKNOWN
).length === 0;
media[server4k ? 'status4k' : 'status'] =
isAllSeasons || shouldStayAvailable
? MediaStatus.AVAILABLE
: media.seasons.some(
(season) => season.status !== MediaStatus.UNKNOWN
)
? MediaStatus.PARTIALLY_AVAILABLE
: MediaStatus.UNKNOWN;
await mediaRepository.save(media);
} else {
const tmdb = new TheMovieDb();
let tvShow: TmdbTvDetails;
try {
tvShow = await tmdb.getShowByTvdbId({
tvdbId: sonarrSeries.tvdbId,
});
} catch (e) {
this.log(
'Failed to create new media item during sync. TVDB ID is missing from TMDB?',
'warn',
{ sonarrSeries, errorMessage: e.message }
);
return;
}
const newMedia = new Media({
tmdbId: tvShow.id,
tvdbId: sonarrSeries.tvdbId,
mediaType: MediaType.TV,
serviceId: !server4k ? this.currentServer.id : undefined,
serviceId4k: server4k ? this.currentServer.id : undefined,
externalServiceId: !server4k ? sonarrSeries.id : undefined,
externalServiceId4k: server4k ? sonarrSeries.id : undefined,
externalServiceSlug: !server4k ? sonarrSeries.titleSlug : undefined,
externalServiceSlug4k: server4k ? sonarrSeries.titleSlug : undefined,
seasons: newSeasons,
status:
!server4k && isAllSeasons
? MediaStatus.AVAILABLE
: !server4k &&
newSeasons.some(
(s) =>
s.status === MediaStatus.PARTIALLY_AVAILABLE ||
s.status === MediaStatus.AVAILABLE
)
? MediaStatus.PARTIALLY_AVAILABLE
: !server4k
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN,
status4k:
server4k && isAllSeasons
? MediaStatus.AVAILABLE
: server4k &&
newSeasons.some(
(s) =>
s.status4k === MediaStatus.PARTIALLY_AVAILABLE ||
s.status4k === MediaStatus.AVAILABLE
)
? MediaStatus.PARTIALLY_AVAILABLE
: server4k
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN,
});
this.log(
`Added media for series ${sonarrSeries.title} and set status to ${
MediaStatus[newMedia[server4k ? 'status4k' : 'status']]
}`
);
await mediaRepository.save(newMedia);
}
}
private async processItems(items: SonarrSeries[]) {
await Promise.all(
items.map(async (sonarrSeries) => {
await this.processSonarrSeries(sonarrSeries);
})
);
}
private async loop({
start = 0,
end = BUNDLE_SIZE,
sessionId,
}: {
start?: number;
end?: number;
sessionId?: string;
} = {}) {
const slicedItems = this.items.slice(start, end);
if (!this.running) {
throw new Error('Sync was aborted.');
}
if (this.sessionId !== sessionId) {
throw new Error('New session was started. Old session aborted.');
}
if (start < this.items.length) {
this.progress = start;
await this.processItems(slicedItems);
await new Promise<void>((resolve, reject) =>
setTimeout(() => {
this.loop({
start: start + BUNDLE_SIZE,
end: end + BUNDLE_SIZE,
sessionId,
})
.then(() => resolve())
.catch((e) => reject(new Error(e.message)));
}, UPDATE_RATE)
);
}
}
private log(
message: string,
level: 'info' | 'error' | 'debug' | 'warn' = 'debug',
optional?: Record<string, unknown>
): void {
logger[level](message, { label: 'Sonarr Sync', ...optional });
}
}
export const jobSonarrSync = new JobSonarrSync();

View File

@@ -0,0 +1,195 @@
import { uniqWith } from 'lodash';
import RadarrAPI from '../api/radarr';
import SonarrAPI from '../api/sonarr';
import { MediaType } from '../constants/media';
import logger from '../logger';
import { getSettings } from './settings';
export interface DownloadingItem {
mediaType: MediaType;
externalId: number;
size: number;
sizeLeft: number;
status: string;
timeLeft: string;
estimatedCompletionTime: Date;
title: string;
}
class DownloadTracker {
private radarrServers: Record<number, DownloadingItem[]> = {};
private sonarrServers: Record<number, DownloadingItem[]> = {};
public getMovieProgress(
serverId: number,
externalServiceId: number
): DownloadingItem[] {
if (!this.radarrServers[serverId]) {
return [];
}
return this.radarrServers[serverId].filter(
(item) => item.externalId === externalServiceId
);
}
public getSeriesProgress(
serverId: number,
externalServiceId: number
): DownloadingItem[] {
if (!this.sonarrServers[serverId]) {
return [];
}
return this.sonarrServers[serverId].filter(
(item) => item.externalId === externalServiceId
);
}
public async resetDownloadTracker() {
this.radarrServers = {};
}
public updateDownloads() {
this.updateRadarrDownloads();
this.updateSonarrDownloads();
}
private async updateRadarrDownloads() {
const settings = getSettings();
// Remove duplicate servers
const filteredServers = uniqWith(settings.radarr, (radarrA, radarrB) => {
return (
radarrA.hostname === radarrB.hostname &&
radarrA.port === radarrB.port &&
radarrA.baseUrl === radarrB.baseUrl
);
});
// Load downloads from Radarr servers
Promise.all(
filteredServers.map(async (server) => {
if (server.syncEnabled) {
const radarr = new RadarrAPI({
apiKey: server.apiKey,
url: RadarrAPI.buildRadarrUrl(server, '/api/v3'),
});
const queueItems = await radarr.getQueue();
this.radarrServers[server.id] = queueItems.map((item) => ({
externalId: item.movieId,
estimatedCompletionTime: new Date(item.estimatedCompletionTime),
mediaType: MediaType.MOVIE,
size: item.size,
sizeLeft: item.sizeleft,
status: item.status,
timeLeft: item.timeleft,
title: item.title,
}));
if (queueItems.length > 0) {
logger.debug(
`Found ${queueItems.length} item(s) in progress on Radarr server: ${server.name}`,
{ label: 'Download Tracker' }
);
}
// Duplicate this data to matching servers
const matchingServers = settings.radarr.filter(
(rs) =>
rs.hostname === server.hostname &&
rs.port === server.port &&
rs.baseUrl === server.baseUrl &&
rs.id !== server.id
);
if (matchingServers.length > 0) {
logger.debug(
`Matching download data to ${matchingServers.length} other Radarr server(s)`,
{ label: 'Download Tracker' }
);
}
matchingServers.forEach((ms) => {
if (ms.syncEnabled) {
this.radarrServers[ms.id] = this.radarrServers[server.id];
}
});
}
})
);
}
private async updateSonarrDownloads() {
const settings = getSettings();
// Remove duplicate servers
const filteredServers = uniqWith(settings.sonarr, (sonarrA, sonarrB) => {
return (
sonarrA.hostname === sonarrB.hostname &&
sonarrA.port === sonarrB.port &&
sonarrA.baseUrl === sonarrB.baseUrl
);
});
// Load downloads from Radarr servers
Promise.all(
filteredServers.map(async (server) => {
if (server.syncEnabled) {
const radarr = new SonarrAPI({
apiKey: server.apiKey,
url: SonarrAPI.buildSonarrUrl(server, '/api/v3'),
});
const queueItems = await radarr.getQueue();
this.sonarrServers[server.id] = queueItems.map((item) => ({
externalId: item.seriesId,
estimatedCompletionTime: new Date(item.estimatedCompletionTime),
mediaType: MediaType.TV,
size: item.size,
sizeLeft: item.sizeleft,
status: item.status,
timeLeft: item.timeleft,
title: item.title,
}));
if (queueItems.length > 0) {
logger.debug(
`Found ${queueItems.length} item(s) in progress on Sonarr server: ${server.name}`,
{ label: 'Download Tracker' }
);
}
// Duplicate this data to matching servers
const matchingServers = settings.sonarr.filter(
(rs) =>
rs.hostname === server.hostname &&
rs.port === server.port &&
rs.baseUrl === server.baseUrl &&
rs.id !== server.id
);
if (matchingServers.length > 0) {
logger.debug(
`Matching download data to ${matchingServers.length} other Sonarr server(s)`,
{ label: 'Download Tracker' }
);
}
matchingServers.forEach((ms) => {
if (ms.syncEnabled) {
this.sonarrServers[ms.id] = this.sonarrServers[server.id];
}
});
}
})
);
}
}
const downloadTracker = new DownloadTracker();
export default downloadTracker;

38
server/lib/email/index.ts Normal file
View File

@@ -0,0 +1,38 @@
import nodemailer from 'nodemailer';
import Email from 'email-templates';
import { getSettings } from '../settings';
class PreparedEmail extends Email {
public constructor() {
const settings = getSettings().notifications.agents.email;
const transport = nodemailer.createTransport({
host: settings.options.smtpHost,
port: settings.options.smtpPort,
secure: settings.options.secure,
tls: settings.options.allowSelfSigned
? {
rejectUnauthorized: false,
}
: undefined,
auth:
settings.options.authUser && settings.options.authPass
? {
user: settings.options.authUser,
pass: settings.options.authPass,
}
: undefined,
});
super({
message: {
from: {
name: settings.options.senderName,
address: settings.options.emailFrom,
},
},
send: true,
transport: transport,
});
}
}
export default PreparedEmail;

View File

@@ -1,14 +1,26 @@
import { Notification } from '..';
import Media from '../../../entity/Media';
import { User } from '../../../entity/User';
import { NotificationAgentConfig } from '../../settings';
export interface NotificationPayload {
subject: string;
notifyUser: User;
media?: Media;
image?: string;
message?: string;
extra?: { name: string; value: string }[];
}
export abstract class BaseAgent<T extends NotificationAgentConfig> {
protected settings?: T;
public constructor(settings?: T) {
this.settings = settings;
}
protected abstract getSettings(): T;
}
export interface NotificationAgent {
shouldSend(type: Notification): boolean;
send(type: Notification, payload: NotificationPayload): Promise<boolean>;

View File

@@ -1,8 +1,8 @@
import axios from 'axios';
import { Notification } from '..';
import { hasNotificationType, Notification } from '..';
import logger from '../../../logger';
import { getSettings } from '../../settings';
import type { NotificationAgent, NotificationPayload } from './agent';
import { getSettings, NotificationAgentDiscord } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
enum EmbedColors {
DEFAULT = 0,
@@ -37,6 +37,11 @@ interface DiscordImageEmbed {
width?: number;
}
interface Field {
name: string;
value: string;
inline?: boolean;
}
interface DiscordRichEmbed {
title?: string;
type?: 'rich'; // Always rich for webhooks
@@ -61,11 +66,7 @@ interface DiscordRichEmbed {
icon_url?: string;
proxy_icon_url?: string;
};
fields?: {
name: string;
value: string;
inline?: boolean;
}[];
fields?: Field[];
}
interface DiscordWebhookPayload {
@@ -75,26 +76,125 @@ interface DiscordWebhookPayload {
tts: boolean;
}
class DiscordAgent implements NotificationAgent {
class DiscordAgent
extends BaseAgent<NotificationAgentDiscord>
implements NotificationAgent {
protected getSettings(): NotificationAgentDiscord {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.discord;
}
public buildEmbed(
type: Notification,
payload: NotificationPayload
): DiscordRichEmbed {
let color = EmbedColors.DEFAULT;
let status = 'Unknown';
const settings = getSettings();
let color = EmbedColors.DARK_PURPLE;
const fields: Field[] = [];
switch (type) {
case Notification.MEDIA_PENDING:
color = EmbedColors.ORANGE;
status = 'Pending Approval';
fields.push(
{
name: 'Requested By',
value: payload.notifyUser.displayName ?? '',
inline: true,
},
{
name: 'Status',
value: 'Pending Approval',
inline: true,
}
);
if (settings.main.applicationUrl) {
fields.push({
name: 'View Media',
value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
});
}
break;
case Notification.MEDIA_APPROVED:
color = EmbedColors.PURPLE;
status = 'Processing Request';
fields.push(
{
name: 'Requested By',
value: payload.notifyUser.displayName ?? '',
inline: true,
},
{
name: 'Status',
value: 'Processing Request',
inline: true,
}
);
if (settings.main.applicationUrl) {
fields.push({
name: 'View Media',
value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
});
}
break;
case Notification.MEDIA_AVAILABLE:
color = EmbedColors.GREEN;
status = 'Available';
fields.push(
{
name: 'Requested By',
value: payload.notifyUser.displayName ?? '',
inline: true,
},
{
name: 'Status',
value: 'Available',
inline: true,
}
);
if (settings.main.applicationUrl) {
fields.push({
name: 'View Media',
value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
});
}
break;
case Notification.MEDIA_DECLINED:
color = EmbedColors.RED;
fields.push(
{
name: 'Requested By',
value: payload.notifyUser.displayName ?? '',
inline: true,
},
{
name: 'Status',
value: 'Declined',
inline: true,
}
);
if (settings.main.applicationUrl) {
fields.push({
name: 'View Media',
value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
});
}
break;
case Notification.MEDIA_FAILED:
color = EmbedColors.RED;
if (settings.main.applicationUrl) {
fields.push({
name: 'View Media',
value: `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`,
});
}
break;
}
@@ -103,18 +203,9 @@ class DiscordAgent implements NotificationAgent {
description: payload.message,
color,
timestamp: new Date().toISOString(),
author: { name: 'Overseerr' },
author: { name: 'Overseerr', url: settings.main.applicationUrl },
fields: [
{
name: 'Requested By',
value: payload.notifyUser.username ?? '',
inline: true,
},
{
name: 'Status',
value: status,
inline: true,
},
...fields,
// If we have extra data, map it to fields for discord notifications
...(payload.extra ?? []).map((extra) => ({
name: extra.name,
@@ -127,14 +218,11 @@ class DiscordAgent implements NotificationAgent {
};
}
// TODO: Add checking for type here once we add notification type filters for agents
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public shouldSend(_type: Notification): boolean {
const settings = getSettings();
public shouldSend(type: Notification): boolean {
if (
settings.notifications.agents.discord?.enabled &&
settings.notifications.agents.discord?.options?.webhookUrl
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
@@ -146,11 +234,9 @@ class DiscordAgent implements NotificationAgent {
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
const settings = getSettings();
logger.debug('Sending discord notification', { label: 'Notifications' });
try {
const webhookUrl =
settings.notifications.agents.discord?.options?.webhookUrl;
const webhookUrl = this.getSettings().options.webhookUrl;
if (!webhookUrl) {
return false;

View File

@@ -1,54 +1,42 @@
import type { NotificationAgent, NotificationPayload } from './agent';
import { Notification } from '..';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
import { hasNotificationType, Notification } from '..';
import path from 'path';
import { getSettings } from '../../settings';
import nodemailer from 'nodemailer';
import Email from 'email-templates';
import { getSettings, NotificationAgentEmail } from '../../settings';
import logger from '../../../logger';
import { getRepository } from 'typeorm';
import { User } from '../../../entity/User';
import { Permission } from '../../permissions';
import PreparedEmail from '../../email';
class EmailAgent
extends BaseAgent<NotificationAgentEmail>
implements NotificationAgent {
protected getSettings(): NotificationAgentEmail {
if (this.settings) {
return this.settings;
}
class EmailAgent implements NotificationAgent {
// TODO: Add checking for type here once we add notification type filters for agents
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public shouldSend(_type: Notification): boolean {
const settings = getSettings();
if (settings.notifications.agents.email.enabled) {
return settings.notifications.agents.email;
}
public shouldSend(type: Notification): boolean {
const settings = this.getSettings();
if (
settings.enabled &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
private getSmtpTransport() {
const emailSettings = getSettings().notifications.agents.email.options;
return nodemailer.createTransport({
host: emailSettings.smtpHost,
port: emailSettings.smtpPort,
secure: emailSettings.secure,
auth: {
user: emailSettings.authUser,
pass: emailSettings.authPass,
},
});
}
private getNewEmail() {
const settings = getSettings().notifications.agents.email;
return new Email({
message: {
from: settings.options.emailFrom,
},
send: true,
transport: this.getSmtpTransport(),
});
}
private async sendMediaRequestEmail(payload: NotificationPayload) {
const settings = getSettings().main;
// This is getting main settings for the whole app
const applicationUrl = getSettings().main.applicationUrl;
try {
const userRepository = getRepository(User);
const users = await userRepository.find();
@@ -57,7 +45,7 @@ class EmailAgent implements NotificationAgent {
users
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
.forEach((user) => {
const email = this.getNewEmail();
const email = new PreparedEmail();
email.send({
template: path.join(
@@ -72,8 +60,11 @@ class EmailAgent implements NotificationAgent {
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.username,
actionUrl: settings.applicationUrl,
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
requestType: 'New Request',
},
});
@@ -88,12 +79,59 @@ class EmailAgent implements NotificationAgent {
}
}
private async sendMediaApprovedEmail(payload: NotificationPayload) {
const settings = getSettings().main;
private async sendMediaFailedEmail(payload: NotificationPayload) {
// This is getting main settings for the whole app
const applicationUrl = getSettings().main.applicationUrl;
try {
const email = this.getNewEmail();
const userRepository = getRepository(User);
const users = await userRepository.find();
email.send({
// Send to all users with the manage requests permission (or admins)
users
.filter((user) => user.hasPermission(Permission.MANAGE_REQUESTS))
.forEach((user) => {
const email = new PreparedEmail();
email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
),
message: {
to: user.email,
},
locals: {
body:
"A user's new request has failed to add to Sonarr or Radarr",
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
requestType: 'Failed Request',
},
});
});
return true;
} catch (e) {
logger.error('Mail notification failed to send', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
private async sendMediaApprovedEmail(payload: NotificationPayload) {
// This is getting main settings for the whole app
const applicationUrl = getSettings().main.applicationUrl;
try {
const email = new PreparedEmail();
await email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
@@ -106,8 +144,11 @@ class EmailAgent implements NotificationAgent {
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.username,
actionUrl: settings.applicationUrl,
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
requestType: 'Request Approved',
},
});
@@ -121,12 +162,13 @@ class EmailAgent implements NotificationAgent {
}
}
private async sendMediaAvailableEmail(payload: NotificationPayload) {
const settings = getSettings().main;
private async sendMediaDeclinedEmail(payload: NotificationPayload) {
// This is getting main settings for the whole app
const applicationUrl = getSettings().main.applicationUrl;
try {
const email = this.getNewEmail();
const email = new PreparedEmail();
email.send({
await email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
@@ -135,12 +177,52 @@ class EmailAgent implements NotificationAgent {
to: payload.notifyUser.email,
},
locals: {
body: 'Your requsested media is now available!',
body: 'Your request for the following media was declined:',
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.username,
actionUrl: settings.applicationUrl,
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
requestType: 'Request Declined',
},
});
return true;
} catch (e) {
logger.error('Mail notification failed to send', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
private async sendMediaAvailableEmail(payload: NotificationPayload) {
// This is getting main settings for the whole app
const applicationUrl = getSettings().main.applicationUrl;
try {
const email = new PreparedEmail();
await email.send({
template: path.join(
__dirname,
'../../../templates/email/media-request'
),
message: {
to: payload.notifyUser.email,
},
locals: {
body: 'Your requested media is now available!',
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),
requestedBy: payload.notifyUser.displayName,
actionUrl: applicationUrl
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
: undefined,
applicationUrl,
requestType: 'Now Available',
},
});
@@ -154,6 +236,32 @@ class EmailAgent implements NotificationAgent {
}
}
private async sendTestEmail(payload: NotificationPayload) {
// This is getting main settings for the whole app
const applicationUrl = getSettings().main.applicationUrl;
try {
const email = new PreparedEmail();
await email.send({
template: path.join(__dirname, '../../../templates/email/test-email'),
message: {
to: payload.notifyUser.email,
},
locals: {
body: payload.message,
applicationUrl,
},
});
return true;
} catch (e) {
logger.error('Mail notification failed to send', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
public async send(
type: Notification,
payload: NotificationPayload
@@ -167,9 +275,18 @@ class EmailAgent implements NotificationAgent {
case Notification.MEDIA_APPROVED:
this.sendMediaApprovedEmail(payload);
break;
case Notification.MEDIA_DECLINED:
this.sendMediaDeclinedEmail(payload);
break;
case Notification.MEDIA_AVAILABLE:
this.sendMediaAvailableEmail(payload);
break;
case Notification.MEDIA_FAILED:
this.sendMediaFailedEmail(payload);
break;
case Notification.TEST_NOTIFICATION:
this.sendTestEmail(payload);
break;
}
return true;

View File

@@ -0,0 +1,129 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import logger from '../../../logger';
import { getSettings, NotificationAgentPushover } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface PushoverPayload {
token: string;
user: string;
title: string;
message: string;
html: number;
}
class PushoverAgent
extends BaseAgent<NotificationAgentPushover>
implements NotificationAgent {
protected getSettings(): NotificationAgentPushover {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.pushover;
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.accessToken &&
this.getSettings().options.userToken &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
private constructMessageDetails(
type: Notification,
payload: NotificationPayload
): { title: string; message: string } {
const settings = getSettings();
let messageTitle = '';
let message = '';
const title = payload.subject;
const plot = payload.message;
const username = payload.notifyUser.displayName;
switch (type) {
case Notification.MEDIA_PENDING:
messageTitle = 'New Request';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${username}\n\n`;
message += `<b>Status</b>\nPending Approval\n`;
break;
case Notification.MEDIA_APPROVED:
messageTitle = 'Request Approved';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${username}\n\n`;
message += `<b>Status</b>\nProcessing Request\n`;
break;
case Notification.MEDIA_AVAILABLE:
messageTitle = 'Now available!';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${username}\n\n`;
message += `<b>Status</b>\nAvailable\n`;
break;
case Notification.MEDIA_DECLINED:
messageTitle = 'Request Declined';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${username}\n\n`;
message += `<b>Status</b>\nDeclined\n`;
break;
case Notification.TEST_NOTIFICATION:
messageTitle = 'Test Notification';
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `<b>Requested By</b>\n${username}\n`;
break;
}
if (settings.main.applicationUrl && payload.media) {
const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `<a href="${actionUrl}">Open in Overseerr</a>`;
}
return { title: messageTitle, message };
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending Pushover notification', { label: 'Notifications' });
try {
const endpoint = 'https://api.pushover.net/1/messages.json';
const { accessToken, userToken } = this.getSettings().options;
const { title, message } = this.constructMessageDetails(type, payload);
await axios.post(endpoint, {
token: accessToken,
user: userToken,
title: title,
message: message,
html: 1,
} as PushoverPayload);
return true;
} catch (e) {
logger.error('Error sending Pushover notification', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
}
export default PushoverAgent;

View File

@@ -0,0 +1,243 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import logger from '../../../logger';
import { getSettings, NotificationAgentSlack } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface EmbedField {
type: 'plain_text' | 'mrkdwn';
text: string;
}
interface TextItem {
type: 'plain_text' | 'mrkdwn';
text: string;
emoji?: boolean;
}
interface Element {
type: 'button';
text?: TextItem;
value: string;
url: string;
action_id: 'button-action';
}
interface EmbedBlock {
type: 'header' | 'actions' | 'section' | 'context';
block_id?: 'section789';
text?: TextItem;
fields?: EmbedField[];
accessory?: {
type: 'image';
image_url: string;
alt_text: string;
};
elements?: Element[];
}
interface SlackBlockEmbed {
blocks: EmbedBlock[];
}
class SlackAgent
extends BaseAgent<NotificationAgentSlack>
implements NotificationAgent {
protected getSettings(): NotificationAgentSlack {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.slack;
}
public buildEmbed(
type: Notification,
payload: NotificationPayload
): SlackBlockEmbed {
const settings = getSettings();
let header = 'Overseerr';
let actionUrl: string | undefined;
const fields: EmbedField[] = [];
switch (type) {
case Notification.MEDIA_PENDING:
header = 'New Request';
fields.push(
{
type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
},
{
type: 'mrkdwn',
text: '*Status*\nPending Approval',
}
);
if (settings.main.applicationUrl) {
actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`;
}
break;
case Notification.MEDIA_APPROVED:
header = 'Request Approved';
fields.push(
{
type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
},
{
type: 'mrkdwn',
text: '*Status*\nProcessing Request',
}
);
if (settings.main.applicationUrl) {
actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`;
}
break;
case Notification.MEDIA_DECLINED:
header = 'Request Declined';
fields.push(
{
type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
},
{
type: 'mrkdwn',
text: '*Status*\nDeclined',
}
);
if (settings.main.applicationUrl) {
actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`;
}
break;
case Notification.MEDIA_AVAILABLE:
header = 'Now available!';
fields.push(
{
type: 'mrkdwn',
text: `*Requested By*\n${payload.notifyUser.displayName ?? ''}`,
},
{
type: 'mrkdwn',
text: '*Status*\nAvailable',
}
);
if (settings.main.applicationUrl) {
actionUrl = `${settings.main.applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`;
}
break;
}
const blocks: EmbedBlock[] = [
{
type: 'header',
text: {
type: 'plain_text',
text: header,
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${payload.subject}*`,
},
},
];
if (payload.message) {
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: payload.message,
},
accessory: payload.image
? {
type: 'image',
image_url: payload.image,
alt_text: payload.subject,
}
: undefined,
});
}
if (fields.length > 0) {
blocks.push({
type: 'section',
fields: [
...fields,
...(payload.extra ?? []).map(
(extra): EmbedField => ({
type: 'mrkdwn',
text: `*${extra.name}*\n${extra.value}`,
})
),
],
});
}
if (actionUrl) {
blocks.push({
type: 'actions',
elements: [
{
action_id: 'button-action',
type: 'button',
url: actionUrl,
value: 'open_overseerr',
text: {
type: 'plain_text',
text: 'Open Overseerr',
},
},
],
});
}
return {
blocks,
};
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending slack notification', { label: 'Notifications' });
try {
const webhookUrl = this.getSettings().options.webhookUrl;
if (!webhookUrl) {
return false;
}
await axios.post(webhookUrl, this.buildEmbed(type, payload));
return true;
} catch (e) {
logger.error('Error sending Slack notification', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
}
export default SlackAgent;

View File

@@ -0,0 +1,135 @@
import axios from 'axios';
import { hasNotificationType, Notification } from '..';
import logger from '../../../logger';
import { getSettings, NotificationAgentTelegram } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
interface TelegramPayload {
text: string;
parse_mode: string;
chat_id: string;
}
class TelegramAgent
extends BaseAgent<NotificationAgentTelegram>
implements NotificationAgent {
private baseUrl = 'https://api.telegram.org/';
protected getSettings(): NotificationAgentTelegram {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.telegram;
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.botAPI &&
this.getSettings().options.chatId &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
private escapeText(text: string | undefined): string {
return text ? text.replace(/[_*[\]()~>#+=|{}.!-]/gi, (x) => '\\' + x) : '';
}
private buildMessage(
type: Notification,
payload: NotificationPayload
): string {
const settings = getSettings();
let message = '';
const title = this.escapeText(payload.subject);
const plot = this.escapeText(payload.message);
const user = this.escapeText(payload.notifyUser.displayName);
/* eslint-disable no-useless-escape */
switch (type) {
case Notification.MEDIA_PENDING:
message += `\*New Request\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n\n`;
message += `\*Status\*\nPending Approval\n`;
break;
case Notification.MEDIA_APPROVED:
message += `\*Request Approved\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n\n`;
message += `\*Status\*\nProcessing Request\n`;
break;
case Notification.MEDIA_DECLINED:
message += `\*Request Declined\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n\n`;
message += `\*Status\*\nDeclined\n`;
break;
case Notification.MEDIA_AVAILABLE:
message += `\*Now available\\!\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n\n`;
message += `\*Status\*\nAvailable\n`;
break;
case Notification.TEST_NOTIFICATION:
message += `\*Test Notification\*\n`;
message += `${title}\n\n`;
message += `${plot}\n\n`;
message += `\*Requested By\*\n${user}\n`;
break;
}
if (settings.main.applicationUrl && payload.media) {
const actionUrl = `${settings.main.applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `\[Open in Overseerr\]\(${actionUrl}\)`;
}
/* eslint-enable */
return message;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending telegram notification', { label: 'Notifications' });
try {
const endpoint = `${this.baseUrl}bot${
this.getSettings().options.botAPI
}/sendMessage`;
await axios.post(endpoint, {
text: this.buildMessage(type, payload),
parse_mode: 'MarkdownV2',
chat_id: `${this.getSettings().options.chatId}`,
} as TelegramPayload);
return true;
} catch (e) {
logger.error('Error sending Telegram notification', {
label: 'Notifications',
message: e.message,
});
return false;
}
}
}
export default TelegramAgent;

View File

@@ -0,0 +1,139 @@
import axios from 'axios';
import { get } from 'lodash';
import { hasNotificationType, Notification } from '..';
import { MediaStatus } from '../../../constants/media';
import logger from '../../../logger';
import { getSettings, NotificationAgentWebhook } from '../../settings';
import { BaseAgent, NotificationAgent, NotificationPayload } from './agent';
type KeyMapFunction = (
payload: NotificationPayload,
type: Notification
) => string;
const KeyMap: Record<string, string | KeyMapFunction> = {
notification_type: (_payload, type) => Notification[type],
subject: 'subject',
message: 'message',
image: 'image',
notifyuser_username: 'notifyUser.displayName',
notifyuser_email: 'notifyUser.email',
notifyuser_avatar: 'notifyUser.avatar',
media_tmdbid: 'media.tmdbId',
media_imdbid: 'media.imdbId',
media_tvdbid: 'media.tvdbId',
media_type: 'media.mediaType',
media_status: (payload) =>
payload.media?.status ? MediaStatus[payload.media?.status] : '',
media_status4k: (payload) =>
payload.media?.status ? MediaStatus[payload.media?.status4k] : '',
};
class WebhookAgent
extends BaseAgent<NotificationAgentWebhook>
implements NotificationAgent {
protected getSettings(): NotificationAgentWebhook {
if (this.settings) {
return this.settings;
}
const settings = getSettings();
return settings.notifications.agents.webhook;
}
private parseKeys(
finalPayload: Record<string, unknown>,
payload: NotificationPayload,
type: Notification
): Record<string, unknown> {
Object.keys(finalPayload).forEach((key) => {
if (key === '{{extra}}') {
finalPayload.extra = payload.extra ?? [];
delete finalPayload[key];
key = 'extra';
} else if (key === '{{media}}') {
if (payload.media) {
finalPayload.media = finalPayload[key];
} else {
finalPayload.media = null;
}
delete finalPayload[key];
key = 'media';
}
if (typeof finalPayload[key] === 'string') {
Object.keys(KeyMap).forEach((keymapKey) => {
const keymapValue = KeyMap[keymapKey as keyof typeof KeyMap];
finalPayload[key] = (finalPayload[key] as string).replace(
`{{${keymapKey}}}`,
typeof keymapValue === 'function'
? keymapValue(payload, type)
: get(payload, keymapValue) ?? ''
);
});
} else if (finalPayload[key] && typeof finalPayload[key] === 'object') {
finalPayload[key] = this.parseKeys(
finalPayload[key] as Record<string, unknown>,
payload,
type
);
}
});
return finalPayload;
}
private buildPayload(type: Notification, payload: NotificationPayload) {
const payloadString = Buffer.from(
this.getSettings().options.jsonPayload,
'base64'
).toString('ascii');
const parsedJSON = JSON.parse(JSON.parse(payloadString));
return this.parseKeys(parsedJSON, payload, type);
}
public shouldSend(type: Notification): boolean {
if (
this.getSettings().enabled &&
this.getSettings().options.webhookUrl &&
hasNotificationType(type, this.getSettings().types)
) {
return true;
}
return false;
}
public async send(
type: Notification,
payload: NotificationPayload
): Promise<boolean> {
logger.debug('Sending webhook notification', { label: 'Notifications' });
try {
const { webhookUrl, authHeader } = this.getSettings().options;
if (!webhookUrl) {
return false;
}
await axios.post(webhookUrl, this.buildPayload(type, payload), {
headers: {
Authorization: authHeader,
},
});
return true;
} catch (e) {
logger.error('Error sending Webhook notification', {
label: 'Notifications',
errorMessage: e.message,
});
return false;
}
}
}
export default WebhookAgent;

View File

@@ -1,12 +1,37 @@
import logger from '../../logger';
import { getSettings } from '../settings';
import type { NotificationAgent, NotificationPayload } from './agents/agent';
export enum Notification {
MEDIA_PENDING = 2,
MEDIA_APPROVED = 4,
MEDIA_AVAILABLE = 8,
MEDIA_FAILED = 16,
TEST_NOTIFICATION = 32,
MEDIA_DECLINED = 64,
}
export const hasNotificationType = (
types: Notification | Notification[],
value: number
): boolean => {
let total = 0;
// If we are not checking any notifications, bail out and return true
if (types === 0) {
return true;
}
if (Array.isArray(types)) {
// Combine all notification values into one
total = types.reduce((a, v) => a + v, 0);
} else {
total = types;
}
return !!(value & total);
};
class NotificationManager {
private activeAgents: NotificationAgent[] = [];
@@ -19,11 +44,12 @@ class NotificationManager {
type: Notification,
payload: NotificationPayload
): void {
const settings = getSettings().notifications;
logger.info(`Sending notification for ${Notification[type]}`, {
label: 'Notifications',
});
this.activeAgents.forEach((agent) => {
if (agent.shouldSend(type)) {
if (settings.enabled && agent.shouldSend(type)) {
agent.send(type, payload);
}
});

View File

@@ -7,6 +7,12 @@ export enum Permission {
REQUEST = 32,
VOTE = 64,
AUTO_APPROVE = 128,
AUTO_APPROVE_MOVIE = 256,
AUTO_APPROVE_TV = 512,
REQUEST_4K = 1024,
REQUEST_4K_MOVIE = 2048,
REQUEST_4K_TV = 4096,
REQUEST_ADVANCED = 8192,
}
/**

View File

@@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { merge } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { Permission } from './permissions';
export interface Library {
id: string;
@@ -14,6 +15,7 @@ export interface PlexSettings {
machineId?: string;
ip: string;
port: number;
useSsl?: boolean;
libraries: Library[];
}
@@ -30,6 +32,9 @@ interface DVRSettings {
activeDirectory: string;
is4k: boolean;
isDefault: boolean;
externalUrl?: string;
syncEnabled: boolean;
preventSearch: boolean;
}
export interface RadarrSettings extends DVRSettings {
@@ -46,24 +51,40 @@ export interface SonarrSettings extends DVRSettings {
export interface MainSettings {
apiKey: string;
applicationUrl: string;
csrfProtection: boolean;
defaultPermissions: number;
hideAvailable: boolean;
trustProxy: boolean;
}
interface PublicSettings {
initialized: boolean;
}
interface NotificationAgent {
interface FullPublicSettings extends PublicSettings {
movie4kEnabled: boolean;
series4kEnabled: boolean;
hideAvailable: boolean;
}
export interface NotificationAgentConfig {
enabled: boolean;
types: number;
options: Record<string, unknown>;
}
interface NotificationAgentDiscord extends NotificationAgent {
export interface NotificationAgentDiscord extends NotificationAgentConfig {
options: {
webhookUrl: string;
};
}
interface NotificationAgentEmail extends NotificationAgent {
export interface NotificationAgentSlack extends NotificationAgentConfig {
options: {
webhookUrl: string;
};
}
export interface NotificationAgentEmail extends NotificationAgentConfig {
options: {
emailFrom: string;
smtpHost: string;
@@ -71,15 +92,47 @@ interface NotificationAgentEmail extends NotificationAgent {
secure: boolean;
authUser?: string;
authPass?: string;
allowSelfSigned: boolean;
senderName: string;
};
}
export interface NotificationAgentTelegram extends NotificationAgentConfig {
options: {
botAPI: string;
chatId: string;
};
}
export interface NotificationAgentPushover extends NotificationAgentConfig {
options: {
accessToken: string;
userToken: string;
priority: number;
sound: string;
};
}
export interface NotificationAgentWebhook extends NotificationAgentConfig {
options: {
webhookUrl: string;
jsonPayload: string;
authHeader: string;
};
}
interface NotificationAgents {
email: NotificationAgentEmail;
discord: NotificationAgentDiscord;
slack: NotificationAgentSlack;
telegram: NotificationAgentTelegram;
pushover: NotificationAgentPushover;
webhook: NotificationAgentWebhook;
}
interface NotificationSettings {
enabled: boolean;
autoapprovalEnabled: boolean;
agents: NotificationAgents;
}
@@ -93,7 +146,9 @@ interface AllSettings {
notifications: NotificationSettings;
}
const SETTINGS_PATH = path.join(__dirname, '../../config/settings.json');
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/settings.json`
: path.join(__dirname, '../../config/settings.json');
class Settings {
private data: AllSettings;
@@ -104,11 +159,16 @@ class Settings {
main: {
apiKey: '',
applicationUrl: '',
csrfProtection: false,
defaultPermissions: Permission.REQUEST,
hideAvailable: false,
trustProxy: false,
},
plex: {
name: '',
ip: '127.0.0.1',
port: 32400,
useSsl: false,
libraries: [],
},
radarr: [],
@@ -117,6 +177,8 @@ class Settings {
initialized: false,
},
notifications: {
enabled: true,
autoapprovalEnabled: false,
agents: {
email: {
enabled: false,
@@ -124,8 +186,10 @@ class Settings {
options: {
emailFrom: '',
smtpHost: '127.0.0.1',
smtpPort: 465,
smtpPort: 587,
secure: false,
allowSelfSigned: false,
senderName: 'Overseerr',
},
},
discord: {
@@ -135,6 +199,41 @@ class Settings {
webhookUrl: '',
},
},
slack: {
enabled: false,
types: 0,
options: {
webhookUrl: '',
},
},
telegram: {
enabled: false,
types: 0,
options: {
botAPI: '',
chatId: '',
},
},
pushover: {
enabled: false,
types: 0,
options: {
accessToken: '',
userToken: '',
priority: 0,
sound: '',
},
},
webhook: {
enabled: false,
types: 0,
options: {
webhookUrl: '',
authHeader: '',
jsonPayload:
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJzdWJqZWN0XCI6IFwie3tzdWJqZWN0fX1cIixcbiAgICBcIm1lc3NhZ2VcIjogXCJ7e21lc3NhZ2V9fVwiLFxuICAgIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgICBcImVtYWlsXCI6IFwie3tub3RpZnl1c2VyX2VtYWlsfX1cIixcbiAgICBcInVzZXJuYW1lXCI6IFwie3tub3RpZnl1c2VyX3VzZXJuYW1lfX1cIixcbiAgICBcImF2YXRhclwiOiBcInt7bm90aWZ5dXNlcl9hdmF0YXJ9fVwiLFxuICAgIFwie3ttZWRpYX19XCI6IHtcbiAgICAgICAgXCJtZWRpYV90eXBlXCI6IFwie3ttZWRpYV90eXBlfX1cIixcbiAgICAgICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgICAgIFwiaW1kYklkXCI6IFwie3ttZWRpYV9pbWRiaWR9fVwiLFxuICAgICAgICBcInR2ZGJJZFwiOiBcInt7bWVkaWFfdHZkYmlkfX1cIixcbiAgICAgICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgICAgIFwic3RhdHVzNGtcIjogXCJ7e21lZGlhX3N0YXR1czRrfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i',
},
},
},
},
};
@@ -187,6 +286,19 @@ class Settings {
this.data.public = data;
}
get fullPublicSettings(): FullPublicSettings {
return {
...this.data.public,
movie4kEnabled: this.data.radarr.some(
(radarr) => radarr.is4k && radarr.isDefault
),
series4kEnabled: this.data.sonarr.some(
(sonarr) => sonarr.is4k && sonarr.isDefault
),
hideAvailable: this.data.main.hideAvailable,
};
}
get notifications(): NotificationSettings {
return this.data.notifications;
}
@@ -211,7 +323,7 @@ class Settings {
}
private generateApiKey(): string {
return Buffer.from(`${Date.now()}${this.clientId}`).toString('base64');
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
}
/**

View File

@@ -1,5 +1,17 @@
import * as winston from 'winston';
import 'winston-daily-rotate-file';
import path from 'path';
import fs from 'fs';
// Migrate away from old log
const OLD_LOG_FILE = path.join(__dirname, '../config/logs/overseerr.log');
if (fs.existsSync(OLD_LOG_FILE)) {
const file = fs.lstatSync(OLD_LOG_FILE);
if (!file.isSymbolicLink()) {
fs.unlinkSync(OLD_LOG_FILE);
}
}
const hformat = winston.format.printf(
({ level, label, message, timestamp, ...metadata }) => {
@@ -16,15 +28,29 @@ const hformat = winston.format.printf(
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'debug',
format: winston.format.combine(
winston.format.colorize(),
winston.format.splat(),
winston.format.timestamp(),
hformat
),
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: path.join(__dirname, '../config/logs/overseerr.log'),
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.splat(),
winston.format.timestamp(),
hformat
),
}),
new winston.transports.DailyRotateFile({
filename: process.env.CONFIG_DIRECTORY
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr-%DATE%.log`
: path.join(__dirname, '../config/logs/overseerr-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '7d',
createSymlink: true,
symlinkName: 'overseerr.log',
}),
],
});

View File

@@ -1,9 +1,25 @@
import { getRepository } from 'typeorm';
import { User } from '../entity/User';
import { Permission } from '../lib/permissions';
import { getSettings } from '../lib/settings';
export const checkUser: Middleware = async (req, _res, next) => {
if (req.session?.userId) {
const settings = getSettings();
if (req.header('X-API-Key') === settings.main.apiKey) {
const userRepository = getRepository(User);
let userId = 1; // Work on original administrator account
// If a User ID is provided, we will act on that users behalf
if (req.header('X-API-User')) {
userId = Number(req.header('X-API-User'));
}
const user = await userRepository.findOne({ where: { id: userId } });
if (user) {
req.user = user;
}
} else if (req.session?.userId) {
const userRepository = getRepository(User);
const user = await userRepository.findOne({
@@ -24,7 +40,7 @@ export const isAuthenticated = (
if (!req.user || !req.user.hasPermission(permissions ?? 0)) {
res.status(403).json({
status: 403,
error: 'You do not have permisson to access this endpoint',
error: 'You do not have permission to access this endpoint',
});
} else {
next();

View File

@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner, TableUnique } from 'typeorm';
export class DropImdbIdConstraint1607928251245 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropUniqueConstraint(
'media',
'UQ_7ff2d11f6a83cb52386eaebe74b'
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createUniqueConstraint(
'media',
new TableUnique({
name: 'UQ_7ff2d11f6a83cb52386eaebe74b',
columnNames: ['imdbId'],
})
);
}
}

View File

@@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserRequestDeleteCascades1608219049304
implements MigrationInterface {
name = 'AddUserRequestDeleteCascades1608219049304';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "media_request"`
);
await queryRunner.query(`DROP TABLE "media_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
);
await queryRunner.query(
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
}
}

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddLastSeasonChangeMedia1608477467935
implements MigrationInterface {
name = 'AddLastSeasonChangeMedia1608477467935';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_7157aad07c73f6a6ae3bbd5ef5e" UNIQUE ("tmdbId"), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_7157aad07c73f6a6ae3bbd5ef5e" UNIQUE ("tmdbId"), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
}
}

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class ForceDropImdbUniqueConstraint1608477467935
implements MigrationInterface {
name = 'ForceDropImdbUniqueConstraint1608477467936';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_7157aad07c73f6a6ae3bbd5ef5e" UNIQUE ("tmdbId"), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_7157aad07c73f6a6ae3bbd5ef5e" UNIQUE ("tmdbId"), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
}
}

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveTmdbIdUniqueConstraint1609236552057
implements MigrationInterface {
name = 'RemoveTmdbIdUniqueConstraint1609236552057';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"), CONSTRAINT "UQ_7157aad07c73f6a6ae3bbd5ef5e" UNIQUE ("tmdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
}
}

View File

@@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class LocalUsers1610070934506 implements MigrationInterface {
name = 'LocalUsers1610070934506';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer NOT NULL, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt" FROM "user"`
);
await queryRunner.query(`DROP TABLE "user"`);
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
await queryRunner.query(
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType" FROM "user"`
);
await queryRunner.query(`DROP TABLE "user"`);
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
await queryRunner.query(
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer NOT NULL, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType" FROM "temporary_user"`
);
await queryRunner.query(`DROP TABLE "temporary_user"`);
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
await queryRunner.query(
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer NOT NULL, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt" FROM "temporary_user"`
);
await queryRunner.query(`DROP TABLE "temporary_user"`);
}
}

View File

@@ -0,0 +1,91 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Add4kStatusFields1610370640747 implements MigrationInterface {
name = 'Add4kStatusFields1610370640747';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer, "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_season"("id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId" FROM "season"`
);
await queryRunner.query(`DROP TABLE "season"`);
await queryRunner.query(
`ALTER TABLE "temporary_season" RENAME TO "season"`
);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "media_request"`
);
await queryRunner.query(`DROP TABLE "media_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
);
await queryRunner.query(
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`ALTER TABLE "season" RENAME TO "temporary_season"`
);
await queryRunner.query(
`CREATE TABLE "season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer, CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "season"("id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId" FROM "temporary_season"`
);
await queryRunner.query(`DROP TABLE "temporary_season"`);
}
}

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddMediaAddedFieldToMedia1610522845513
implements MigrationInterface {
name = 'AddMediaAddedFieldToMedia1610522845513';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
}
}

View File

@@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddDisplayNameToUser1611508672722 implements MigrationInterface {
name = 'AddDisplayNameToUser1611508672722';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "username" FROM "user"`
);
await queryRunner.query(`DROP TABLE "user"`);
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
await queryRunner.query(
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername") SELECT "id", "email", "", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername" FROM "user"`
);
await queryRunner.query(`DROP TABLE "user"`);
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
await queryRunner.query(
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername" FROM "temporary_user"`
);
await queryRunner.query(`DROP TABLE "temporary_user"`);
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
await queryRunner.query(
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar NOT NULL, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType" FROM "temporary_user"`
);
await queryRunner.query(`DROP TABLE "temporary_user"`);
}
}

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class SonarrRadarrSyncServiceFields1611757511674
implements MigrationInterface {
name = 'SonarrRadarrSyncServiceFields1611757511674';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
}
}

View File

@@ -0,0 +1,51 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddRatingKeysToMedia1611801511397 implements MigrationInterface {
name = 'AddRatingKeysToMedia1611801511397';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
}
}

View File

@@ -0,0 +1,32 @@
import { TmdbCollection } from '../api/themoviedb';
import { MediaType } from '../constants/media';
import Media from '../entity/Media';
import { mapMovieResult, MovieResult } from './Search';
export interface Collection {
id: number;
name: string;
overview?: string;
posterPath?: string;
backdropPath?: string;
parts: MovieResult[];
}
export const mapCollection = (
collection: TmdbCollection,
media: Media[]
): Collection => ({
id: collection.id,
name: collection.name,
overview: collection.overview,
posterPath: collection.poster_path,
backdropPath: collection.backdrop_path,
parts: collection.parts.map((part) =>
mapMovieResult(
part,
media?.find(
(req) => req.tmdbId === part.id && req.mediaType === MediaType.MOVIE
)
)
),
});

View File

@@ -8,9 +8,26 @@ import {
mapCrew,
ExternalIds,
mapExternalIds,
mapVideos,
} from './common';
import Media from '../entity/Media';
export interface Video {
url?: string;
site: 'YouTube';
key: string;
name: string;
size: number;
type:
| 'Clip'
| 'Teaser'
| 'Trailer'
| 'Featurette'
| 'Opening Credits'
| 'Behind the Scenes'
| 'Bloopers';
}
export interface MovieDetails {
id: number;
imdbId?: string;
@@ -23,6 +40,7 @@ export interface MovieDetails {
originalTitle: string;
overview?: string;
popularity: number;
relatedVideos?: Video[];
posterPath?: string;
productionCompanies: ProductionCompany[];
productionCountries: {
@@ -46,8 +64,15 @@ export interface MovieDetails {
cast: Cast[];
crew: Crew[];
};
collection?: {
id: number;
name: string;
posterPath?: string;
backdropPath?: string;
};
mediaInfo?: Media;
externalIds: ExternalIds;
plexUrl?: string;
}
export const mapMovieDetails = (
@@ -58,6 +83,7 @@ export const mapMovieDetails = (
adult: movie.adult,
budget: movie.budget,
genres: movie.genres,
relatedVideos: mapVideos(movie.videos),
originalLanguage: movie.original_language,
originalTitle: movie.original_title,
popularity: movie.popularity,
@@ -87,6 +113,14 @@ export const mapMovieDetails = (
cast: movie.credits.cast.map(mapCast),
crew: movie.credits.crew.map(mapCrew),
},
collection: movie.belongs_to_collection
? {
id: movie.belongs_to_collection.id,
name: movie.belongs_to_collection.name,
posterPath: movie.belongs_to_collection.poster_path,
backdropPath: movie.belongs_to_collection.backdrop_path,
}
: undefined,
externalIds: mapExternalIds(movie.external_ids),
mediaInfo: media,
});

View File

@@ -3,6 +3,7 @@ import type {
TmdbPersonResult,
TmdbTvResult,
} from '../api/themoviedb';
import { MediaType as MainMediaType } from '../constants/media';
import Media from '../entity/Media';
export type MediaType = 'tv' | 'movie' | 'person';
@@ -78,7 +79,7 @@ export const mapTvResult = (
media?: Media
): TvResult => ({
id: tvResult.id,
firstAirDate: tvResult.first_air_Date,
firstAirDate: tvResult.first_air_date,
genreIds: tvResult.genre_ids,
// Some results from tmdb dont return the mediaType so we force it here!
mediaType: tvResult.media_type || 'tv',
@@ -122,12 +123,18 @@ export const mapSearchResults = (
case 'movie':
return mapMovieResult(
result,
media?.find((req) => req.tmdbId === result.id)
media?.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MainMediaType.MOVIE
)
);
case 'tv':
return mapTvResult(
result,
media?.find((req) => req.tmdbId === result.id)
media?.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MainMediaType.TV
)
);
default:
return mapPersonResult(result);

View File

@@ -7,6 +7,8 @@ import {
mapCrew,
ExternalIds,
mapExternalIds,
Keyword,
mapVideos,
} from './common';
import {
TmdbTvEpisodeResult,
@@ -15,6 +17,7 @@ import {
TmdbSeasonWithEpisodes,
} from '../api/themoviedb';
import type Media from '../entity/Media';
import { Video } from './Movie';
interface Episode {
id: number;
@@ -45,6 +48,12 @@ export interface SeasonWithEpisodes extends Season {
externalIds: ExternalIds;
}
interface SpokenLanguage {
englishName: string;
iso_639_1: string;
name: string;
}
export interface TvDetails {
id: number;
backdropPath?: string;
@@ -56,10 +65,11 @@ export interface TvDetails {
profilePath?: string;
}[];
episodeRunTime: number[];
firstAirDate: string;
firstAirDate?: string;
genres: Genre[];
homepage: string;
inProduction: boolean;
relatedVideos?: Video[];
languages: string[];
lastAirDate: string;
lastEpisodeToAir?: Episode;
@@ -74,6 +84,7 @@ export interface TvDetails {
overview: string;
popularity: number;
productionCompanies: ProductionCompany[];
spokenLanguages: SpokenLanguage[];
seasons: Season[];
status: string;
type: string;
@@ -84,6 +95,7 @@ export interface TvDetails {
crew: Crew[];
};
externalIds: ExternalIds;
keywords: Keyword[];
mediaInfo?: Media;
}
@@ -136,6 +148,7 @@ export const mapTvDetails = (
id: genre.id,
name: genre.name,
})),
relatedVideos: mapVideos(show.videos),
homepage: show.homepage,
id: show.id,
inProduction: show.in_production,
@@ -161,6 +174,11 @@ export const mapTvDetails = (
originCountry: company.origin_country,
logoPath: company.logo_path,
})),
spokenLanguages: show.spoken_languages.map((language) => ({
englishName: language.english_name,
iso_639_1: language.iso_639_1,
name: language.name,
})),
seasons: show.seasons.map(mapSeasonResult),
status: show.status,
type: show.type,
@@ -179,5 +197,9 @@ export const mapTvDetails = (
crew: show.credits.crew.map(mapCrew),
},
externalIds: mapExternalIds(show.external_ids),
keywords: show.keywords.results.map((keyword) => ({
id: keyword.id,
name: keyword.name,
})),
mediaInfo: media,
});

Some files were not shown because too many files have changed in this diff Show More