Compare commits

...

217 Commits

Author SHA1 Message Date
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
semantic-release-bot
f1df9eeb2f chore(release): 1.3.1 2020-12-14 08:03:51 +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
sct
a3973791fb Merge branch 'develop' 2020-12-14 07:59:18 +00:00
sct
cb5c0403c1 docs: update bug template with docker version question [skip ci] 2020-12-14 07:57:33 +00:00
sct
7bf924f7e9 fix(frontend): also convert activeProfileId to a number for radarr/sonarr submissions 2020-12-14 07:17:32 +00:00
sct
c53dc3b15d fix(frontend): also convert ports to numbers when saving radarr/sonarr servers 2020-12-14 07:02:29 +00:00
sct
b96bd33982 refactor(frontend): remove search filter buttons until they actually do something 2020-12-14 06:40:31 +00:00
sct
92c9001c9d fix(frontend): new radarr/sonarr ports will be converted to a number before posting 2020-12-14 06:37:34 +00:00
semantic-release-bot
157ab80be1 chore(release): 1.3.0 2020-12-14 05:32:31 +00:00
sct
ac0f95b773 Merge branch 'develop' 2020-12-14 05:27:01 +00:00
sct
5f09e83ed8 fix(api): correctly generate clientId on first startup 2020-12-14 05:18:22 +00:00
sct
27349ec953 docs: update readme.md 2020-12-14 04:49:00 +00:00
allcontributors[bot]
2ce6e0a94a docs: add angrycuban13 as a contributor (#242) [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 13:44:24 +09:00
Angry Cuban
da65988926 docs: Update README.md (#241) [skip ci] 2020-12-14 13:38:47 +09:00
sct
b3b8e7b0cc docs: update readme [skip ci] 2020-12-14 01:05:11 +00:00
sct
c8f1756a2b Merge pull request #239 from weblate/weblate-overseerr-overseerr-frontend
Translations update from Weblate
2020-12-14 00:28:18 +09:00
Hosted Weblate
f943701e13 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: 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
2020-12-13 16:24:27 +01:00
Hosted Weblate
7bf9addd13 feat(lang): translated using Weblate (German)
Currently translated at 99.3% (304 of 306 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
2020-12-13 16:24:27 +01:00
dependabot[bot]
93983c976b build(deps): bump yup from 0.32.6 to 0.32.8 (#223)
Bumps [yup](https://github.com/jquense/yup) from 0.32.6 to 0.32.8.
- [Release notes](https://github.com/jquense/yup/releases)
- [Changelog](https://github.com/jquense/yup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jquense/yup/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-13 21:24:25 +09:00
dependabot[bot]
05a2fddf4a build(deps): bump ini from 1.3.5 to 1.3.8 (#237)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-13 15:28:43 +09:00
sct
7f2be51a7e refactor(frontend): change person details page to show appears in titles as list view 2020-12-12 12:39:05 +00:00
sct
de01e13dda docs: update all-contributors list 2020-12-12 12:37:15 +00:00
allcontributors[bot]
beca52715a docs: add GiyomuWeb as a contributor (#235)
* 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-12 21:09:03 +09:00
allcontributors[bot]
61ed508a56 docs: add KovalevArtem as a contributor (#234)
* 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-12 21:08:21 +09:00
allcontributors[bot]
ce9d9fdf13 docs: add Ahreluth as a contributor (#233)
* 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-12 21:06:55 +09:00
allcontributors[bot]
e32b585e97 docs: add comradekingu as a contributor (#232)
* 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-12 21:05:29 +09:00
allcontributors[bot]
6d2abfa863 docs: add OwsleyJr as a contributor (#231)
* 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-12 21:04:00 +09:00
allcontributors[bot]
d02724770d docs: add azoitos as a contributor (#230)
* 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-12 21:02:57 +09:00
sct
5fa37b5913 ci: add the ability to skip ci with commit message [skip ci] 2020-12-12 11:59:15 +00:00
sct
5155a4caf2 docs: move all-contributors table into the correct section 2020-12-12 11:57:37 +00:00
sct
42381ed5cb docs: fix all-contributors template/position 2020-12-12 11:55:47 +00:00
sct
b99f9e6888 Merge pull request #229 from sct/all-contributors/add-sct
docs: add sct as a contributor
2020-12-12 20:50:51 +09:00
allcontributors[bot]
e6c5d6ff7c docs: create .all-contributorsrc [skip ci] 2020-12-12 11:19:05 +00:00
allcontributors[bot]
4f9514885b docs: update README.md [skip ci] 2020-12-12 11:19:04 +00:00
sct
52e695bd08 docs: remove contributors image so we can replace it with all-contributors 2020-12-12 10:59:12 +00:00
sct
051f1b3e89 feat(frontend): add full cast page for movies and series 2020-12-12 10:54:53 +00:00
sct
4392a881aa Merge pull request #219 from weblate/weblate-overseerr-overseerr-frontend
Translations update from Weblate
2020-12-12 17:32:24 +09:00
Hosted Weblate
0949c9b334 feat(lang): translated using Weblate (English)
Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (English)

Currently translated at 100.0% (304 of 304 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: sct <sctsnipe@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/en/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
30d04ce35a feat(lang): translated using Weblate (French)
Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (French)

Currently translated at 100.0% (304 of 304 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
2020-12-12 08:14:19 +01:00
Hosted Weblate
83efb0e3d4 feat(lang): translated using Weblate (Norwegian Bokmål)
Currently translated at 87.1% (265 of 304 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nb_NO/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
08e968fd00 feat(lang): translated using Weblate (Japanese)
Currently translated at 100.0% (304 of 304 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 99.0% (301 of 304 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 99.0% (301 of 304 strings)

feat(lang): translated using Weblate (Japanese)

Currently translated at 100.0% (304 of 304 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Shingo Takada <geneofthetiger@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
2020-12-12 08:14:19 +01:00
Hosted Weblate
0d8e0d0352 feat(lang): translated using Weblate (Russian)
Currently translated at 26.9% (82 of 304 strings)

Co-authored-by: Artem <KovalevArtem.ru@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
Translation: Overseerr/Overseerr Frontend
2020-12-12 08:14:19 +01:00
Hosted Weblate
1ab3a4b80a feat(lang): translated using Weblate (Dutch)
Currently translated at 18.7% (57 of 304 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
2020-12-12 08:14:19 +01:00
Hosted Weblate
b6e60a412b feat(lang): translated using Weblate (German)
Currently translated at 99.3% (302 of 304 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.3% (302 of 304 strings)

feat(lang): translated using Weblate (German)

Currently translated at 99.3% (302 of 304 strings)

feat(lang): translated using Weblate (German)

Currently translated at 71.3% (217 of 304 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
2020-12-12 08:14:19 +01:00
sct
d4cc727d52 ci: fix typo in github workflow 2020-12-12 07:13:55 +00:00
semantic-release-bot
45ebfb937e chore(release): 1.2.0 2020-12-11 22:55:12 +00:00
sct
85325cda7f Merge branch 'develop' 2020-12-11 22:50:33 +00:00
sct
3fa7ff9858 fix(frontend): properly remove site overlay when closing modals 2020-12-11 12:17:00 +00:00
sct
b16fbafa1f fix(frontend): switch to using Transition component for modals
This fixes the animation issue when opening Request modals

fixes #220
2020-12-11 11:48:02 +00:00
sct
ccb98553f1 fix(frontend): person cards now show correctly in ListView's 2020-12-11 10:36:39 +00:00
sct
d8060afe02 fix: fix missing personid in Discover 2020-12-09 11:39:26 +00:00
sct
6502feb1a5 fix: missing personId in ListView component 2020-12-09 11:20:57 +00:00
sct
8cd067b6e9 feat(lang): update language files 2020-12-09 11:02:03 +00:00
sct
d6eb3ae64e feat: person details page 2020-12-09 10:58:05 +00:00
sct
02d6b46448 docs: update github issue template config
adds github discussions to the options
2020-12-09 09:23:33 +00:00
sct
df94db050b feat(lang): add support for dutch language 2020-12-09 07:18:22 +00:00
sct
8d8e750982 feat(lang): add support for russian languge 2020-12-09 04:51:48 +00:00
sct
6c7f1cad8b refactor: merge weblate langauge files into develop to fix conflicts 2020-12-09 04:40:47 +00:00
sct
74c36ac31d build(deps): update React to v17.0.1 2020-12-09 04:36:01 +00:00
sct
385fb23b72 build(deps): bundled dependency update 2020-12-09 04:33:50 +00:00
sct
adc07a9624 chore(deps-dev): bundled dependency updates 2020-12-09 04:31:20 +00:00
sct
335377e250 chore(deps-dev): upgrade prettier to v2.2.1 2020-12-09 04:26:06 +00:00
dependabot[bot]
383c79c19a build(deps): bump @svgr/webpack from 5.4.0 to 5.5.0 (#214)
Bumps [@svgr/webpack](https://github.com/gregberge/svgr) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/gregberge/svgr/releases)
- [Changelog](https://github.com/gregberge/svgr/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gregberge/svgr/compare/v5.4.0...v5.5.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-09 13:19:41 +09:00
J. Lavoie
44dbb745b6 feat(lang): translated using Weblate (German)
Currently translated at 44.6% (134 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/
2020-12-09 04:34:14 +01:00
J. Lavoie
30a8934626 feat(lang): translated using Weblate (French)
Currently translated at 100.0% (300 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/
2020-12-09 04:34:14 +01:00
Alex Zoitos
2d0afb29d3 fix(frontend): update overflow issues with seasons + email (#217)
* fix(frontend): update overflow issues with seasons + email. Add all message for full seasons

bug #216

* fix: update i18n json files
2020-12-09 12:34:09 +09:00
Artem
86cadb8283 feat(lang): translated using Weblate (Russian)
Currently translated at 27.0% (81 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ru/
2020-12-09 04:19:13 +01:00
T'ai
a494507dfe feat(lang): translated using Weblate (Japanese)
Currently translated at 100.0% (300 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/
2020-12-09 04:19:13 +01:00
Artem
887f5dd487 feat(lang): added translation using Weblate (Russian) 2020-12-09 04:19:13 +01:00
sct
8da357aea0 docs: update contributing.md 2020-12-09 03:17:51 +00:00
sct
a304742828 docs: pull request template/lock and stale bot config/support bot config 2020-12-09 03:00:48 +00:00
sct
a14af7c21c docs: update readme and add code of conduct/contribution guide 2020-12-09 02:39:42 +00:00
sct
ff5106bf42 docs(update readme): updates discord links in the readme with a valid invite code 2020-12-08 15:05:40 +00:00
sct
42788adb75 fix(lang): fix missing i18n string for agent enabled in email notification page 2020-12-08 14:38:47 +00:00
sct
5774445dd7 docs: update issue templates 2020-12-08 23:36:14 +09:00
sct
dc7d1aabb8 docs: add config.yml for issue templates 2020-12-08 23:35:10 +09:00
sct
b22eb5a49a docs: update issue templates 2020-12-08 23:31:37 +09:00
sct
d38b28d206 feat(lang): add nb-NO and de language support to app 2020-12-08 14:11:05 +00:00
Weblate (bot)
b0c663bacc feat(lang): translations update from Weblate (#201)
* Translated using Weblate (Japanese)

Currently translated at 18.0% (54 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

* Translated using Weblate (Japanese)

Currently translated at 18.3% (55 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

* Translated using Weblate (Japanese)

Currently translated at 19.0% (57 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

* chore: Translated using Weblate (Japanese)

Currently translated at 25.0% (75 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

* Added translation using Weblate (German)

* Added translation using Weblate (Norwegian Bokmål)

* chore: Translated using Weblate (French)

Currently translated at 100.0% (300 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/

* chore: Translated using Weblate (German)

Currently translated at 23.6% (71 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/

* chore: Translated using Weblate (French)

Currently translated at 100.0% (300 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/

* revert(lang): deleted translation using Weblate (Norwegian Bokmål)

* feat(lang): added translation using Weblate (Norwegian Bokmål)

* feat(lang): translated using Weblate (Norwegian Bokmål)

Currently translated at 75.6% (227 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nb_NO/

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

Currently translated at 100.0% (300 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/en/

* feat(lang): translated using Weblate (Norwegian Bokmål)

Currently translated at 87.6% (263 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/nb_NO/

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

Currently translated at 26.0% (78 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

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

Currently translated at 35.6% (107 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

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

Currently translated at 35.6% (107 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

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

Currently translated at 37.0% (111 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

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

Currently translated at 100.0% (300 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/fr/

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

Currently translated at 89.3% (268 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

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

Currently translated at 31.0% (93 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/de/

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

Currently translated at 100.0% (300 of 300 strings)

Translation: Overseerr/Overseerr Frontend
Translate-URL: https://hosted.weblate.org/projects/overseerr/overseerr-frontend/ja/

Co-authored-by: T'ai <chivalrousjosh@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: NGVICIOUS <giyomu1@gmail.com>
Co-authored-by: sct <sctsnipe@gmail.com>
2020-12-08 22:52:07 +09:00
sct
5ff8ae32ec docs: Update README 2020-12-08 22:29:23 +09:00
dependabot[bot]
29e932252f build(deps-dev): bump babel-plugin-react-intl from 8.2.2 to 8.2.21 (#193)
Bumps [babel-plugin-react-intl](https://github.com/formatjs/formatjs) from 8.2.2 to 8.2.21.
- [Release notes](https://github.com/formatjs/formatjs/releases)
- [Commits](https://github.com/formatjs/formatjs/compare/babel-plugin-react-intl@8.2.2...babel-plugin-react-intl@8.2.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-08 22:19:18 +09:00
sct
a0e594a292 refactor(frontend): force movie/series tag on titlecard to be uppercase 2020-12-08 10:34:50 +00:00
sct
96f3861c90 chore(deps): update express-openapi-validator to v4.8.0 2020-12-08 10:28:38 +00:00
Allan Nordhøy
77f3f99fbb docs: README reworked
* README reworked

* Requested change made

Co-authored-by: sct <sct@users.noreply.github.com>

Co-authored-by: sct <sct@users.noreply.github.com>
2020-12-08 19:28:20 +09:00
sct
e2b99b68e3 chore(deps): update typeorm to 0.2.29 2020-12-08 09:48:52 +00:00
sct
86addbc4dd chore(deps-dev): upgrade typescript to v4.1.2 and ts-node to v9.1.1 2020-12-08 09:28:53 +00:00
semantic-release-bot
5fc02d0d94 chore(release): 1.1.0 2020-12-08 08:21:58 +00:00
sct
16902f85fb Merge branch 'develop' 2020-12-08 08:16:24 +00:00
sct
a8393707fe feat: generate real api key
This also hides the api key from users without the ADMIN permission. It will not be returned from
the api for them. Regenerate functionality is not in the commit.
2020-12-08 05:04:15 +00:00
sct
20769d58dd docs: update readme with lgtm shield 2020-12-08 04:43:36 +00:00
sct
8e6daf7bd2 fix: fix a few misc unused imports and useless assignments/conditionals 2020-12-08 04:36:43 +00:00
sct
5205ece022 Merge branch 'develop' of github.com:sct/overseerr into develop 2020-12-08 04:15:24 +00:00
sct
79a352b7b3 build(deps): update dependencies and update relevant files 2020-12-08 04:14:18 +00:00
dependabot[bot]
3d3e05ec60 build(deps): bump uuid from 8.3.0 to 8.3.1 (#114)
Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v8.3.0...v8.3.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-08 11:34:01 +09:00
sct
2d51efd716 feat(frontend): only load request/tmdb cards when in the browser view 2020-12-08 02:11:14 +00:00
sct
ee1a9a506a docs: further readme updates 2020-12-07 15:53:55 +00:00
Brandon Cohen
3ab2e79ce8 docs: added to the readme (#202) 2020-12-08 00:35:50 +09:00
sct
5ad01ab14f chore: add license file 2020-12-07 15:00:58 +00:00
sct
b4002d7132 fix(frontend): push updated i18n locale files 2020-12-07 11:54:20 +00:00
sct
83fde46a59 fix(frontend): dont show delete button in request list for users without correct permission 2020-12-07 11:45:23 +00:00
sct
6c4022fb23 feat(frontend): add i18n strings for request list and request item 2020-12-07 11:44:08 +00:00
sct
1ba027b435 feat(frontend): initial version of the requests page (no filtering/sorting) 2020-12-07 10:29:25 +00:00
159 changed files with 10211 additions and 3081 deletions

201
.all-contributorsrc Normal file
View File

@@ -0,0 +1,201 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "sct",
"name": "sct",
"avatar_url": "https://avatars1.githubusercontent.com/u/234213?v=4",
"profile": "https://sct.dev",
"contributions": [
"code",
"design",
"ideas"
]
},
{
"login": "azoitos",
"name": "Alex Zoitos",
"avatar_url": "https://avatars2.githubusercontent.com/u/26529049?v=4",
"profile": "https://github.com/azoitos",
"contributions": [
"code"
]
},
{
"login": "OwsleyJr",
"name": "Brandon Cohen",
"avatar_url": "https://avatars3.githubusercontent.com/u/8635678?v=4",
"profile": "https://github.com/OwsleyJr",
"contributions": [
"code",
"doc"
]
},
{
"login": "Ahreluth",
"name": "Ahreluth",
"avatar_url": "https://avatars2.githubusercontent.com/u/75682440?v=4",
"profile": "https://github.com/Ahreluth",
"contributions": [
"translation"
]
},
{
"login": "KovalevArtem",
"name": "KovalevArtem",
"avatar_url": "https://avatars0.githubusercontent.com/u/36500228?v=4",
"profile": "https://github.com/KovalevArtem",
"contributions": [
"translation"
]
},
{
"login": "GiyomuWeb",
"name": "GiyomuWeb",
"avatar_url": "https://avatars0.githubusercontent.com/u/62489209?v=4",
"profile": "https://github.com/GiyomuWeb",
"contributions": [
"translation"
]
},
{
"login": "angrycuban13",
"name": "Angry Cuban",
"avatar_url": "https://avatars3.githubusercontent.com/u/39564898?v=4",
"profile": "https://github.com/angrycuban13",
"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"
]
}
],
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
"contributorsPerLine": 7,
"projectName": "overseerr",
"projectOwner": "sct",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
}

View File

@@ -1,8 +1,8 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
@@ -30,6 +30,8 @@ module.exports = {
'@typescript-eslint/explicit-function-return-type': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto' }],
'formatjs/no-offset': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
},
overrides: [
{
@@ -52,6 +54,5 @@ module.exports = {
jest: true,
es6: true,
},
reportUnusedDisableDirectives: true,
};

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

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

45
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,45 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**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:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Support via Discord
url: https://discord.gg/PkCWJSeCk7
about: Chat with users and devs on support and setup related topics.
- name: Support via GitHub Discussions
url: https://github.com/sct/overseerr/discussions
about: Ask questions and discuss with other community members

View File

@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

13
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,13 @@
#### Description
#### Screenshot (if UI related)
#### Todos
- [ ] Sucessfully builds `yarn build`
- [ ] Translation Keys `yarn i18n:extract`
- [ ] Database migration created (if required)
#### Issues Fixed or Closed by this PR
- Fixes #XXXX

38
.github/lock.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 365
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels:
- dependencies
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: >
This thread has been automatically locked since there has not been
any recent activity after it was closed. Please open a new issue for
related bugs.
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings just for `issues` or `pulls`
# issues:
# exemptLabels:
# - help-wanted
# lockLabel: outdated
# pulls:
# daysUntilLock: 30
# Repository to extend settings from
# _extends: repo

18
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- dependencies
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@@ -26,15 +26,36 @@ jobs:
build_and_push:
name: Build and push Docker image to Docker Hub
needs: test
if: github.ref == 'refs/heads/develop'
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, 'skip ci')
runs-on: ubuntu-latest
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 }}

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-latest
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

@@ -20,7 +20,7 @@ jobs:
run: yarn lint
- name: build
run: yarn build
semnatic-release:
semantic-release:
name: Tag and release latest version
needs: test
runs-on: ubuntu-latest

25
.github/workflows/support.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: 'Support requests'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v2
with:
github-token: ${{ github.token }}
support-label: 'support'
issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please use our support channels
to get help with Overseerr.
- [Discord](https://discord.gg/PkCWJSeCk7)
- [GitHub Discussions](https://github.com/sct/overseerr/discussions)
close-issue: true
lock-issue: false
issue-lock-reason: 'off-topic'

View File

@@ -6,7 +6,7 @@
"typescript",
"typescriptreact"
],
"typescript.tsdk": "./app/node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"sqltools.connections": [
{
"previewLimit": 50,

View File

@@ -1,3 +1,220 @@
# [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)
### Bug Fixes
* **frontend:** also convert activeProfileId to a number for radarr/sonarr submissions ([7bf924f](https://github.com/sct/overseerr/commit/7bf924f7e94a0e0834f41b4ec067ed277c652766))
* **frontend:** also convert ports to numbers when saving radarr/sonarr servers ([c53dc3b](https://github.com/sct/overseerr/commit/c53dc3b15da522c6e6ab76bbc9d15008a8a9fb9d))
* **frontend:** new radarr/sonarr ports will be converted to a number before posting ([92c9001](https://github.com/sct/overseerr/commit/92c9001c9d1f2cbd272a5897ea1157d2cadbce2d))
# [1.3.0](https://github.com/sct/overseerr/compare/v1.2.0...v1.3.0) (2020-12-14)
### Bug Fixes
* **api:** correctly generate clientId on first startup ([5f09e83](https://github.com/sct/overseerr/commit/5f09e83ed870336638d3e9d94fcf55ead928e737))
### Features
* **frontend:** add full cast page for movies and series ([051f1b3](https://github.com/sct/overseerr/commit/051f1b3e899bf749e632743e5c8d45a02b621998))
* **lang:** translated using Weblate (Dutch) ([1ab3a4b](https://github.com/sct/overseerr/commit/1ab3a4b80a081d7e4a201f1290cd270ed5b38ac7))
* **lang:** translated using Weblate (English) ([0949c9b](https://github.com/sct/overseerr/commit/0949c9b334b3a4b6c342517a157a9e2b7596f2f0))
* **lang:** translated using Weblate (French) ([f943701](https://github.com/sct/overseerr/commit/f943701e13c7f0de5a711302597858cc898b16e2))
* **lang:** translated using Weblate (French) ([30d04ce](https://github.com/sct/overseerr/commit/30d04ce35adc21070cce37ab10384154afda191b))
* **lang:** translated using Weblate (German) ([7bf9add](https://github.com/sct/overseerr/commit/7bf9addd13a707aac23b64ef3f1733e491d40a4e))
* **lang:** translated using Weblate (German) ([b6e60a4](https://github.com/sct/overseerr/commit/b6e60a412b30907aea751a4cf1ce0cc8230f9814))
* **lang:** translated using Weblate (Japanese) ([08e968f](https://github.com/sct/overseerr/commit/08e968fd0097ec7b2a65de064ed5b07e7c49ef39))
* **lang:** translated using Weblate (Norwegian Bokmål) ([83efb0e](https://github.com/sct/overseerr/commit/83efb0e3d4d96b6a2d2ebdd85d36c9d78c1717b2))
* **lang:** translated using Weblate (Russian) ([0d8e0d0](https://github.com/sct/overseerr/commit/0d8e0d0352f72fdb65ee8f054371eae08c39fe33))
# [1.2.0](https://github.com/sct/overseerr/compare/v1.1.0...v1.2.0) (2020-12-11)
### Bug Fixes
* **frontend:** person cards now show correctly in ListView's ([ccb9855](https://github.com/sct/overseerr/commit/ccb98553f104c1aebd33796b7090cc9bbe964bd7))
* **frontend:** properly remove site overlay when closing modals ([3fa7ff9](https://github.com/sct/overseerr/commit/3fa7ff9858d14d132151f3329164d55d74638f53))
* **frontend:** switch to using Transition component for modals ([b16fbaf](https://github.com/sct/overseerr/commit/b16fbafa1f3d5e105c0a4ba6f1d66aa064019636)), closes [#220](https://github.com/sct/overseerr/issues/220)
* fix missing personid in Discover ([d8060af](https://github.com/sct/overseerr/commit/d8060afe02574337f51b88cab0a0f824976ac721))
* missing personId in ListView component ([6502feb](https://github.com/sct/overseerr/commit/6502feb1a5be3c6daab33230814fe74632c87f7e))
* **frontend:** update overflow issues with seasons + email ([#217](https://github.com/sct/overseerr/issues/217)) ([2d0afb2](https://github.com/sct/overseerr/commit/2d0afb29d37798a626e3f182571ccce43d80063c)), closes [#216](https://github.com/sct/overseerr/issues/216)
* **lang:** fix missing i18n string for agent enabled in email notification page ([42788ad](https://github.com/sct/overseerr/commit/42788adb75f7d23e68327688b1c542dd047e9609))
### Features
* **lang:** update language files ([8cd067b](https://github.com/sct/overseerr/commit/8cd067b6e9df1a3c8f4056789436a31177703986))
* person details page ([d6eb3ae](https://github.com/sct/overseerr/commit/d6eb3ae64ef46bd62145010d3029e272676487c3))
* **lang:** add nb-NO and de language support to app ([d38b28d](https://github.com/sct/overseerr/commit/d38b28d2061b38366989ff412957a5dee5766c6f))
* **lang:** add support for dutch language ([df94db0](https://github.com/sct/overseerr/commit/df94db050bf68a925118e0ce865d27178b702f9e))
* **lang:** add support for russian languge ([8d8e750](https://github.com/sct/overseerr/commit/8d8e7509826514eebc859374d2e1ab212cc442d1))
* **lang:** added translation using Weblate (Russian) ([887f5dd](https://github.com/sct/overseerr/commit/887f5dd487b61676029652d99cbc5b40213aa22e))
* **lang:** translated using Weblate (French) ([30a8934](https://github.com/sct/overseerr/commit/30a8934626fa2d47e95b5925d7e4227a0d0aa728))
* **lang:** translated using Weblate (German) ([44dbb74](https://github.com/sct/overseerr/commit/44dbb745b6216ce19fab4740520785c6414cf367))
* **lang:** translated using Weblate (Japanese) ([a494507](https://github.com/sct/overseerr/commit/a494507dfeafb0cfd2bd66fb01138522e0e80737))
* **lang:** translated using Weblate (Russian) ([86cadb8](https://github.com/sct/overseerr/commit/86cadb8283fcab8745b4c09f8429fd9e46708813))
* **lang:** translations update from Weblate ([#201](https://github.com/sct/overseerr/issues/201)) ([b0c663b](https://github.com/sct/overseerr/commit/b0c663baccd994e234b4d41d86486c3af4906344))
# [1.1.0](https://github.com/sct/overseerr/compare/v1.0.0...v1.1.0) (2020-12-08)
### Bug Fixes
* fix a few misc unused imports and useless assignments/conditionals ([8e6daf7](https://github.com/sct/overseerr/commit/8e6daf7bd271ce5bebf4a00f5bb1144bd6b60aa5))
* **frontend:** dont show delete button in request list for users without correct permission ([83fde46](https://github.com/sct/overseerr/commit/83fde46a59c6f1910806a6106b5526b8adbc386c))
* **frontend:** push updated i18n locale files ([b4002d7](https://github.com/sct/overseerr/commit/b4002d71323a04e7991198cedc263660e872df8d))
### Features
* generate real api key ([a839370](https://github.com/sct/overseerr/commit/a8393707fec85a9262af5ba8c03d205190b2235b))
* **frontend:** add i18n strings for request list and request item ([6c4022f](https://github.com/sct/overseerr/commit/6c4022fb236583ad20d4c4c6693c1339e165b4af))
* **frontend:** initial version of the requests page (no filtering/sorting) ([1ba027b](https://github.com/sct/overseerr/commit/1ba027b4357e078c3f177d9d07208049f0c1ce65))
* **frontend:** only load request/tmdb cards when in the browser view ([2d51efd](https://github.com/sct/overseerr/commit/2d51efd71612ec969b83c62d6aa0dac6df9391a3))
# 1.0.0 (2020-12-06)

132
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,132 @@
# Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[ryan@sct.dev](mailto:ryan@sct.dev).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][mozilla coc].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][faq]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

49
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,49 @@
# Contributing to Overseerr
All help is welcome and greatly appreciated. If you would like to contribute to the project the steps below can get you started:
## Development
### Tools Required
- HTML/Typescript/Javascript editor of choice. ([VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.)
- [NodeJS](https://nodejs.org/en/download/) (Node 12.x.x or higher)
- [Yarn](https://yarnpkg.com/)
- [Git](https://git-scm.com/downloads)
### 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`
- 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`.
3. Install dependencies `yarn`
4. `yarn dev` to build and watch for changes
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.
### Contributing Code
- If you are taking on an existing bug or feature ticket, please comment on the [GitHub Issue](https://github.com/sct/overseerr/issues) to avoid multiple people working on the same thing.
- All commits **must** follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
- It is okay if you squash your PR down to be a single commit that fits this standard.
- 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 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.
- If you have questions or need help, you can reach out in [GitHub Discussions](https://github.com/sct/overseerr/discussions) or in our [Discord](https://discord.gg/PkCWJSeCk7).
- Only open pull requests to `develop`. Never `master`. Any PR's opened to master will be closed.
## Translation
We use [Weblate](https://hosted.weblate.org/engage/overseerr/) for our translations so please feel free to contribute to localizing Overseerr!
## Attribution
This contribution guide was inspired by the [Next.js](https://github.com/vercel/next.js) and [Radarr](https://github.com/Radarr/Radarr) contribution guides.

View File

@@ -12,6 +12,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
@@ -21,3 +26,5 @@ COPY --from=BUILD_IMAGE /app/.next ./.next
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
CMD yarn start
EXPOSE 5055

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 sct
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

122
README.md
View File

@@ -2,34 +2,130 @@
<img src="https://i.imgur.com/TMoEG7g.png" alt="Overseerr">
</p>
<p align="center">
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20Release/badge.svg?branch=master" alt="Overseerr Release" />
<img src="https://github.com/sct/overseerr/workflows/Overseerr%20CI/badge.svg" alt="Overseerr CI">
</p>
<p align="center">
<a href="https://discord.gg/PkCWJSeCk7">
<img src="https://img.shields.io/discord/783137440809746482" alt="Discord">
</a>
<img src="https://img.shields.io/docker/pulls/sctx/overseerr" alt="Docker pulls">
<a href="https://hosted.weblate.org/engage/overseerr/">
<img src="https://hosted.weblate.org/widgets/overseerr/-/overseerr-frontend/svg-badge.svg" alt="Translation status" />
</a>
<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-20-orange.svg"/></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
</p>
**Overseerr** is a tool for managing requests for your media library. It integrates with existing services such as **Sonarr** and **Radarr**!
**Overseerr** is a free and open source software application for managing requests for your media library. It integrates with your existing services such as **Sonarr**, **Radarr** and **Plex**!
## Current Features
- Full Plex integration. Login and manage user access with Plex!
- Integrates easily with your existing services. Currently Overseerr supports Sonarr and Radarr. More in the future!
- Syncs to your Plex library to know what titles you already have
- Complex request system that allows users to request individual seasons or movies in a friendly, easy to use UI
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests
- Integrates easily with your existing services. Currently Overseerr supports Sonarr and Radarr. More to come!
- Syncs to your Plex library to know what titles you already have.
- Complex request system allowing users to request individual seasons or movies in a friendly, easy to use UI.
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests.
- Granular permission system
- Mobile friendly design, for when you need to approve requests on the go!
## In Development
- A lot!
- 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)
## Planned Features
- More notification types (Slack/Telegram/etc.).
- 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).
- Compatibility APIs (to work with existing tools in your system).
## Running Overseerr
Currently, Overseerr is only distributed through Docker images. If you have Docker, you can run Overseerr as per:
```
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
```
After running Overseerr for the first time, configure it by visiting the web UI at http://[address]:5055 and completing the setup steps.
⚠️ 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! ⚠️
## Preview
<img src="https://i.imgur.com/Mjbyruv.png">
## Support
- 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
- Coming soon
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
## Contribution
## Community
You can develop Overseer entirely in docker. Make sure you have [Docker Desktop](https://www.docker.com/products/docker-desktop) installed before continuing.
You can ask questions, share ideas, and more in [GitHub Discussions](https://github.com/sct/overseerr/discussions).
1. Make sure you have [Docker Desktop](https://www.docker.com/products/docker-desktop) installed.
2. Run `docker-compose up -d` to start the server
3. Access the container at http://localhost:3000
If you would like to chat with community members you can join the [Overseerr Discord](https://discord.gg/PkCWJSeCk7).
That's it!
Our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md) applies to all Overseerr community channels.
## 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>
</tr>
<tr>
<td align="center"><a href="https://github.com/jvennik"><img src="https://avatars3.githubusercontent.com/u/6672637?v=4" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" width="100px;" alt=""/><br /><sub><b>sebstrgg</b></sub></a><br /><a href="#translation-sebstrgg" title="Translation">🌍</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->

View File

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

View File

@@ -5,6 +5,7 @@ const devConfig = {
logging: false,
entities: ['server/entity/**/*.ts'],
migrations: ['server/migration/**/*.ts'],
subscribers: ['server/subscriber/**/*.ts'],
cli: {
entitiesDir: 'server/entity',
migrationsDir: 'server/migration',
@@ -19,6 +20,7 @@ const prodConfig = {
entities: ['dist/entity/**/*.js'],
migrations: ['dist/migration/**/*.js'],
migrationsRun: true,
subscribers: ['dist/subscriber/**/*.js'],
cli: {
entitiesDir: 'dist/entity',
migrationsDir: 'dist/migration',

View File

@@ -375,6 +375,7 @@ components:
example: 1
logoPath:
type: string
nullable: true
originCountry:
type: string
name:
@@ -428,18 +429,14 @@ components:
releaseDate:
type: string
revenue:
type: string
type: number
nullable: true
runtime:
type: number
spokenLanguages:
type: array
items:
type: object
properties:
iso_639_1:
type: string
name:
type: string
$ref: '#/components/schemas/SpokenLanguage'
status:
type: string
tagline:
@@ -476,6 +473,7 @@ components:
type: string
airDate:
type: string
nullable: true
episodeNumber:
type: number
overview:
@@ -483,11 +481,12 @@ components:
productionCode:
type: string
seasonNumber:
type: string
type: number
showId:
type: number
stillPath:
type: string
nullable: true
voteAverage:
type: number
voteCount:
@@ -499,6 +498,7 @@ components:
type: number
airDate:
type: string
nullable: true
episodeCount:
type: number
name:
@@ -536,6 +536,7 @@ components:
type: number
profilePath:
type: string
nullable: true
episodeRunTime:
type: array
items:
@@ -586,6 +587,10 @@ components:
type: array
items:
$ref: '#/components/schemas/ProductionCompany'
spokenLanguages:
type: array
items:
$ref: '#/components/schemas/SpokenLanguage'
seasons:
type: array
items:
@@ -611,6 +616,10 @@ components:
$ref: '#/components/schemas/Crew'
externalIds:
$ref: '#/components/schemas/ExternalIds'
keywords:
type: array
items:
$ref: '#/components/schemas/Keyword'
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
MediaRequest:
@@ -637,7 +646,6 @@ components:
readOnly: true
requestedBy:
$ref: '#/components/schemas/User'
readOnly: true
modifiedBy:
anyOf:
- $ref: '#/components/schemas/User'
@@ -658,6 +666,7 @@ components:
tvdbId:
type: number
readOnly: true
nullable: true
status:
type: number
requests:
@@ -696,6 +705,7 @@ components:
type: number
profilePath:
type: string
nullable: true
Crew:
type: object
properties:
@@ -715,25 +725,34 @@ components:
type: string
profilePath:
type: string
nullable: true
ExternalIds:
type: object
properties:
facebookId:
type: string
nullable: true
freebaseId:
type: string
nullable: true
freebaseMid:
type: string
nullable: true
imdbId:
type: string
nullable: true
instagramId:
type: string
nullable: true
tvdbId:
type: string
type: number
nullable: true
tvrageId:
type: string
type: number
nullable: true
twitterId:
type: string
nullable: true
ServiceProfile:
type: object
properties:
@@ -887,6 +906,8 @@ components:
type: string
character:
type: string
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
CreditCrew:
type: object
properties:
@@ -941,17 +962,45 @@ components:
type: string
job:
type: string
mediaInfo:
$ref: '#/components/schemas/MediaInfo'
Keyword:
type: object
properties:
id:
type: number
example: 1
name:
type: string
example: 'anime'
SpokenLanguage:
type: object
properties:
englishName:
type: string
example: 'English'
nullable: true
iso_639_1:
type: string
example: 'en'
name:
type: string
example: 'English'
securitySchemes:
cookieAuth:
type: apiKey
name: connect.sid
in: cookie
apiKey:
type: apiKey
in: header
name: X-Api-Key
paths:
/settings/main:
get:
summary: Returns main settings
description: Retreives all main settings in JSON format
description: Retrieves all main settings in JSON format
tags:
- settings
responses:
@@ -979,6 +1028,19 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/MainSettings'
/settings/main/regenerate:
get:
summary: Returns main settings with newly generated API Key
description: Retreives all main settings in JSON format with new API Key
tags:
- settings
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/MainSettings'
/settings/plex:
get:
summary: Returns plex settings
@@ -1116,7 +1178,7 @@ paths:
/settings/radarr/test:
post:
summary: Test radarr configuration
description: Test if the provided Radarr congifuration values are valid. Returns profiles and root folders on success
description: Test if the provided Radarr configuration values are valid. Returns profiles and root folders on success
tags:
- settings
requestBody:
@@ -1260,7 +1322,7 @@ paths:
/settings/sonarr/test:
post:
summary: Test Sonarr configuration
description: Test if the provided Sonarr congifuration values are valid. Returns profiles and root folders on success
description: Test if the provided Sonarr configuration values are valid. Returns profiles and root folders on success
tags:
- settings
requestBody:
@@ -1363,7 +1425,7 @@ paths:
/settings/initialize:
get:
summary: Set the application as initialized
description: Sets the app as initalized and allows the user to navigate to pages other than the setup page
description: Sets the app as initialized and allows the user to navigate to pages other than the setup page
tags:
- settings
responses:
@@ -1457,6 +1519,33 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DiscordSettings'
/settings/about:
get:
summary: Return current about stats
description: Returns current server stats in JSON format
tags:
- settings
responses:
'200':
description: Returned about settings
content:
application/json:
schema:
type: object
properties:
version:
type: string
example: '1.0.0'
totalRequests:
type: number
example: 100
totalMediaItems:
type: number
example: 100
tz:
type: string
nullable: true
example: Asia/Tokyo
/auth/me:
get:
summary: Returns the currently logged in user
@@ -1823,6 +1912,51 @@ paths:
- $ref: '#/components/schemas/MovieResult'
- $ref: '#/components/schemas/TvResult'
- $ref: '#/components/schemas/PersonResult'
/discover/keyword/{keywordId}/movies:
get:
summary: Request list of movies from keyword
description: Returns list of movies based on provided keyword ID in JSON format
tags:
- search
parameters:
- in: path
name: keywordId
required: true
schema:
type: number
example: 207317
- in: query
name: page
schema:
type: number
example: 1
default: 1
- in: query
name: language
schema:
type: string
example: en
responses:
'200':
description: List of movies
content:
application/json:
schema:
type: object
properties:
page:
type: number
example: 1
totalPages:
type: number
example: 20
totalResults:
type: number
example: 200
results:
type: array
items:
$ref: '#/components/schemas/MovieResult'
/request:
get:
summary: Get all requests
@@ -2374,7 +2508,7 @@ paths:
crew:
type: array
items:
$ref: "#/components/schemas/CreditCrew"
$ref: '#/components/schemas/CreditCrew'
id:
type: number
/media:
@@ -2442,3 +2576,4 @@ paths:
security:
- cookieAuth: []
- apiKey: []

View File

@@ -1,6 +1,6 @@
{
"name": "overseerr",
"version": "1.0.0",
"version": "1.11.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",
@@ -9,51 +9,55 @@
"build": "yarn build:next && yarn build:server",
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\"",
"start": "NODE_ENV=production node dist/index.js",
"i18n:extract": "extract-messages -l=en,ja,fr -o src/i18n/locale -d en --flat true './src/**/!(*.test).{ts,tsx}'",
"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": {
"@svgr/webpack": "^5.4.0",
"axios": "^0.20.0",
"@svgr/webpack": "^5.5.0",
"axios": "^0.21.0",
"body-parser": "^1.19.0",
"bowser": "^2.11.0",
"connect-typeorm": "^1.1.4",
"cookie-parser": "^1.4.5",
"email-templates": "^8.0.0",
"email-templates": "^8.0.2",
"express": "^4.17.1",
"express-openapi-validator": "^3.16.15",
"express-openapi-validator": "^4.8.0",
"express-session": "^1.17.1",
"formik": "^2.2.1",
"formik": "^2.2.5",
"intl": "^1.2.5",
"lodash": "^4.17.20",
"next": "^10.0.3",
"node-schedule": "^1.3.2",
"nodemailer": "^6.4.16",
"nookies": "^2.4.0",
"nookies": "^2.5.0",
"plex-api": "^5.3.1",
"pug": "^3.0.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-intl": "^5.8.5",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-intersection-observer": "^8.31.0",
"react-intl": "^5.10.6",
"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.1",
"react-use-clipboard": "1.0.2",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.0.0",
"swagger-ui-express": "^4.1.4",
"swr": "^0.3.5",
"typeorm": "^0.2.26",
"uuid": "^8.3.0",
"swagger-ui-express": "^4.1.5",
"swr": "^0.3.9",
"typeorm": "^0.2.29",
"uuid": "^8.3.2",
"winston": "^3.3.3",
"xml2js": "^0.4.23",
"yamljs": "^0.3.0",
"yup": "^0.29.3"
"yup": "^0.32.8"
},
"devDependencies": {
"@babel/cli": "^7.11.6",
"@babel/cli": "^7.12.8",
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@semantic-release/changelog": "^5.0.1",
@@ -66,48 +70,48 @@
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/email-templates": "^7.1.0",
"@types/express": "^4.17.8",
"@types/express": "^4.17.9",
"@types/express-session": "^1.17.0",
"@types/lodash": "^4.14.161",
"@types/node": "^14.10.0",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.11",
"@types/node-schedule": "^1.3.1",
"@types/nodemailer": "^6.4.0",
"@types/react": "^16.9.49",
"@types/react-dom": "^16.9.8",
"@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/swagger-ui-express": "^4.1.2",
"@types/uuid": "^8.3.0",
"@types/xml2js": "^0.4.5",
"@types/xml2js": "^0.4.7",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.9",
"@typescript-eslint/eslint-plugin": "^4.0.0",
"@typescript-eslint/parser": "^3.10.1",
"@types/yup": "^0.29.10",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.9.1",
"autoprefixer": "^9",
"babel-plugin-react-intl": "^8.2.2",
"babel-plugin-react-intl": "^8.2.21",
"babel-plugin-react-intl-auto": "^3.3.0",
"commitizen": "^4.2.1",
"copyfiles": "^2.4.0",
"commitizen": "^4.2.2",
"copyfiles": "^2.4.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.10.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-formatjs": "^2.7.10",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.2",
"eslint": "^7.15.0",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-formatjs": "^2.9.10",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.2.0",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"extract-react-intl-messages": "^4.1.1",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"nodemon": "^2.0.4",
"husky": "^4.3.5",
"lint-staged": "^10.5.3",
"nodemon": "^2.0.6",
"postcss": "^7",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2",
"prettier": "^2.2.1",
"semantic-release": "^17.3.0",
"semantic-release-docker": "^2.2.0",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"ts-node": "^9.0.0",
"typescript": "^4.0.2"
"ts-node": "^9.1.1",
"typescript": "^4.1.2"
},
"config": {
"commitizen": {
@@ -168,7 +172,13 @@
"prepareCmd": "docker build -t sctx/overseerr ."
}
],
"semantic-release-docker"
"semantic-release-docker",
[
"@semantic-release/github",
{
"addReleases": "bottom"
}
]
],
"branches": [
"master"
@@ -178,7 +188,8 @@
{
"path": "semantic-release-docker",
"name": "sctx/overseerr"
}
},
"@semantic-release/github"
]
}
}

BIN
public/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@@ -1 +1 @@
{"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":"","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":"#1e2937","display":"standalone"}

View File

@@ -4,10 +4,12 @@ import { getSettings } from '../lib/settings';
export interface PlexLibraryItem {
ratingKey: string;
parentRatingKey?: string;
grandparentRatingKey?: string;
title: string;
guid: string;
parentGuid?: string;
type: 'movie' | 'show' | 'season';
grandparentGuid?: string;
type: 'movie' | 'show' | 'season' | 'episode';
}
interface PlexLibraryResponse {
@@ -20,6 +22,7 @@ export interface PlexLibrary {
type: 'show' | 'movie';
key: string;
title: string;
agent: string;
}
interface PlexLibrariesResponse {
@@ -61,7 +64,8 @@ class PlexAPI {
this.plexClient = new NodePlexAPI({
hostname: settings.plex.ip,
post: settings.plex.port,
port: settings.plex.port,
https: settings.plex.useSsl,
token: plexToken,
authenticator: {
authenticate: (
@@ -119,9 +123,9 @@ class PlexAPI {
return response.MediaContainer.Metadata[0];
}
public async getRecentlyAdded(): Promise<PlexLibraryItem[]> {
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,4 +1,5 @@
import Axios, { AxiosInstance } from 'axios';
import logger from '../logger';
interface RadarrMovieOptions {
title: string;
@@ -75,11 +76,9 @@ class RadarrAPI {
}
};
public addMovie = async (
options: RadarrMovieOptions
): Promise<RadarrMovie> => {
public addMovie = async (options: RadarrMovieOptions): Promise<void> => {
try {
const response = await this.axios.post<RadarrMovie>(`/movie`, {
await this.axios.post<RadarrMovie>(`/movie`, {
title: options.title,
qualityProfileId: options.qualityProfileId,
profileId: options.profileId,
@@ -93,10 +92,15 @@ class RadarrAPI {
searchForMovie: options.searchNow,
},
});
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,
}
);
}
};

View File

@@ -116,7 +116,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 +126,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

@@ -6,7 +6,7 @@ interface SonarrSeason {
monitored: boolean;
}
interface SonarrSeries {
export interface SonarrSeries {
title: string;
sortTitle: string;
seasonCount: number;
@@ -33,7 +33,7 @@ interface SonarrSeries {
tvMazeId: number;
firstAired: string;
lastInfoSync?: string;
seriesType: string;
seriesType: 'standard' | 'daily' | 'anime';
cleanTitle: string;
imdbId: string;
titleSlug: string;
@@ -76,7 +76,9 @@ interface AddSeriesOptions {
title: string;
profileId: number;
seasons: number[];
seasonFolder: boolean;
rootFolderPath: string;
seriesType: SonarrSeries['seriesType'];
monitored?: boolean;
searchNow?: boolean;
}
@@ -149,8 +151,10 @@ class SonarrAPI {
monitored: false,
}))
),
seasonFolder: options.seasonFolder,
monitored: options.monitored,
rootFolderPath: options.rootFolderPath,
seriesType: options.seriesType,
addOptions: {
ignoreEpisodesWithFiles: true,
searchForMissingEpisodes: options.searchNow,
@@ -162,7 +166,7 @@ class SonarrAPI {
} catch (e) {
logger.error('Something went wrong adding a series to Sonarr', {
label: 'Sonarr API',
message: e.message,
errorMessage: e.message,
error: e,
});
throw new Error('Failed to add series');

View File

@@ -1,5 +1,7 @@
import axios, { AxiosInstance } from 'axios';
export const ANIME_KEYWORD_ID = 210024;
interface SearchOptions {
query: string;
page?: number;
@@ -258,6 +260,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 +275,14 @@ export interface TmdbTvDetails {
crew: TmdbCreditCrew[];
};
external_ids: TmdbExternalIds;
keywords: {
results: TmdbKeyword[];
};
}
export interface TmdbKeyword {
id: number;
name: string;
}
export interface TmdbPersonDetail {
@@ -437,7 +452,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',
},
});
return response.data;
@@ -524,6 +542,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,
@@ -679,9 +723,10 @@ class TheMovieDb {
public getMovieTrending = async ({
page = 1,
timeWindow = 'day',
}: { page?: number; timeWindow?: 'day' | 'week' } = {}): Promise<
TmdbSearchMovieResponse
> => {
}: {
page?: number;
timeWindow?: 'day' | 'week';
} = {}): Promise<TmdbSearchMovieResponse> => {
try {
const response = await this.axios.get<TmdbSearchMovieResponse>(
`/trending/movie/${timeWindow}`,
@@ -701,9 +746,10 @@ class TheMovieDb {
public getTvTrending = async ({
page = 1,
timeWindow = 'day',
}: { page?: number; timeWindow?: 'day' | 'week' } = {}): Promise<
TmdbSearchTvResponse
> => {
}: {
page?: number;
timeWindow?: 'day' | 'week';
} = {}): Promise<TmdbSearchTvResponse> => {
try {
const response = await this.axios.get<TmdbSearchTvResponse>(
`/trending/tv/${timeWindow}`,

View File

@@ -8,14 +8,11 @@ import {
UpdateDateColumn,
getRepository,
In,
AfterUpdate,
} 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';
@Entity()
class Media {
@@ -98,32 +95,6 @@ class Media {
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 },
});
if (relatedRequests.length > 0) {
const tmdb = new TheMovieDb();
const movie = await tmdb.getMovie({ movieId: this.tmdbId });
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}`,
});
});
}
}
}
}
}
export default Media;

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()
@@ -62,7 +70,7 @@ export class MediaRequest {
}
@AfterInsert()
private async notifyNewRequest() {
private async _notifyNewRequest() {
if (this.status === MediaRequestStatus.PENDING) {
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
@@ -110,7 +118,7 @@ export class MediaRequest {
* auto approved content
*/
@AfterUpdate()
private async notifyApproved() {
private async _notifyApproved() {
if (this.status === MediaRequestStatus.APPROVED) {
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
@@ -151,7 +159,7 @@ export class MediaRequest {
@AfterUpdate()
@AfterInsert()
private async updateParentStatus() {
private async _updateParentStatus() {
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: this.media.id },
@@ -206,7 +214,7 @@ export class MediaRequest {
}
@AfterRemove()
private async handleRemoveParentUpdate() {
private async _handleRemoveParentUpdate() {
const mediaRepository = getRepository(Media);
const fullMedia = await mediaRepository.findOneOrFail({
where: { id: this.media.id },
@@ -219,7 +227,7 @@ export class MediaRequest {
@AfterUpdate()
@AfterInsert()
private async sendToRadarr() {
private async _sendToRadarr() {
if (
this.status === MediaRequestStatus.APPROVED &&
this.type === MediaType.MOVIE
@@ -234,13 +242,24 @@ export class MediaRequest {
return;
}
const radarrSettings = settings.radarr.find(
(radarr) => radarr.isDefault && !radarr.is4k
);
if (!radarrSettings) {
logger.info(
'There is no default radarr configured. Did you set any of your Radarr servers as default?',
{ label: 'Media Request' }
);
return;
}
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 });
@@ -267,7 +286,7 @@ export class MediaRequest {
@AfterUpdate()
@AfterInsert()
private async sendToSonarr() {
private async _sendToSonarr() {
if (
this.status === MediaRequestStatus.APPROVED &&
this.type === MediaType.TV
@@ -283,6 +302,18 @@ export class MediaRequest {
return;
}
const sonarrSettings = settings.sonarr.find(
(sonarr) => sonarr.isDefault && !sonarr.is4k
);
if (!sonarrSettings) {
logger.info(
'There is no default 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'],
@@ -293,12 +324,11 @@ export class MediaRequest {
}
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 });
@@ -306,13 +336,32 @@ export class MediaRequest {
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';
}
// Run this asynchronously so we don't wait for it on the UI side
sonarr.addSeries({
profileId: sonarrSettings.activeProfileId,
rootFolderPath: sonarrSettings.activeDirectory,
profileId:
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId,
rootFolderPath:
seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
? sonarrSettings.activeAnimeDirectory
: sonarrSettings.activeDirectory,
title: series.name,
tvdbid: series.external_ids.tvdb_id,
seasons: this.seasons.map((season) => season.seasonNumber),
seasonFolder: sonarrSettings.enableSeasonFolders,
seriesType,
monitored: true,
searchNow: true,
});

View File

@@ -5,16 +5,9 @@ import {
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
AfterInsert,
AfterUpdate,
getRepository,
RelationId,
} 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 {
@@ -39,60 +32,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

@@ -5,11 +5,11 @@ import { createConnection, getRepository } from 'typeorm';
import routes from './routes';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import session, { Store } from 'express-session';
import { TypeormStore } from 'connect-typeorm/out';
import YAML from 'yamljs';
import swaggerUi from 'swagger-ui-express';
import { OpenApiValidator } from 'express-openapi-validator';
import * as OpenApiValidator from 'express-openapi-validator';
import { Session } from './entity/Session';
import { getSettings } from './lib/settings';
import logger from './logger';
@@ -17,9 +17,11 @@ 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 { getAppVersion } from './utils/appVersion';
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();
@@ -29,7 +31,7 @@ app
.then(async () => {
await createConnection();
// Load Settings
getSettings().load();
const settings = getSettings().load();
// Register Notification Agents
notificationManager.registerAgents([new DiscordAgent(), new EmailAgent()]);
@@ -47,7 +49,7 @@ app
server.use(
'/api',
session({
secret: 'verysecret',
secret: settings.clientId,
resave: false,
saveUninitialized: false,
cookie: {
@@ -56,22 +58,23 @@ app
store: new TypeormStore({
cleanupLimit: 2,
ttl: 1000 * 60 * 60 * 24 * 30,
}).connect(sessionRespository),
}).connect(sessionRespository) as Store,
})
);
const apiDocs = YAML.load(API_SPEC_PATH);
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
await new OpenApiValidator({
apiSpec: API_SPEC_PATH,
validateRequests: true,
validateResponses: true,
}).install(server);
server.use(
OpenApiValidator.middleware({
apiSpec: API_SPEC_PATH,
validateRequests: true,
})
);
/**
* This is a workaround to convert dates to strings before they are validated by
* OpenAPI validator. Otherwise, they are treated as objects instead of strings
* and response validation will fail
*/
server.use((req, res, next) => {
server.use((_req, res, next) => {
const original = res.json;
res.json = function jsonp(json) {
return original.call(this, JSON.parse(JSON.stringify(json)));
@@ -83,8 +86,10 @@ app
server.use(
(
err: { status: number; message: string; errors: string[] },
req: Request,
_req: Request,
res: Response,
// We must provide a next function for the function signature here even though its not used
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_next: NextFunction
) => {
// format error
@@ -95,15 +100,21 @@ app
}
);
const port = Number(process.env.PORT) || 3000;
server.listen(port, (err) => {
if (err) {
throw err;
}
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,7 @@
import { PersonCreditCast, PersonCreditCrew } from '../../models/Person';
export interface PersonCombinedCreditsResponse {
id: number;
cast: PersonCreditCast[];
crew: PersonCreditCrew[];
}

View File

@@ -0,0 +1,6 @@
export interface SettingsAboutResponse {
version: string;
totalRequests: number;
totalMediaItems: number;
tz?: string;
}

View File

@@ -7,8 +7,10 @@ import { MediaStatus, MediaType } from '../../constants/media';
import logger from '../../logger';
import { getSettings, Library } from '../../lib/settings';
import Season from '../../entity/Season';
import { uniqWith } from 'lodash';
const BUNDLE_SIZE = 10;
const BUNDLE_SIZE = 20;
const UPDATE_RATE = 4 * 1000;
const imdbRegex = new RegExp(/imdb:\/\/(tt[0-9]+)/);
const tmdbRegex = new RegExp(/tmdb:\/\/([0-9]+)/);
@@ -51,64 +53,81 @@ class JobPlexSync {
private async processMovie(plexitem: PlexLibraryItem) {
const mediaRepository = getRepository(Media);
if (plexitem.guid.match(plexRegex)) {
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
const newMedia = new Media();
try {
if (plexitem.guid.match(plexRegex)) {
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
const newMedia = new Media();
metadata.Guid.forEach((ref) => {
if (ref.id.match(imdbRegex)) {
newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined;
} else if (ref.id.match(tmdbRegex)) {
const tmdbMatch = ref.id.match(tmdbRegex)?.[1];
newMedia.tmdbId = Number(tmdbMatch);
if (!metadata.Guid) {
logger.debug('No Guid metadata for this title. Skipping', {
label: 'Plex Sync',
ratingKey: plexitem.ratingKey,
});
return;
}
});
const existing = await this.getExisting(newMedia.tmdbId);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${metadata.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
existing.status = MediaStatus.AVAILABLE;
mediaRepository.save(existing);
this.log(
`Request for ${metadata.title} exists. Setting status AVAILABLE`,
'info'
);
} else {
newMedia.status = MediaStatus.AVAILABLE;
newMedia.mediaType = MediaType.MOVIE;
await mediaRepository.save(newMedia);
this.log(`Saved ${plexitem.title}`);
}
} else {
const matchedid = plexitem.guid.match(/imdb:\/\/(tt[0-9]+)/);
if (matchedid?.[1]) {
const tmdbMovie = await this.tmdb.getMovieByImdbId({
imdbId: matchedid[1],
metadata.Guid.forEach((ref) => {
if (ref.id.match(imdbRegex)) {
newMedia.imdbId = ref.id.match(imdbRegex)?.[1] ?? undefined;
} else if (ref.id.match(tmdbRegex)) {
const tmdbMatch = ref.id.match(tmdbRegex)?.[1];
newMedia.tmdbId = Number(tmdbMatch);
}
});
const existing = await this.getExisting(tmdbMovie.id);
const existing = await this.getExisting(newMedia.tmdbId);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${plexitem.title}`);
this.log(`Title exists and is already available ${metadata.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
existing.status = MediaStatus.AVAILABLE;
await mediaRepository.save(existing);
mediaRepository.save(existing);
this.log(
`Request for ${plexitem.title} exists. Setting status AVAILABLE`,
`Request for ${metadata.title} exists. Setting status AVAILABLE`,
'info'
);
} else if (tmdbMovie) {
const newMedia = new Media();
newMedia.imdbId = tmdbMovie.external_ids.imdb_id;
newMedia.tmdbId = tmdbMovie.id;
} else {
newMedia.status = MediaStatus.AVAILABLE;
newMedia.mediaType = MediaType.MOVIE;
await mediaRepository.save(newMedia);
this.log(`Saved ${tmdbMovie.title}`);
this.log(`Saved ${plexitem.title}`);
}
} else {
const matchedid = plexitem.guid.match(/imdb:\/\/(tt[0-9]+)/);
if (matchedid?.[1]) {
const tmdbMovie = await this.tmdb.getMovieByImdbId({
imdbId: matchedid[1],
});
const existing = await this.getExisting(tmdbMovie.id);
if (existing && existing.status === MediaStatus.AVAILABLE) {
this.log(`Title exists and is already available ${plexitem.title}`);
} else if (existing && existing.status !== MediaStatus.AVAILABLE) {
existing.status = MediaStatus.AVAILABLE;
await mediaRepository.save(existing);
this.log(
`Request for ${plexitem.title} exists. Setting status AVAILABLE`,
'info'
);
} else if (tmdbMovie) {
const newMedia = new Media();
newMedia.imdbId = tmdbMovie.external_ids.imdb_id;
newMedia.tmdbId = tmdbMovie.id;
newMedia.status = MediaStatus.AVAILABLE;
newMedia.mediaType = MediaType.MOVIE;
await mediaRepository.save(newMedia);
this.log(`Saved ${tmdbMovie.title}`);
}
}
}
} catch (e) {
this.log(
`Failed to process plex item. ratingKey: ${
plexitem.parentRatingKey ?? plexitem.ratingKey
}`,
'error'
);
}
}
@@ -119,7 +138,9 @@ class JobPlexSync {
try {
const metadata = await this.plexClient.getMetadata(
plexitem.parentRatingKey ?? plexitem.ratingKey,
plexitem.grandparentRatingKey ??
plexitem.parentRatingKey ??
plexitem.ratingKey,
{ includeChildren: true }
);
if (metadata.guid.match(tvdbRegex)) {
@@ -222,9 +243,15 @@ class JobPlexSync {
} catch (e) {
this.log(
`Failed to process plex item. ratingKey: ${
plexitem.parentRatingKey ?? plexitem.ratingKey
plexitem.grandparentRatingKey ??
plexitem.parentRatingKey ??
plexitem.ratingKey
}`,
'error'
'error',
{
errorMessage: e.message,
plexitem,
}
);
}
}
@@ -234,7 +261,11 @@ class JobPlexSync {
slicedItems.map(async (plexitem) => {
if (plexitem.type === 'movie') {
await this.processMovie(plexitem);
} else if (plexitem.type === 'show') {
} else if (
plexitem.type === 'show' ||
plexitem.type === 'episode' ||
plexitem.type === 'season'
) {
await this.processShow(plexitem);
}
})
@@ -253,23 +284,24 @@ class JobPlexSync {
this.progress = start;
await this.processItems(slicedItems);
await new Promise((resolve) =>
await new Promise<void>((resolve) =>
setTimeout(async () => {
await this.loop({
start: start + BUNDLE_SIZE,
end: end + BUNDLE_SIZE,
});
resolve();
}, 5000)
}, UPDATE_RATE)
);
}
}
private log(
message: string,
level: 'info' | 'error' | 'debug' = 'debug'
level: 'info' | 'error' | 'debug' = 'debug',
optional?: Record<string, unknown>
): void {
logger[level](message, { label: 'Plex Sync' });
logger[level](message, { label: 'Plex Sync', ...optional });
}
public async run(): Promise<void> {
@@ -283,20 +315,40 @@ class JobPlexSync {
});
this.plexClient = new PlexAPI({ plexToken: admin.plexToken });
if (this.isRecentOnly) {
this.currentLibrary = {
id: '0',
name: 'Recently Added',
enabled: true,
};
this.log(`Beginning to process recently added`, 'info');
this.items = await this.plexClient.getRecentlyAdded();
await this.loop();
} else {
this.libraries = settings.plex.libraries.filter(
(library) => library.enabled
);
this.libraries = settings.plex.libraries.filter(
(library) => library.enabled
);
if (this.isRecentOnly) {
for (const library of this.libraries) {
this.currentLibrary = library;
this.log(
`Beginning to process recently added for library: ${library.name}`,
'info'
);
const libraryItems = await this.plexClient.getRecentlyAdded(
library.id
);
// Bundle items up by rating keys
this.items = uniqWith(libraryItems, (mediaA, mediaB) => {
if (mediaA.grandparentRatingKey && mediaB.grandparentRatingKey) {
return (
mediaA.grandparentRatingKey === mediaB.grandparentRatingKey
);
}
if (mediaA.parentRatingKey && mediaB.parentRatingKey) {
return mediaA.parentRatingKey === mediaB.parentRatingKey;
}
return mediaA.ratingKey === mediaB.ratingKey;
});
await this.loop();
}
} else {
for (const library of this.libraries) {
this.currentLibrary = library;
this.log(`Beginning to process library: ${library.name}`, 'info');

View File

@@ -13,7 +13,7 @@ export const startJobs = (): void => {
// Run recently added plex sync every 5 minutes
scheduledJobs.push({
name: 'Plex Recently Added Sync',
job: schedule.scheduleJob('0 */10 * * * *', () => {
job: schedule.scheduleJob('0 */5 * * * *', () => {
logger.info('Starting scheduled job: Plex Recently Added Sync', {
label: 'Jobs',
});
@@ -23,7 +23,7 @@ export const startJobs = (): void => {
// Run full plex sync every 6 hours
scheduledJobs.push({
name: 'Plex Full Library Sync',
job: schedule.scheduleJob('0 0 */6 * * *', () => {
job: schedule.scheduleJob('0 0 3 * * *', () => {
logger.info('Starting scheduled job: Plex Full Sync', { label: 'Jobs' });
jobPlexFullSync.run();
}),

View File

@@ -127,7 +127,9 @@ class DiscordAgent implements NotificationAgent {
};
}
public shouldSend(type: Notification): boolean {
// 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 (

View File

@@ -7,10 +7,12 @@ import Email from 'email-templates';
import logger from '../../../logger';
import { getRepository } from 'typeorm';
import { User } from '../../../entity/User';
import { hasPermission, Permission } from '../../permissions';
import { Permission } from '../../permissions';
class EmailAgent implements NotificationAgent {
public shouldSend(type: Notification): boolean {
// 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) {
@@ -27,10 +29,13 @@ class EmailAgent implements NotificationAgent {
host: emailSettings.smtpHost,
port: emailSettings.smtpPort,
secure: emailSettings.secure,
auth: {
user: emailSettings.authUser,
pass: emailSettings.authPass,
},
auth:
emailSettings.authUser && emailSettings.authPass
? {
user: emailSettings.authUser,
pass: emailSettings.authPass,
}
: undefined,
});
}
@@ -133,7 +138,7 @@ class EmailAgent implements NotificationAgent {
to: payload.notifyUser.email,
},
locals: {
body: 'Your requsested media is now available!',
body: 'Your requested media is now available!',
mediaName: payload.subject,
imageUrl: payload.image,
timestamp: new Date().toTimeString(),

View File

@@ -14,6 +14,7 @@ export interface PlexSettings {
machineId?: string;
ip: string;
port: number;
useSsl?: boolean;
libraries: Library[];
}
@@ -84,7 +85,7 @@ interface NotificationSettings {
}
interface AllSettings {
clientId?: string;
clientId: string;
main: MainSettings;
plex: PlexSettings;
radarr: RadarrSettings[];
@@ -100,14 +101,16 @@ class Settings {
constructor(initialSettings?: AllSettings) {
this.data = {
clientId: uuidv4(),
main: {
apiKey: 'temp',
apiKey: '',
applicationUrl: '',
},
plex: {
name: '',
ip: '127.0.0.1',
port: 32400,
useSsl: false,
libraries: [],
},
radarr: [],
@@ -143,6 +146,10 @@ class Settings {
}
get main(): MainSettings {
if (!this.data.main.apiKey) {
this.data.main.apiKey = this.generateApiKey();
this.save();
}
return this.data.main;
}
@@ -199,6 +206,16 @@ class Settings {
return this.data.clientId;
}
public regenerateApiKey(): MainSettings {
this.main.apiKey = this.generateApiKey();
this.save();
return this.main;
}
private generateApiKey(): string {
return Buffer.from(`${Date.now()}${uuidv4()})`).toString('base64');
}
/**
* Settings Load
*
@@ -207,10 +224,10 @@ class Settings {
* @param overrideSettings If passed in, will override all existing settings with these
* values
*/
public load(overrideSettings?: AllSettings): AllSettings {
public load(overrideSettings?: AllSettings): Settings {
if (overrideSettings) {
this.data = overrideSettings;
return this.data;
return this;
}
if (!fs.existsSync(SETTINGS_PATH)) {
@@ -222,7 +239,7 @@ class Settings {
this.data = merge(this.data, JSON.parse(data));
this.save();
}
return this.data;
return this;
}
public save(): void {

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

@@ -1,5 +1,4 @@
import { TmdbMovieDetails } from '../api/themoviedb';
import { MediaRequest } from '../entity/MediaRequest';
import {
ProductionCompany,
Genre,

View File

@@ -3,6 +3,7 @@ import {
TmdbPersonCreditCrew,
TmdbPersonDetail,
} from '../api/themoviedb';
import Media from '../entity/Media';
export interface PersonDetail {
id: number;
@@ -42,6 +43,7 @@ export interface PersonCredit {
title: string;
adult: boolean;
releaseDate: string;
mediaInfo?: Media;
}
export interface PersonCreditCast extends PersonCredit {
@@ -76,7 +78,8 @@ export const mapPersonDetails = (person: TmdbPersonDetail): PersonDetail => ({
});
export const mapCastCredits = (
cast: TmdbPersonCreditCast
cast: TmdbPersonCreditCast,
media?: Media
): PersonCreditCast => ({
id: cast.id,
originalLanguage: cast.original_language,
@@ -100,10 +103,12 @@ export const mapCastCredits = (
adult: cast.adult,
releaseDate: cast.release_date,
character: cast.character,
mediaInfo: media,
});
export const mapCrewCredits = (
crew: TmdbPersonCreditCrew
crew: TmdbPersonCreditCrew,
media?: Media
): PersonCreditCrew => ({
id: crew.id,
originalLanguage: crew.original_language,
@@ -128,4 +133,5 @@ export const mapCrewCredits = (
releaseDate: crew.release_date,
department: crew.department,
job: crew.job,
mediaInfo: media,
});

View File

@@ -3,7 +3,6 @@ import type {
TmdbPersonResult,
TmdbTvResult,
} from '../api/themoviedb';
import type { MediaRequest } from '../entity/MediaRequest';
import Media from '../entity/Media';
export type MediaType = 'tv' | 'movie' | 'person';

View File

@@ -7,6 +7,7 @@ import {
mapCrew,
ExternalIds,
mapExternalIds,
Keyword,
} from './common';
import {
TmdbTvEpisodeResult,
@@ -45,6 +46,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,7 +63,7 @@ export interface TvDetails {
profilePath?: string;
}[];
episodeRunTime: number[];
firstAirDate: string;
firstAirDate?: string;
genres: Genre[];
homepage: string;
inProduction: boolean;
@@ -74,6 +81,7 @@ export interface TvDetails {
overview: string;
popularity: number;
productionCompanies: ProductionCompany[];
spokenLanguages: SpokenLanguage[];
seasons: Season[];
status: string;
type: string;
@@ -84,6 +92,7 @@ export interface TvDetails {
crew: Crew[];
};
externalIds: ExternalIds;
keywords: Keyword[];
mediaInfo?: Media;
}
@@ -161,6 +170,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 +193,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,
});

View File

@@ -11,6 +11,11 @@ export interface ProductionCompany {
name: string;
}
export interface Keyword {
id: number;
name: string;
}
export interface Genre {
id: number;
name: string;

View File

@@ -14,7 +14,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
return res.status(500).json({
status: 500,
error:
'Requsted user endpoint withuot valid authenticated user in session',
'Requested user endpoint without valid authenticated user in session',
});
}
const user = await userRepository.findOneOrFail({
@@ -24,7 +24,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
return res.status(200).json(user.filter());
});
authRoutes.post('/login', async (req, res) => {
authRoutes.post('/login', async (req, res, next) => {
const userRepository = getRepository(User);
const body = req.body as { authToken?: string };
@@ -86,6 +86,22 @@ authRoutes.post('/login', async (req, res) => {
avatar: account.thumb,
});
await userRepository.save(user);
} else {
logger.info(
'Failed login attempt from user without access to plex server',
{
label: 'Auth',
account: {
...account,
authentication_token: '__REDACTED__',
authToken: '__REDACTED__',
},
}
);
return next({
status: 403,
message: 'You do not have access to this Plex server',
});
}
}
@@ -97,9 +113,10 @@ authRoutes.post('/login', async (req, res) => {
return res.status(200).json(user?.filter() ?? {});
} catch (e) {
logger.error(e.message, { label: 'Auth' });
res
.status(500)
.json({ error: 'Something went wrong. Is your auth token valid?' });
return next({
status: 500,
message: 'Something went wrong. Is your auth token valid?',
});
}
});

View File

@@ -1,9 +1,5 @@
import { Router } from 'express';
import TheMovieDb, {
TmdbMovieResult,
TmdbTvResult,
TmdbPersonResult,
} from '../api/themoviedb';
import TheMovieDb from '../api/themoviedb';
import { mapMovieResult, mapTvResult, mapPersonResult } from '../models/Search';
import Media from '../entity/Media';
import { isMovie, isPerson } from '../utils/typeHelpers';
@@ -125,4 +121,36 @@ discoverRoutes.get('/trending', async (req, res) => {
});
});
discoverRoutes.get<{ keywordId: string }>(
'/keyword/:keywordId/movies',
async (req, res) => {
const tmdb = new TheMovieDb();
const data = await tmdb.getMoviesByKeyword({
keywordId: Number(req.params.keywordId),
page: Number(req.query.page),
language: req.query.language as string,
});
const media = await Media.getRelatedMedia(
data.results.map((result) => result.id)
);
return res.status(200).json({
page: data.page,
totalPages: data.total_pages,
totalResults: data.total_results,
results: data.results.map((result) =>
mapMovieResult(
result,
media.find(
(req) =>
req.tmdbId === result.id && req.mediaType === MediaType.MOVIE
)
)
),
});
}
);
export default discoverRoutes;

View File

@@ -12,8 +12,8 @@ const mediaRoutes = Router();
mediaRoutes.get('/', async (req, res, next) => {
const mediaRepository = getRepository(Media);
const pageSize = Number(req.query.take) ?? 20;
const skip = Number(req.query.skip) ?? 0;
const pageSize = req.query.take ? Number(req.query.take) : 20;
const skip = req.query.skip ? Number(req.query.skip) : 0;
let statusFilter:
| MediaStatus

View File

@@ -1,7 +1,6 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { mapMovieDetails } from '../models/Movie';
import { MediaRequest } from '../entity/MediaRequest';
import { mapMovieResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';

View File

@@ -1,6 +1,6 @@
import { Router } from 'express';
import next from 'next';
import TheMovieDb from '../api/themoviedb';
import Media from '../entity/Media';
import logger from '../logger';
import {
mapCastCredits,
@@ -33,9 +33,27 @@ personRoutes.get('/:id/combined_credits', async (req, res) => {
language: req.query.language as string,
});
const castMedia = await Media.getRelatedMedia(
combinedCredits.cast.map((result) => result.id)
);
const crewMedia = await Media.getRelatedMedia(
combinedCredits.crew.map((result) => result.id)
);
return res.status(200).json({
cast: combinedCredits.cast.map((result) => mapCastCredits(result)),
crew: combinedCredits.crew.map((result) => mapCrewCredits(result)),
cast: combinedCredits.cast.map((result) =>
mapCastCredits(
result,
castMedia.find((med) => med.tmdbId === result.id)
)
),
crew: combinedCredits.crew.map((result) =>
mapCrewCredits(
result,
crewMedia.find((med) => med.tmdbId === result.id)
)
),
id: combinedCredits.id,
});
});

View File

@@ -15,12 +15,12 @@ const requestRoutes = Router();
requestRoutes.get('/', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const pageSize = Number(req.query.take) ?? 20;
const skip = Number(req.query.skip) ?? 0;
const pageSize = req.query.take ? Number(req.query.take) : 20;
const skip = req.query.skip ? Number(req.query.skip) : 0;
let statusFilter:
| MediaRequestStatus
| FindOperator<MediaRequestStatus>
| FindOperator<string | MediaRequestStatus>
| undefined = undefined;
switch (req.query.filter) {

View File

@@ -4,10 +4,11 @@ import {
RadarrSettings,
SonarrSettings,
Library,
MainSettings,
} from '../lib/settings';
import { getRepository } from 'typeorm';
import { User } from '../entity/User';
import PlexAPI, { PlexLibrary } from '../api/plexapi';
import PlexAPI from '../api/plexapi';
import { jobPlexFullSync } from '../job/plexsync';
import SonarrAPI from '../api/sonarr';
import RadarrAPI from '../api/radarr';
@@ -16,13 +17,34 @@ import { scheduledJobs } from '../job/schedule';
import { Permission } from '../lib/permissions';
import { isAuthenticated } from '../middleware/auth';
import { merge } from 'lodash';
import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest';
import { getAppVersion } from '../utils/appVersion';
import { SettingsAboutResponse } from '../interfaces/api/settingsInterfaces';
const settingsRoutes = Router();
settingsRoutes.get('/main', (_req, res) => {
const filteredMainSettings = (
user: User,
main: MainSettings
): Partial<MainSettings> => {
if (!user?.hasPermission(Permission.ADMIN)) {
return {
applicationUrl: main.applicationUrl,
};
}
return main;
};
settingsRoutes.get('/main', (req, res, next) => {
const settings = getSettings();
res.status(200).json(settings.main);
if (!req.user) {
return next({ status: 500, message: 'User missing from request' });
}
res.status(200).json(filteredMainSettings(req.user, settings.main));
});
settingsRoutes.post('/main', (req, res) => {
@@ -34,6 +56,18 @@ settingsRoutes.post('/main', (req, res) => {
return res.status(200).json(settings.main);
});
settingsRoutes.get('/main/regenerate', (req, res, next) => {
const settings = getSettings();
const main = settings.regenerateApiKey();
if (!req.user) {
return next({ status: 500, message: 'User missing from request' });
}
return res.status(200).json(filteredMainSettings(req.user, main));
});
settingsRoutes.get('/plex', (_req, res) => {
const settings = getSettings();
@@ -84,17 +118,22 @@ settingsRoutes.get('/plex/library', async (req, res) => {
const libraries = await plexapi.getLibraries();
const newLibraries: Library[] = libraries.map((library) => {
const existing = settings.plex.libraries.find(
(l) => l.id === library.key
);
const newLibraries: Library[] = libraries
// Remove libraries that are not movie or show
.filter((library) => library.type === 'movie' || library.type === 'show')
// Remove libraries that do not have a metadata agent set (usually personal video libraries)
.filter((library) => library.agent !== 'com.plexapp.agents.none')
.map((library) => {
const existing = settings.plex.libraries.find(
(l) => l.id === library.key && l.name === library.title
);
return {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
};
});
return {
id: library.key,
name: library.title,
enabled: existing?.enabled ?? false,
};
});
settings.plex.libraries = newLibraries;
}
@@ -120,7 +159,7 @@ settingsRoutes.get('/plex/sync', (req, res) => {
return res.status(200).json(jobPlexFullSync.status());
});
settingsRoutes.get('/radarr', (req, res) => {
settingsRoutes.get('/radarr', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.radarr);
@@ -261,7 +300,7 @@ settingsRoutes.delete<{ id: string }>('/radarr/:id', (req, res) => {
return res.status(200).json(removed[0]);
});
settingsRoutes.get('/sonarr', (req, res) => {
settingsRoutes.get('/sonarr', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.sonarr);
@@ -372,7 +411,7 @@ settingsRoutes.delete<{ id: string }>('/sonarr/:id', (req, res) => {
return res.status(200).json(removed[0]);
});
settingsRoutes.get('/jobs', (req, res) => {
settingsRoutes.get('/jobs', (_req, res) => {
return res.status(200).json(
scheduledJobs.map((job) => ({
name: job.name,
@@ -384,7 +423,7 @@ settingsRoutes.get('/jobs', (req, res) => {
settingsRoutes.get(
'/initialize',
isAuthenticated(Permission.ADMIN),
(req, res) => {
(_req, res) => {
const settings = getSettings();
settings.public.initialized = true;
@@ -394,7 +433,7 @@ settingsRoutes.get(
}
);
settingsRoutes.get('/notifications/discord', (req, res) => {
settingsRoutes.get('/notifications/discord', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.discord);
@@ -409,7 +448,7 @@ settingsRoutes.post('/notifications/discord', (req, res) => {
res.status(200).json(settings.notifications.agents.discord);
});
settingsRoutes.get('/notifications/email', (req, res) => {
settingsRoutes.get('/notifications/email', (_req, res) => {
const settings = getSettings();
res.status(200).json(settings.notifications.agents.email);
@@ -424,4 +463,19 @@ settingsRoutes.post('/notifications/email', (req, res) => {
res.status(200).json(settings.notifications.agents.email);
});
settingsRoutes.get('/about', async (req, res) => {
const mediaRepository = getRepository(Media);
const mediaRequestRepository = getRepository(MediaRequest);
const totalMediaItems = await mediaRepository.count();
const totalRequests = await mediaRequestRepository.count();
return res.status(200).json({
version: getAppVersion(),
totalMediaItems,
totalRequests,
tz: process.env.TZ,
} as SettingsAboutResponse);
});
export default settingsRoutes;

View File

@@ -1,10 +1,10 @@
import { Router } from 'express';
import TheMovieDb from '../api/themoviedb';
import { MediaRequest } from '../entity/MediaRequest';
import { mapTvDetails, mapSeasonWithEpisodes } from '../models/Tv';
import { mapTvResult } from '../models/Search';
import Media from '../entity/Media';
import RottenTomatoes from '../api/rottentomatoes';
import logger from '../logger';
const tvRoutes = Router();
@@ -20,6 +20,10 @@ tvRoutes.get('/:id', async (req, res, next) => {
return res.status(200).json(mapTvDetails(tv, media));
} catch (e) {
logger.error('Failed to get tv show', {
label: 'API',
errorMessage: e.message,
});
return next({ status: 404, message: 'TV Show does not exist' });
}
});
@@ -102,7 +106,7 @@ tvRoutes.get('/:id/ratings', async (req, res, next) => {
const rtratings = await rtapi.getTVRatings(
tv.name,
Number(tv.first_air_date.slice(0, 4))
tv.first_air_date ? Number(tv.first_air_date.slice(0, 4)) : undefined
);
if (!rtratings) {

View File

@@ -1,11 +1,13 @@
import { Router } from 'express';
import { getRepository } from 'typeorm';
import { MediaRequest } from '../entity/MediaRequest';
import { User } from '../entity/User';
import { hasPermission, Permission } from '../lib/permissions';
import logger from '../logger';
const router = Router();
router.get('/', async (req, res) => {
router.get('/', async (_req, res) => {
const userRepository = getRepository(User);
const users = await userRepository.find();
@@ -94,13 +96,49 @@ router.delete<{ id: string }>('/:id', async (req, res, next) => {
try {
const userRepository = getRepository(User);
const user = await userRepository.findOneOrFail({
const user = await userRepository.findOne({
where: { id: Number(req.params.id) },
relations: ['requests'],
});
if (!user) {
return next({ status: 404, message: 'User not found' });
}
if (user.id === 1) {
return next({ status: 405, message: 'This account cannot be deleted.' });
}
if (user.hasPermission(Permission.ADMIN)) {
return next({
status: 405,
message: 'You cannot delete users with administrative privileges.',
});
}
const requestRepository = getRepository(MediaRequest);
/**
* Requests are usually deleted through a cascade constraint. Those however, do
* not trigger the removal event so listeners to not run and the parent Media
* will not be updated back to unknown for titles that were still pending. So
* we manually remove all requests from the user here so the parent media's
* properly reflect the change.
*/
await requestRepository.remove(user.requests);
await userRepository.delete(user.id);
return res.status(200).json(user.filter());
} catch (e) {
next({ status: 404, message: 'User not found' });
logger.error('Something went wrong deleting a user', {
label: 'API',
userId: req.params.id,
errorMessage: e.message,
});
return next({
status: 500,
message: 'Something went wrong deleting the user',
});
}
});

View File

@@ -0,0 +1,112 @@
import {
EntitySubscriberInterface,
EventSubscriber,
getRepository,
UpdateEvent,
} from 'typeorm';
import TheMovieDb from '../api/themoviedb';
import { MediaStatus, MediaType } from '../constants/media';
import Media from '../entity/Media';
import { MediaRequest } from '../entity/MediaRequest';
import notificationManager, { Notification } from '../lib/notifications';
@EventSubscriber()
export class MediaSubscriber implements EntitySubscriberInterface {
private async notifyAvailableMovie(entity: Media) {
if (entity.status === MediaStatus.AVAILABLE) {
if (entity.mediaType === MediaType.MOVIE) {
const requestRepository = getRepository(MediaRequest);
const relatedRequests = await requestRepository.find({
where: { media: entity },
});
if (relatedRequests.length > 0) {
const tmdb = new TheMovieDb();
const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
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}`,
});
});
}
}
}
}
private async notifyAvailableSeries(entity: Media, dbEntity: Media) {
const newAvailableSeasons = entity.seasons
.filter((season) => season.status === MediaStatus.AVAILABLE)
.map((season) => season.seasonNumber);
const oldAvailableSeasons = dbEntity.seasons
.filter((season) => season.status === MediaStatus.AVAILABLE)
.map((season) => season.seasonNumber);
const changedSeasons = newAvailableSeasons.filter(
(seasonNumber) => !oldAvailableSeasons.includes(seasonNumber)
);
if (changedSeasons.length > 0) {
const tmdb = new TheMovieDb();
const requestRepository = getRepository(MediaRequest);
const processedSeasons: number[] = [];
for (const changedSeasonNumber of changedSeasons) {
const requests = await requestRepository.find({
where: { media: entity },
});
const request = requests.find(
(request) =>
// Check if the season is complete AND it contains the current season that was just marked available
request.seasons.every((season) =>
newAvailableSeasons.includes(season.seasonNumber)
) &&
request.seasons.some(
(season) => season.seasonNumber === changedSeasonNumber
)
);
if (request && !processedSeasons.includes(changedSeasonNumber)) {
processedSeasons.push(
...request.seasons.map((season) => season.seasonNumber)
);
const tv = await tmdb.getTvShow({ tvId: entity.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(', '),
},
],
});
}
}
}
}
public beforeUpdate(event: UpdateEvent<Media>): void {
if (
event.entity.mediaType === MediaType.MOVIE &&
event.entity.status === MediaStatus.AVAILABLE
) {
this.notifyAvailableMovie(event.entity);
}
if (
event.entity.mediaType === MediaType.TV &&
(event.entity.status === MediaStatus.AVAILABLE ||
event.entity.status === MediaStatus.PARTIALLY_AVAILABLE)
) {
this.notifyAvailableSeries(event.entity, event.databaseEntity);
}
}
}

View File

@@ -47,7 +47,7 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en')
padding-bottom: 25px;\
text-align: center;\
')
a(href='https://example.com' style='\
a(href=actionUrl style='\
text-shadow: 0 1px 0 #ffffff;\
font-weight: 700;\
font-size: 16px;\

View File

@@ -2,8 +2,9 @@ declare module 'plex-api' {
export default class PlexAPI {
constructor(intiialOptions: {
hostname: string;
post: number;
port: number;
token?: string;
https?: boolean;
authenticator: {
authenticate: (
_plexApi: PlexAPI,

View File

@@ -0,0 +1,12 @@
export const getAppVersion = (): string => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require('../../package.json');
let finalVersion = version;
if (version === '0.1.0') {
finalVersion = `develop-${process.env.COMMIT_TAG ?? 'local'}`;
}
return finalVersion;
};

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 575 289.83"><defs><path d="M575 24.91C573.44 12.15 563.97 1.98 551.91 0C499.05 0 76.18 0 23.32 0C10.11 2.17 0 14.16 0 28.61C0 51.84 0 237.64 0 260.86C0 276.86 12.37 289.83 27.64 289.83C79.63 289.83 495.6 289.83 547.59 289.83C561.65 289.83 573.26 278.82 575 264.57C575 216.64 575 48.87 575 24.91Z" id="d1pwhf9wy2"></path><path d="M69.35 58.24L114.98 58.24L114.98 233.89L69.35 233.89L69.35 58.24Z" id="g5jjnq26yS"></path><path d="M201.2 139.15C197.28 112.38 195.1 97.5 194.67 94.53C192.76 80.2 190.94 67.73 189.2 57.09C185.25 57.09 165.54 57.09 130.04 57.09L130.04 232.74L170.01 232.74L170.15 116.76L186.97 232.74L215.44 232.74L231.39 114.18L231.54 232.74L271.38 232.74L271.38 57.09L211.77 57.09L201.2 139.15Z" id="i3Prh1JpXt"></path><path d="M346.71 93.63C347.21 95.87 347.47 100.95 347.47 108.89C347.47 115.7 347.47 170.18 347.47 176.99C347.47 188.68 346.71 195.84 345.2 198.48C343.68 201.12 339.64 202.43 333.09 202.43C333.09 190.9 333.09 98.66 333.09 87.13C338.06 87.13 341.45 87.66 343.25 88.7C345.05 89.75 346.21 91.39 346.71 93.63ZM367.32 230.95C372.75 229.76 377.31 227.66 381.01 224.67C384.7 221.67 387.29 217.52 388.77 212.21C390.26 206.91 391.14 196.38 391.14 180.63C391.14 174.47 391.14 125.12 391.14 118.95C391.14 102.33 390.49 91.19 389.48 85.53C388.46 79.86 385.93 74.71 381.88 70.09C377.82 65.47 371.9 62.15 364.12 60.13C356.33 58.11 343.63 57.09 321.54 57.09C319.27 57.09 307.93 57.09 287.5 57.09L287.5 232.74L342.78 232.74C355.52 232.34 363.7 231.75 367.32 230.95Z" id="a4ov9rRGQm"></path><path d="M464.76 204.7C463.92 206.93 460.24 208.06 457.46 208.06C454.74 208.06 452.93 206.98 452.01 204.81C451.09 202.65 450.64 197.72 450.64 190C450.64 185.36 450.64 148.22 450.64 143.58C450.64 135.58 451.04 130.59 451.85 128.6C452.65 126.63 454.41 125.63 457.13 125.63C459.91 125.63 463.64 126.76 464.6 129.03C465.55 131.3 466.03 136.15 466.03 143.58C466.03 146.58 466.03 161.58 466.03 188.59C465.74 197.84 465.32 203.21 464.76 204.7ZM406.68 231.21L447.76 231.21C449.47 224.5 450.41 220.77 450.6 220.02C454.32 224.52 458.41 227.9 462.9 230.14C467.37 232.39 474.06 233.51 479.24 233.51C486.45 233.51 492.67 231.62 497.92 227.83C503.16 224.05 506.5 219.57 507.92 214.42C509.34 209.26 510.05 201.42 510.05 190.88C510.05 185.95 510.05 146.53 510.05 141.6C510.05 131 509.81 124.08 509.34 120.83C508.87 117.58 507.47 114.27 505.14 110.88C502.81 107.49 499.42 104.86 494.98 102.98C490.54 101.1 485.3 100.16 479.26 100.16C474.01 100.16 467.29 101.21 462.81 103.28C458.34 105.35 454.28 108.49 450.64 112.7C450.64 108.89 450.64 89.85 450.64 55.56L406.68 55.56L406.68 231.21Z" id="fk968BpsX"></path></defs><g><g><g><use xlink:href="#d1pwhf9wy2" opacity="1" fill="#f6c700" fill-opacity="1"></use><g><use xlink:href="#d1pwhf9wy2" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#g5jjnq26yS" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#g5jjnq26yS" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#i3Prh1JpXt" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#i3Prh1JpXt" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#a4ov9rRGQm" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#a4ov9rRGQm" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#fk968BpsX" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#fk968BpsX" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.04 133.4"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="66.7" x2="185.04" y2="66.7" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M51.06,66.7h0A17.67,17.67,0,0,1,68.73,49h-.1A17.67,17.67,0,0,1,86.3,66.7h0A17.67,17.67,0,0,1,68.63,84.37h.1A17.67,17.67,0,0,1,51.06,66.7Zm82.67-31.33h32.9A17.67,17.67,0,0,0,184.3,17.7h0A17.67,17.67,0,0,0,166.63,0h-32.9A17.67,17.67,0,0,0,116.06,17.7h0A17.67,17.67,0,0,0,133.73,35.37Zm-113,98h63.9A17.67,17.67,0,0,0,102.3,115.7h0A17.67,17.67,0,0,0,84.63,98H20.73A17.67,17.67,0,0,0,3.06,115.7h0A17.67,17.67,0,0,0,20.73,133.37Zm83.92-49h6.25L125.5,49h-8.35l-8.9,23.2h-.1L99.4,49H90.5Zm32.45,0h7.8V49h-7.8Zm22.2,0h24.95V77.2H167.1V70h15.35V62.8H167.1V56.2h16.25V49h-24ZM10.1,35.4h7.8V6.9H28V0H0V6.9H10.1ZM39,35.4h7.8V20.1H61.9V35.4h7.8V0H61.9V13.2H46.75V0H39Zm41.25,0h25V28.2H88V21h15.35V13.8H88V7.2h16.25V0h-24Zm-79,49H9V57.25h.1l9,27.15H24l9.3-27.15h.1V84.4h7.8V49H29.45l-8.2,23.1h-.1L13,49H1.2Zm112.09,49H126a24.59,24.59,0,0,0,7.56-1.15,19.52,19.52,0,0,0,6.35-3.37,16.37,16.37,0,0,0,4.37-5.5A16.91,16.91,0,0,0,146,115.8a18.5,18.5,0,0,0-1.68-8.25,15.1,15.1,0,0,0-4.52-5.53A18.55,18.55,0,0,0,133.07,99,33.54,33.54,0,0,0,125,98H113.29Zm7.81-28.2h4.6a17.43,17.43,0,0,1,4.67.62,11.68,11.68,0,0,1,3.88,1.88,9,9,0,0,1,2.62,3.18,9.87,9.87,0,0,1,1,4.52,11.92,11.92,0,0,1-1,5.08,8.69,8.69,0,0,1-2.67,3.34,10.87,10.87,0,0,1-4,1.83,21.57,21.57,0,0,1-5,.55H121.1Zm36.14,28.2h14.5a23.11,23.11,0,0,0,4.73-.5,13.38,13.38,0,0,0,4.27-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68,9.16,9.16,0,0,0-.55-3.2,7.79,7.79,0,0,0-1.57-2.62,8.38,8.38,0,0,0-2.45-1.85,10,10,0,0,0-3.18-1v-.1a9.28,9.28,0,0,0,4.43-2.82,7.42,7.42,0,0,0,1.67-5,8.34,8.34,0,0,0-1.15-4.65,7.88,7.88,0,0,0-3-2.73,12.9,12.9,0,0,0-4.17-1.3,34.42,34.42,0,0,0-4.63-.32h-13.2Zm7.8-28.8h5.3a10.79,10.79,0,0,1,1.85.17,5.77,5.77,0,0,1,1.7.58,3.33,3.33,0,0,1,1.23,1.13,3.22,3.22,0,0,1,.47,1.82,3.63,3.63,0,0,1-.42,1.8,3.34,3.34,0,0,1-1.13,1.2,4.78,4.78,0,0,1-1.57.65,8.16,8.16,0,0,1-1.78.2H165Zm0,14.15h5.9a15.12,15.12,0,0,1,2.05.15,7.83,7.83,0,0,1,2,.55,4,4,0,0,1,1.58,1.17,3.13,3.13,0,0,1,.62,2,3.71,3.71,0,0,1-.47,1.95,4,4,0,0,1-1.23,1.3,4.78,4.78,0,0,1-1.67.7,8.91,8.91,0,0,1-1.83.2h-7Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,36 @@
import React from 'react';
interface AlertProps {
title: string;
type?: 'warning';
}
const Alert: React.FC<AlertProps> = ({ title, children }) => {
return (
<div className="rounded-md bg-yellow-600 p-4 mb-8">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-yellow-200"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-200">{title}</h3>
<div className="mt-2 text-sm text-yellow-300">{children}</div>
</div>
</div>
</div>
);
};
export default Alert;

View File

@@ -2,7 +2,7 @@ import React from 'react';
interface HeaderProps {
extraMargin?: number;
subtext?: string;
subtext?: React.ReactNode;
}
const Header: React.FC<HeaderProps> = ({

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { withProperties } from '../../../utils/typeHelpers';
interface ListItemProps {
title: string;
}
const ListItem: React.FC<ListItemProps> = ({ title, children }) => {
return (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<dt className="text-sm font-medium text-gray-200">{title}</dt>
<dd className="mt-1 flex text-sm text-gray-400 sm:mt-0 sm:col-span-2">
<span className="flex-grow">{children}</span>
</dd>
</div>
);
};
interface ListProps {
title: string;
subTitle?: string;
}
const List: React.FC<ListProps> = ({ title, subTitle, children }) => {
return (
<>
<div>
<h3 className="text-lg leading-6 font-medium text-gray-100">{title}</h3>
{subTitle && (
<p className="mt-1 max-w-2xl text-sm text-gray-300">{subTitle}</p>
)}
</div>
<div className="mt-5 border-t border-gray-800">
<dl className="divide-y divide-gray-800">{children}</dl>
</div>
</>
);
};
export default withProperties(List, { Item: ListItem });

View File

@@ -69,6 +69,7 @@ const ListView: React.FC<ListViewProps> = ({
case 'person':
titleCard = (
<PersonCard
personId={title.id}
name={title.name}
profilePath={title.profilePath}
canExpand

View File

@@ -1,14 +1,14 @@
import React, { MouseEvent, ReactNode, useRef } from 'react';
import ReactDOM from 'react-dom';
import Button, { ButtonType } from '../Button';
import { useTransition, animated } from 'react-spring';
import { useLockBodyScroll } from '../../../hooks/useLockBodyScroll';
import LoadingSpinner from '../LoadingSpinner';
import useClickOutside from '../../../hooks/useClickOutside';
import { useIntl } from 'react-intl';
import globalMessages from '../../../i18n/globalMessages';
import Transition from '../../Transition';
interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
interface ModalProps {
title?: string;
onCancel?: (e?: MouseEvent<HTMLElement>) => void;
onOk?: (e?: MouseEvent<HTMLButtonElement>) => void;
@@ -53,7 +53,6 @@ const Modal: React.FC<ModalProps> = ({
tertiaryDisabled = false,
tertiaryText,
onTertiary,
...props
}) => {
const intl = useIntl();
const modalRef = useRef<HTMLDivElement>(null);
@@ -63,134 +62,123 @@ const Modal: React.FC<ModalProps> = ({
: undefined;
});
useLockBodyScroll(true, disableScrollLock);
const containerTransitions = useTransition(!loading, null, {
from: { opacity: 0, transform: 'scale(0.5)' },
enter: { opacity: 1, transform: 'scale(1)' },
leave: { opacity: 0, transform: 'scale(0.5)' },
config: { tension: 500, velocity: 40, friction: 60 },
});
const loadingTransitions = useTransition(loading, null, {
from: { opacity: 0, transform: 'scale(0.5)' },
enter: { opacity: 1, transform: 'scale(1)' },
leave: { opacity: 0, transform: 'scale(0.5)' },
config: { tension: 500, velocity: 40, friction: 60 },
});
return (
<>
{ReactDOM.createPortal(
<animated.div
className="fixed top-0 left-0 right-0 bottom-0 bg-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
style={props.style}
onKeyDown={(e) => {
if (e.key === 'Escape') {
typeof onCancel === 'function' && backgroundClickable
? onCancel()
: undefined;
}
}}
return ReactDOM.createPortal(
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="fixed top-0 left-0 right-0 bottom-0 bg-gray-800 bg-opacity-50 w-full h-full z-50 flex justify-center items-center"
onKeyDown={(e) => {
if (e.key === 'Escape') {
typeof onCancel === 'function' && backgroundClickable
? onCancel()
: undefined;
}
}}
>
<Transition
enter="transition opacity-0 duration-300 transform scale-75"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={loading}
>
<div style={{ position: 'absolute' }}>
<LoadingSpinner />
</div>
</Transition>
<Transition
enter="transition opacity-0 duration-300 transform scale-75"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={!loading}
>
<div
className="inline-block align-bottom bg-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6 max-h-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
ref={modalRef}
>
{loadingTransitions.map(
({ props, item, key }) =>
item && (
<animated.div
style={{ ...props, position: 'absolute' }}
key={key}
<div className="sm:flex sm:items-center">
{iconSvg && (
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gray-600 text-white sm:mx-0 sm:h-10 sm:w-10">
{iconSvg}
</div>
)}
<div
className={`mt-3 text-center sm:mt-0 sm:text-left ${
iconSvg ? 'sm:ml-4' : 'mb-6'
}`}
>
{title && (
<h3
className="text-lg leading-6 font-medium text-white"
id="modal-headline"
>
<LoadingSpinner />
</animated.div>
)
{title}
</h3>
)}
</div>
</div>
{children && (
<div className="mt-4 text-sm leading-5 text-gray-300">
{children}
</div>
)}
{containerTransitions.map(
({ props, item, key }) =>
item && (
<animated.div
style={props}
className="inline-block align-bottom bg-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6 max-h-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
key={key}
ref={modalRef}
{(onCancel || onOk || onSecondary || onTertiary) && (
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
{typeof onOk === 'function' && (
<Button
buttonType={okButtonType}
onClick={onOk}
className="ml-3"
disabled={okDisabled}
>
<div className="sm:flex sm:items-center">
{iconSvg && (
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gray-600 text-white sm:mx-0 sm:h-10 sm:w-10">
{iconSvg}
</div>
)}
<div
className={`mt-3 text-center sm:mt-0 sm:text-left ${
iconSvg ? 'sm:ml-4' : 'mb-6'
}`}
>
{title && (
<h3
className="text-lg leading-6 font-medium text-white"
id="modal-headline"
>
{title}
</h3>
)}
</div>
</div>
{children && (
<div className="mt-4 text-sm leading-5 text-gray-300">
{children}
</div>
)}
{(onCancel || onOk || onSecondary || onTertiary) && (
<div className="mt-5 sm:mt-4 flex justify-center sm:justify-start flex-row-reverse">
{typeof onOk === 'function' && (
<Button
buttonType={okButtonType}
onClick={onOk}
className="ml-3"
disabled={okDisabled}
>
{okText ? okText : 'Ok'}
</Button>
)}
{typeof onSecondary === 'function' && secondaryText && (
<Button
buttonType={secondaryButtonType}
onClick={onSecondary}
className="ml-3"
disabled={secondaryDisabled}
>
{secondaryText}
</Button>
)}
{typeof onTertiary === 'function' && tertiaryText && (
<Button
buttonType={tertiaryButtonType}
onClick={onTertiary}
className="ml-3"
disabled={tertiaryDisabled}
>
{tertiaryText}
</Button>
)}
{typeof onCancel === 'function' && (
<Button
buttonType={cancelButtonType}
onClick={onCancel}
className="ml-3 sm:ml-0 sm:px-4"
>
{cancelText
? cancelText
: intl.formatMessage(globalMessages.cancel)}
</Button>
)}
</div>
)}
</animated.div>
)
{okText ? okText : 'Ok'}
</Button>
)}
{typeof onSecondary === 'function' && secondaryText && (
<Button
buttonType={secondaryButtonType}
onClick={onSecondary}
className="ml-3"
disabled={secondaryDisabled}
>
{secondaryText}
</Button>
)}
{typeof onTertiary === 'function' && tertiaryText && (
<Button
buttonType={tertiaryButtonType}
onClick={onTertiary}
className="ml-3"
disabled={tertiaryDisabled}
>
{tertiaryText}
</Button>
)}
{typeof onCancel === 'function' && (
<Button
buttonType={cancelButtonType}
onClick={onCancel}
className="ml-3 sm:ml-0 sm:px-4"
>
{cancelText
? cancelText
: intl.formatMessage(globalMessages.cancel)}
</Button>
)}
</div>
)}
</animated.div>,
document.body
)}
</>
</div>
</Transition>
</div>,
document.body
);
};

View File

@@ -0,0 +1,85 @@
import React, { AllHTMLAttributes } from 'react';
import { withProperties } from '../../../utils/typeHelpers';
const TBody: React.FC = ({ children }) => {
return (
<tbody className="bg-gray-600 divide-y divide-gray-700">{children}</tbody>
);
};
const TH: React.FC<AllHTMLAttributes<HTMLTableHeaderCellElement>> = ({
children,
className,
...props
}) => {
const style = [
'px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider',
];
if (className) {
style.push(className);
}
return (
<th className={style.join(' ')} {...props}>
{children}
</th>
);
};
interface TDProps extends AllHTMLAttributes<HTMLTableCellElement> {
alignText?: 'left' | 'center' | 'right';
noPadding?: boolean;
}
const TD: React.FC<TDProps> = ({
children,
alignText = 'left',
noPadding,
className,
...props
}) => {
const style = ['whitespace-nowrap text-sm leading-5 text-white'];
switch (alignText) {
case 'left':
style.push('text-left');
break;
case 'center':
style.push('text-center');
break;
case 'right':
style.push('text-right');
break;
}
if (!noPadding) {
style.push('px-6 py-4');
}
if (className) {
style.push(className);
}
return (
<td className={style.join(' ')} {...props}>
{children}
</td>
);
};
const Table: React.FC = ({ children }) => {
return (
<div className="flex flex-col">
<div className="my-2 overflow-x-auto -mx-6 sm:-mx-6 md:mx-4 lg:mx-4">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden sm:rounded-lg">
<table className="min-w-full">{children}</table>
</div>
</div>
</div>
</div>
);
};
export default withProperties(Table, { TH, TBody, TD });

View File

@@ -0,0 +1,70 @@
import React, { useContext } from 'react';
import { useSWRInfinite } from 'swr';
import type { MovieResult } from '../../../../server/models/Search';
import ListView from '../../Common/ListView';
import { LanguageContext } from '../../../context/LanguageContext';
import Header from '../../Common/Header';
interface SearchResult {
page: number;
totalResults: number;
totalPages: number;
results: MovieResult[];
}
const Holiday: React.FC = () => {
const { locale } = useContext(LanguageContext);
const { data, error, size, setSize } = useSWRInfinite<SearchResult>(
(pageIndex: number, previousPageData: SearchResult | null) => {
if (previousPageData && pageIndex + 1 > previousPageData.totalPages) {
return null;
}
return `/api/v1/discover/keyword/207317/movies?page=${
pageIndex + 1
}&language=${locale}`;
},
{
initialSize: 3,
}
);
const isLoadingInitialData = !data && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && data && typeof data[size - 1] === 'undefined');
const fetchMore = () => {
setSize(size + 1);
};
if (error) {
return <div>{error}</div>;
}
const titles = data?.reduce(
(a, v) => [...a, ...v.results],
[] as MovieResult[]
);
const isEmpty = !isLoadingInitialData && titles?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.results.length < 20);
return (
<>
<Header>Happy Holidays!</Header>
<ListView
items={titles}
isEmpty={isEmpty}
isLoading={
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
}
isReachingEnd={isReachingEnd}
onScrollBottom={fetchMore}
/>
</>
);
};
export default Holiday;

View File

@@ -7,13 +7,11 @@ import type {
} from '../../../server/models/Search';
import TitleCard from '../TitleCard';
import PersonCard from '../PersonCard';
import { MediaRequest } from '../../../server/entity/MediaRequest';
import TmdbTitleCard from '../TitleCard/TmdbTitleCard';
import Slider from '../Slider';
import Link from 'next/link';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext';
import type Media from '../../../server/entity/Media';
import type { MediaResultsResponse } from '../../../server/interfaces/api/mediaInterfaces';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import RequestCard from '../RequestCard';
@@ -59,9 +57,18 @@ const Discover: React.FC = () => {
`/api/v1/discover/tv?language=${locale}`
);
const { data: movieUpcomingData, error: movieUpcomingError } = useSWR<
MovieDiscoverResult
>(`/api/v1/discover/movies/upcoming?language=${locale}`);
const {
data: movieUpcomingData,
error: movieUpcomingError,
} = useSWR<MovieDiscoverResult>(
`/api/v1/discover/movies/upcoming?language=${locale}`
);
const {
data: holUpcomingData,
error: holUpcomingError,
} = useSWR<MovieDiscoverResult>(
`/api/v1/discover/keyword/207317/movies?language=${locale}`
);
const { data: trendingData, error: trendingError } = useSWR<MixedResult>(
`/api/v1/discover/trending?language=${locale}`
@@ -71,9 +78,12 @@ const Discover: React.FC = () => {
'/api/v1/media?filter=available&take=20&sort=modified'
);
const { data: requests, error: requestError } = useSWR<
RequestResultsResponse
>('/api/v1/request?filter=unavailable&take=10&sort=modified&skip=0');
const {
data: requests,
error: requestError,
} = useSWR<RequestResultsResponse>(
'/api/v1/request?filter=unavailable&take=10&sort=modified&skip=0'
);
return (
<>
@@ -136,6 +146,57 @@ const Discover: React.FC = () => {
placeholder={<RequestCard.Placeholder />}
emptyMessage={intl.formatMessage(messages.nopending)}
/>
{/* Special Temporary Slider */}
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0">
<Link href="/discover/holiday">
<a className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<span>
<span role="img" aria-label="Christmas Tree" className="mr-2">
🎄
</span>
Happy Holidays!
<span role="img" aria-label="Christmas Tree" className="ml-2">
🎄
</span>
</span>
<svg
className="w-6 h-6 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 9l3 3m0 0l-3 3m3-3H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</a>
</Link>
</div>
</div>
<Slider
sliderKey="holiday"
isLoading={!holUpcomingData && !holUpcomingError}
isEmpty={false}
items={holUpcomingData?.results.map((title) => (
<TitleCard
key={`holiday-movie-slider-${title.id}`}
id={title.id}
image={title.posterPath}
status={title.mediaInfo?.status}
summary={title.overview}
title={title.title}
userScore={title.voteAverage}
year={title.releaseDate}
mediaType={title.mediaType}
/>
))}
/>
{/* End Special Temporary Slider */}
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0">
<Link href="/discover/movies/upcoming">
@@ -238,7 +299,11 @@ const Discover: React.FC = () => {
);
case 'person':
return (
<PersonCard name={title.name} profilePath={title.profilePath} />
<PersonCard
personId={title.id}
name={title.name}
profilePath={title.profilePath}
/>
);
}
})}

View File

@@ -0,0 +1,55 @@
import React from 'react';
import TmdbLogo from '../../assets/services/tmdb.svg';
import ImdbLogo from '../../assets/services/imdb.svg';
import RTLogo from '../../assets/services/rt.svg';
interface ExternalLinkBlockProps {
mediaType: 'movie' | 'tv';
imdbId?: string;
tmdbId?: number;
rtUrl?: string;
}
const ExternalLinkBlock: React.FC<ExternalLinkBlockProps> = ({
imdbId,
tmdbId,
rtUrl,
mediaType,
}) => {
return (
<div className="flex justify-end items-center">
{tmdbId && (
<a
href={`https://www.themoviedb.org/${mediaType}/${tmdbId}`}
className="w-8 mx-2 opacity-50 hover:opacity-100 transition duration-300"
target="_blank"
rel="noreferrer"
>
<TmdbLogo />
</a>
)}
{imdbId && (
<a
href={`https://www.imdb.com/title/${imdbId}`}
className="w-8 mx-2 opacity-50 hover:opacity-100 transition duration-300"
target="_blank"
rel="noreferrer"
>
<ImdbLogo />
</a>
)}
{rtUrl && (
<a
href={`${rtUrl}`}
className="w-14 mx-2 opacity-50 hover:opacity-100 transition duration-300"
target="_blank"
rel="noreferrer"
>
<RTLogo />
</a>
)}
</div>
);
};
export default ExternalLinkBlock;

View File

@@ -29,6 +29,42 @@ const availableLanguages: AvailableLanguageObject = {
code: 'fr',
display: 'Français',
},
'nb-NO': {
code: 'nb-NO',
display: 'Norwegian Bokmål',
},
de: {
code: 'de',
display: 'German',
},
ru: {
code: 'ru',
display: 'Russian',
},
nl: {
code: 'nl',
display: 'Nederlands',
},
es: {
code: 'es',
display: 'Spanish',
},
it: {
code: 'it',
display: 'Italian',
},
'pt-BR': {
code: 'pt-BR',
display: 'Portuguese (Brazil)',
},
sr: {
code: 'sr',
display: 'Serbian',
},
sv: {
code: 'sv',
display: 'Swedish',
},
};
const LanguagePicker: React.FC = () => {
@@ -38,7 +74,7 @@ const LanguagePicker: React.FC = () => {
useClickOutside(dropdownRef, () => setDropdownOpen(false));
return (
<div className="ml-3 relative">
<div className="relative">
<div>
<button
className="p-1 text-gray-400 rounded-full hover:bg-gray-500 hover:text-white focus:outline-none focus:ring focus:text-white"
@@ -61,10 +97,10 @@ const LanguagePicker: React.FC = () => {
</div>
<Transition
show={isDropdownOpen}
enter="transition ease-out duration-100"
enter="transition ease-out duration-100 opacity-0"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leave="transition ease-in duration-75 opacity-100"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>

View File

@@ -27,7 +27,7 @@ const SearchInput: React.FC = () => {
</div>
<input
id="search_field"
className="block w-full h-full pl-8 pr-3 py-2 rounded-md border-transparent focus:border-transparent bg-gray-600 text-white placeholder-gray-300 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
className="block w-full h-full pl-8 pr-1 py-2 rounded-md border-transparent focus:border-transparent bg-gray-600 text-white placeholder-gray-300 focus:outline-none focus:ring-0 focus:placeholder-gray-400 sm:text-base"
placeholder={intl.formatMessage(messages.searchPlaceholder)}
type="search"
value={searchValue}

View File

@@ -1,9 +1,10 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useRef } from 'react';
import Transition from '../../Transition';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { defineMessages, FormattedMessage } from 'react-intl';
import { useUser, Permission } from '../../../hooks/useUser';
import useClickOutside from '../../../hooks/useClickOutside';
const messages = defineMessages({
dashboard: 'Discover',
@@ -116,8 +117,10 @@ const SidebarLinks: SidebarLinkProps[] = [
];
const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
const navRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const { hasPermission } = useUser();
useClickOutside(navRef, () => setClosed());
return (
<>
<div className="md:hidden">
@@ -166,7 +169,10 @@ const Sidebar: React.FC<SidebarProps> = ({ open, setClosed }) => {
</svg>
</button>
</div>
<div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
<div
ref={navRef}
className="flex-1 h-0 pt-5 pb-4 overflow-y-auto"
>
<div className="flex-shrink-0 flex items-center px-4">
<span className="text-xl text-gray-50">
<Link href="/">

View File

@@ -2,10 +2,10 @@ import React, { useState } from 'react';
import SearchInput from './SearchInput';
import UserDropdown from './UserDropdown';
import Sidebar from './Sidebar';
import Notifications from './Notifications';
import LanguagePicker from './LanguagePicker';
import { useRouter } from 'next/router';
import { defineMessages, FormattedMessage } from 'react-intl';
import { Permission, useUser } from '../../hooks/useUser';
const messages = defineMessages({
alphawarning:
@@ -14,6 +14,7 @@ const messages = defineMessages({
const Layout: React.FC = ({ children }) => {
const [isSidebarOpen, setSidebarOpen] = useState(false);
const { hasPermission } = useUser();
const router = useRouter();
return (
@@ -43,9 +44,8 @@ const Layout: React.FC = ({ children }) => {
</button>
<div className="flex-1 px-4 flex justify-between">
<SearchInput />
<div className="ml-4 flex items-center md:ml-6">
<div className="flex items-center md:ml-6">
<LanguagePicker />
<Notifications />
<UserDropdown />
</div>
</div>
@@ -57,7 +57,7 @@ const Layout: React.FC = ({ children }) => {
>
<div className="pt-2 pb-6 md:py-6">
<div className="max-w-8xl mx-auto px-4 sm:px-6 md:px-8">
{router.pathname === '/' && (
{router.pathname === '/' && hasPermission(Permission.ADMIN) && (
<div className="rounded-md bg-indigo-700 p-4 mt-2">
<div className="flex">
<div className="flex-shrink-0">
@@ -85,7 +85,7 @@ const Layout: React.FC = ({ children }) => {
target="_blank"
rel="noreferrer"
>
Github &rarr;
GitHub &rarr;
</a>
</p>
</div>

View File

@@ -5,25 +5,36 @@ import axios from 'axios';
import { useRouter } from 'next/dist/client/router';
import ImageFader from '../Common/ImageFader';
import { defineMessages, FormattedMessage } from 'react-intl';
import Transition from '../Transition';
import LanguagePicker from '../Layout/LanguagePicker';
const messages = defineMessages({
signinplex: 'Sign in to continue',
});
const Login: React.FC = () => {
const [error, setError] = useState('');
const [isProcessing, setProcessing] = useState(false);
const [authToken, setAuthToken] = useState<string | undefined>(undefined);
const { user, revalidate } = useUser();
const router = useRouter();
// Effect that is triggered when the `authToken` comes back from the Plex OAuth
// We take the token and attempt to login. If we get a success message, we will
// ask swr to revalidate the user which _shouid_ come back with a valid user.
// ask swr to revalidate the user which _should_ come back with a valid user.
useEffect(() => {
const login = async () => {
const response = await axios.post('/api/v1/auth/login', { authToken });
setProcessing(true);
try {
const response = await axios.post('/api/v1/auth/login', { authToken });
if (response.data?.email) {
revalidate();
if (response.data?.email) {
revalidate();
}
} catch (e) {
setError(e.response.data.message);
setAuthToken(undefined);
setProcessing(false);
}
};
if (authToken) {
@@ -49,7 +60,10 @@ const Login: React.FC = () => {
'/images/rotate4.jpg',
]}
/>
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-md relative z-50">
<div className="absolute top-4 right-4 z-50">
<LanguagePicker />
</div>
<div className="px-4 sm:px-2 md:px-0 sm:mx-auto sm:w-full sm:max-w-md relative z-40">
<img
src="/logo.png"
className="mx-auto max-h-32 w-auto"
@@ -64,7 +78,40 @@ const Login: React.FC = () => {
className="bg-gray-800 bg-opacity-50 py-8 px-4 shadow sm:rounded-lg sm:px-10"
style={{ backdropFilter: 'blur(5px)' }}
>
<Transition
show={!!error}
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="opacity-100 transition duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="rounded-md bg-red-600 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-red-300"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-300">{error}</h3>
</div>
</div>
</div>
</Transition>
<PlexLoginButton
isProcessing={isProcessing}
onAuthToken={(authToken) => setAuthToken(authToken)}
/>
</div>

View File

@@ -0,0 +1,66 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useContext } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import { MovieDetails } from '../../../../server/models/Movie';
import { LanguageContext } from '../../../context/LanguageContext';
import Error from '../../../pages/_error';
import Header from '../../Common/Header';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PersonCard from '../../PersonCard';
const messages = defineMessages({
fullcast: 'Full Cast',
});
const MovieCast: React.FC = () => {
const router = useRouter();
const intl = useIntl();
const { locale } = useContext(LanguageContext);
const { data, error } = useSWR<MovieDetails>(
`/api/v1/movie/${router.query.movieId}?language=${locale}`
);
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <Error statusCode={404} />;
}
return (
<>
<Header
subtext={
<Link href={`/movie/${data.id}`}>
<a className="hover:underline">{data.title}</a>
</Link>
}
>
{intl.formatMessage(messages.fullcast)}
</Header>
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8">
{data?.credits.cast.map((person) => {
return (
<li
key={person.id}
className="col-span-1 flex flex-col text-center items-center"
>
<PersonCard
name={person.name}
personId={person.id}
subName={person.character}
profilePath={person.profilePath}
canExpand
/>
</li>
);
})}
</ul>
</>
);
};
export default MovieCast;

View File

@@ -37,6 +37,7 @@ import type { RTRating } from '../../../server/api/rottentomatoes';
import Error from '../../pages/_error';
import Head from 'next/head';
import globalMessages from '../../i18n/globalMessages';
import ExternalLinkBlock from '../ExternalLinkBlock';
const messages = defineMessages({
releasedate: 'Release Date',
@@ -65,6 +66,7 @@ const messages = defineMessages({
'This will remove all media data including all requests for this item. This action is irreversible. If this item exists in your Plex library, the media information will be recreated next sync.',
approve: 'Approve',
decline: 'Decline',
studio: 'Studio',
});
interface MovieDetailsProps {
@@ -193,14 +195,14 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
)}
</SlideOver>
<div className="flex flex-col items-center md:flex-row md:items-end pt-4">
<div className="mr-4 flex-shrink-0">
<div className="md:mr-4 flex-shrink-0">
<img
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`}
alt=""
className="rounded md:rounded-lg shadow md:shadow-2xl w-32 md:w-52"
/>
</div>
<div className="text-white flex flex-col mr-4 mt-4 md:mt-0 text-center md:text-left">
<div className="text-white flex flex-col md:mr-4 mt-4 md:mt-0 text-center md:text-left">
<div className="mb-2">
{data.mediaInfo?.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">
@@ -352,7 +354,7 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
{hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="default"
className="ml-2"
className="ml-2 first:ml-0"
onClick={() => setShowManager(true)}
>
<svg
@@ -395,34 +397,36 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
<div className="bg-gray-900 rounded-lg shadow border border-gray-800">
{(data.voteCount > 0 || ratingData) && (
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0 items-center justify-center">
{ratingData?.criticsRating && (
<>
<span className="text-sm">
{ratingData.criticsRating === 'Rotten' ? (
<RTRotten className="w-6 mr-1" />
) : (
<RTFresh className="w-6 mr-1" />
)}
</span>
<span className="text-gray-400 text-sm mr-4 last:mr-0">
{ratingData.criticsScore}%
</span>
</>
)}
{ratingData?.audienceRating && (
<>
<span className="text-sm">
{ratingData.audienceRating === 'Spilled' ? (
<RTAudRotten className="w-6 mr-1" />
) : (
<RTAudFresh className="w-6 mr-1" />
)}
</span>
<span className="text-gray-400 text-sm mr-4 last:mr-0">
{ratingData.audienceScore}%
</span>
</>
)}
{ratingData?.criticsRating &&
(ratingData?.criticsScore ?? 0) > 0 && (
<>
<span className="text-sm">
{ratingData.criticsRating === 'Rotten' ? (
<RTRotten className="w-6 mr-1" />
) : (
<RTFresh className="w-6 mr-1" />
)}
</span>
<span className="text-gray-400 text-sm mr-4 last:mr-0">
{ratingData.criticsScore}%
</span>
</>
)}
{ratingData?.audienceRating &&
(ratingData?.audienceScore ?? 0) > 0 && (
<>
<span className="text-sm">
{ratingData.audienceRating === 'Spilled' ? (
<RTAudRotten className="w-6 mr-1" />
) : (
<RTAudFresh className="w-6 mr-1" />
)}
</span>
<span className="text-gray-400 text-sm mr-4 last:mr-0">
{ratingData.audienceScore}%
</span>
</>
)}
{data.voteCount > 0 && (
<>
<span className="text-sm">
@@ -484,14 +488,40 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</span>
</div>
)}
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.originallanguage} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{data.originalLanguage}
</span>
</div>
{data.spokenLanguages.some(
(lng) => lng.iso_639_1 === data.originalLanguage
) && (
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.originallanguage} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{
data.spokenLanguages.find(
(lng) => lng.iso_639_1 === data.originalLanguage
)?.name
}
</span>
</div>
)}
{data.productionCompanies[0] && (
<div className="flex px-4 py-2 border-b border-gray-800 last:border-b-0">
<span className="text-sm">
<FormattedMessage {...messages.studio} />
</span>
<span className="flex-1 text-right text-gray-400 text-sm">
{data.productionCompanies[0]?.name}
</span>
</div>
)}
</div>
<div className="mt-4">
<ExternalLinkBlock
mediaType="movie"
tmdbId={data.id}
imdbId={data.externalIds.imdbId}
rtUrl={ratingData?.url}
/>
</div>
</div>
</div>
@@ -522,11 +552,12 @@ const MovieDetails: React.FC<MovieDetailsProps> = ({ movie }) => {
</div>
<Slider
sliderKey="cast"
isLoading={!data && !error}
isLoading={false}
isEmpty={false}
items={data?.credits.cast.slice(0, 20).map((person) => (
<PersonCard
key={`cast-item-${person.id}`}
personId={person.id}
name={person.name}
subName={person.character}
profilePath={person.profilePath}

View File

@@ -1,6 +1,8 @@
import Link from 'next/link';
import React from 'react';
interface PersonCardProps {
personId: number;
name: string;
subName?: string;
profilePath?: string;
@@ -8,50 +10,55 @@ interface PersonCardProps {
}
const PersonCard: React.FC<PersonCardProps> = ({
personId,
name,
subName,
profilePath,
canExpand = false,
}) => {
return (
<div
className={`relative ${
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
} bg-gray-600 rounded-lg text-white shadow-lg hover:bg-gray-500 transition ease-in-out duration-150 cursor-pointer`}
>
<div style={{ paddingBottom: '150%' }}>
<div className="absolute inset-0 flex flex-col items-center justify-center">
{profilePath && (
<div
style={{
backgroundImage: `url(https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath})`,
}}
className="rounded-full w-28 h-28 md:w-32 md:h-32 bg-cover bg-center mb-6"
/>
)}
{!profilePath && (
<svg
className="w-28 h-28 md:w-32 md:h-32 mb-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
clipRule="evenodd"
/>
</svg>
)}
<div className="whitespace-normal text-center">{name}</div>
{subName && (
<div className="whitespace-normal text-center text-sm text-gray-300">
{subName}
<Link href={`/person/${personId}`}>
<a className={canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'}>
<div
className={`relative ${
canExpand ? 'w-full' : 'w-36 sm:w-36 md:w-44'
} bg-gray-600 rounded-lg text-white shadow-lg hover:bg-gray-500 transition ease-in-out duration-150 cursor-pointer`}
>
<div style={{ paddingBottom: '150%' }}>
<div className="absolute inset-0 flex flex-col items-center justify-center">
{profilePath && (
<div
style={{
backgroundImage: `url(https://image.tmdb.org/t/p/w600_and_h900_bestv2${profilePath})`,
}}
className="rounded-full w-28 h-28 md:w-32 md:h-32 bg-cover bg-center mb-6"
/>
)}
{!profilePath && (
<svg
className="w-28 h-28 md:w-32 md:h-32 mb-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z"
clipRule="evenodd"
/>
</svg>
)}
<div className="whitespace-normal text-center">{name}</div>
{subName && (
<div className="whitespace-normal text-center text-sm text-gray-300">
{subName}
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
</a>
</Link>
);
};

View File

@@ -0,0 +1,131 @@
import { useRouter } from 'next/router';
import React, { useContext } from 'react';
import useSWR from 'swr';
import type { PersonDetail } from '../../../server/models/Person';
import type { PersonCombinedCreditsResponse } from '../../../server/interfaces/api/personInterfaces';
import Error from '../../pages/_error';
import LoadingSpinner from '../Common/LoadingSpinner';
import TitleCard from '../TitleCard';
import { defineMessages, useIntl } from 'react-intl';
import { LanguageContext } from '../../context/LanguageContext';
const messages = defineMessages({
appearsin: 'Appears in',
ascharacter: 'as {character}',
nobiography: 'No biography available.',
});
const PersonDetails: React.FC = () => {
const intl = useIntl();
const { locale } = useContext(LanguageContext);
const router = useRouter();
const { data, error } = useSWR<PersonDetail>(
`/api/v1/person/${router.query.personId}`
);
const {
data: combinedCredits,
error: errorCombinedCredits,
} = useSWR<PersonCombinedCreditsResponse>(
`/api/v1/person/${router.query.personId}/combined_credits?language=${locale}`
);
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <Error statusCode={404} />;
}
const sortedCast = combinedCredits?.cast.sort((a, b) => {
const aDate =
a.mediaType === 'movie'
? a.releaseDate?.slice(0, 4) ?? 0
: a.firstAirDate?.slice(0, 4) ?? 0;
const bDate =
b.mediaType === 'movie'
? b.releaseDate?.slice(0, 4) ?? 0
: b.firstAirDate?.slice(0, 4) ?? 0;
if (aDate > bDate) {
return -1;
}
return 1;
});
const isLoading = !combinedCredits && !errorCombinedCredits;
return (
<>
<div className="flex mt-8 mb-8 flex-col md:flex-row items-center md:items-start">
{data.profilePath && (
<div
style={{
backgroundImage: `url(https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.profilePath})`,
}}
className="rounded-full w-36 h-36 md:w-44 md:h-44 bg-cover bg-center mb-6 md:mb-0 mr-0 md:mr-6 flex-shrink-0"
/>
)}
<div className="text-gray-300 text-center md:text-left">
<h1 className="text-3xl md:text-4xl text-white mb-4">{data.name}</h1>
<div>
{data.biography
? data.biography
: intl.formatMessage(messages.nobiography)}
</div>
</div>
</div>
<div className="md:flex md:items-center md:justify-between mb-4 mt-6">
<div className="flex-1 min-w-0">
<div className="inline-flex text-xl leading-7 text-gray-300 hover:text-white sm:text-2xl sm:leading-9 sm:truncate items-center">
<span>{intl.formatMessage(messages.appearsin)}</span>
</div>
</div>
</div>
<ul className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-8">
{sortedCast?.map((media) => {
return (
<li
key={`list-cast-item-${media.id}`}
className="col-span-1 flex flex-col text-center items-center"
>
<TitleCard
id={media.id}
title={media.mediaType === 'movie' ? media.title : media.name}
userScore={media.voteAverage}
year={
media.mediaType === 'movie'
? media.releaseDate
: media.firstAirDate
}
image={media.posterPath}
summary={media.overview}
mediaType={media.mediaType as 'movie' | 'tv'}
status={media.mediaInfo?.status}
canExpand
/>
{media.character && (
<div className="mt-2 text-gray-300 text-xs truncate w-36 sm:w-36 md:w-44 text-center">
{intl.formatMessage(messages.ascharacter, {
character: media.character,
})}
</div>
)}
</li>
);
})}
{isLoading &&
[...Array(20)].map((_item, i) => (
<li
key={`placeholder-${i}`}
className="col-span-1 flex flex-col text-center items-center"
>
<TitleCard.Placeholder canExpand />
</li>
))}
</ul>
</>
);
};
export default PersonDetails;

View File

@@ -12,23 +12,23 @@ const plexOAuth = new PlexOAuth();
interface PlexLoginButtonProps {
onAuthToken: (authToken: string) => void;
isProcessing?: boolean;
onError?: (message: string) => void;
}
const PlexLoginButton: React.FC<PlexLoginButtonProps> = ({
onAuthToken,
onError,
isProcessing,
}) => {
const intl = useIntl();
const [loading, setLoading] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const getPlexLogin = async () => {
setLoading(true);
try {
const authToken = await plexOAuth.login();
setLoading(false);
setIsProcessing(true);
onAuthToken(authToken);
} catch (e) {
if (onError) {

View File

@@ -43,26 +43,30 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
return (
<div className="block">
<div className="px-4 py-4 sm:px-6 ">
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<div className="mr-6 flex items-center text-sm leading-5 text-gray-300">
<svg
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg>
<span>{request.requestedBy.username}</span>
<div className="mr-6 flex-col items-center text-sm leading-5 text-gray-300 flex-1 min-w-0">
<div className="flex flex-nowrap mb-1 white">
<svg
className="min-w-0 flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clipRule="evenodd"
/>
</svg>
<span className="truncate w-40 md:w-auto">
{request.requestedBy.username}
</span>
</div>
{request.modifiedBy && (
<>
<div className="flex flex-nowrap">
<svg
className="flex-shrink-0 ml-2 mr-1.5 h-5 w-5 text-gray-300"
className="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
@@ -74,8 +78,10 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
clipRule="evenodd"
/>
</svg>
<span>{request.modifiedBy?.username}</span>
</>
<span className="truncate w-40 md:w-auto">
{request.modifiedBy?.username}
</span>
</div>
)}
</div>
<div className="ml-2 flex-shrink-0 flex">
@@ -189,13 +195,18 @@ const RequestBlock: React.FC<RequestBlockProps> = ({ request, onUpdate }) => {
</div>
</div>
{(request.seasons ?? []).length > 0 && (
<div className="mt-2 text-sm flex items-center">
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
<div className="mt-2 text-sm flex flex-col">
<div className="mb-2">{intl.formatMessage(messages.seasons)}</div>
<div>
{request.seasons.map((season) => (
<span
key={`season-${season.id}`}
className="mr-2 mb-1 inline-block"
>
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
</div>
)}
</div>

View File

@@ -1,13 +1,11 @@
import React, { useContext, useState } from 'react';
import React, { useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { TvDetails } from '../../../server/models/Tv';
import type { MovieDetails } from '../../../server/models/Movie';
import useSWR from 'swr';
import { LanguageContext } from '../../context/LanguageContext';
import {
MediaStatus,
MediaRequestStatus,
} from '../../../server/constants/media';
import { MediaRequestStatus } from '../../../server/constants/media';
import Badge from '../Common/Badge';
import { useUser, Permission } from '../../hooks/useUser';
import axios from 'axios';
@@ -16,10 +14,12 @@ import { withProperties } from '../../utils/typeHelpers';
import Link from 'next/link';
import { defineMessages, useIntl } from 'react-intl';
import globalMessages from '../../i18n/globalMessages';
import StatusBadge from '../StatusBadge';
const messages = defineMessages({
requestedby: 'Requested by {username}',
seasons: 'Seasons',
all: 'All',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
@@ -41,6 +41,9 @@ interface RequestCardProps {
}
const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
const { ref, inView } = useInView({
triggerOnce: true,
});
const intl = useIntl();
const { hasPermission } = useUser();
const { locale } = useContext(LanguageContext);
@@ -49,11 +52,13 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`;
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
`${url}?language=${locale}`
inView ? `${url}?language=${locale}` : null
);
const { data: requestData, error: requestError, revalidate } = useSWR<
MediaRequest
>(`/api/v1/request/${request.id}`, {
const {
data: requestData,
error: requestError,
revalidate,
} = useSWR<MediaRequest>(`/api/v1/request/${request.id}`, {
initialData: request,
});
@@ -66,7 +71,11 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
};
if (!title && !error) {
return <RequestCardPlaceholder />;
return (
<div ref={ref}>
<RequestCardPlaceholder />
</div>
);
}
if (!requestData && !requestError) {
@@ -97,41 +106,34 @@ const RequestCard: React.FC<RequestCardProps> = ({ request }) => {
{isMovie(title) ? title.title : title.name}
</Link>
</h2>
<div className="text-xs sm:text-sm">
<div className="text-xs sm:text-sm truncate">
{intl.formatMessage(messages.requestedby, {
username: requestData.requestedBy.username,
})}
</div>
<div className="mt-1 sm:mt-2">
{requestData.media.status === MediaStatus.AVAILABLE && (
<Badge badgeType="success">
{intl.formatMessage(globalMessages.available)}
</Badge>
)}
{requestData.media.status === MediaStatus.PARTIALLY_AVAILABLE && (
<Badge badgeType="success">
{intl.formatMessage(globalMessages.partiallyavailable)}
</Badge>
)}
{requestData.media.status === MediaStatus.PROCESSING && (
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.unavailable)}
</Badge>
)}
{requestData.media.status === MediaStatus.PENDING && (
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.pending)}
</Badge>
)}
</div>
{requestData.media.status && (
<div className="mt-1 sm:mt-2">
<StatusBadge status={requestData.media.status} />
</div>
)}
{request.seasons.length > 0 && (
<div className="hidden mt-2 text-sm sm:flex items-center">
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
{!isMovie(title) &&
title.seasons.filter((season) => season.seasonNumber !== 0)
.length === request.seasons.length ? (
<span className="mr-2 uppercase">
<Badge>{intl.formatMessage(messages.all)}</Badge>
</span>
))}
) : (
<div className="hide-scrollbar overflow-x-scroll">
{request.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
</div>
)}
{requestData.status === MediaRequestStatus.PENDING &&

View File

@@ -0,0 +1,250 @@
import React, { useContext } from 'react';
import { useInView } from 'react-intersection-observer';
import type { MediaRequest } from '../../../../server/entity/MediaRequest';
import {
useIntl,
FormattedDate,
FormattedRelativeTime,
defineMessages,
} from 'react-intl';
import { useUser, Permission } from '../../../hooks/useUser';
import { LanguageContext } from '../../../context/LanguageContext';
import type { MovieDetails } from '../../../../server/models/Movie';
import type { TvDetails } from '../../../../server/models/Tv';
import useSWR from 'swr';
import Badge from '../../Common/Badge';
import StatusBadge from '../../StatusBadge';
import Table from '../../Common/Table';
import { MediaRequestStatus } from '../../../../server/constants/media';
import Button from '../../Common/Button';
import axios from 'axios';
import globalMessages from '../../../i18n/globalMessages';
import Link from 'next/link';
const messages = defineMessages({
requestedby: 'Requested by {username}',
seasons: 'Seasons',
notavailable: 'N/A',
});
const isMovie = (movie: MovieDetails | TvDetails): movie is MovieDetails => {
return (movie as MovieDetails).title !== undefined;
};
interface RequestItemProps {
request: MediaRequest;
onDelete: () => void;
}
const RequestItem: React.FC<RequestItemProps> = ({ request, onDelete }) => {
const { ref, inView } = useInView({
triggerOnce: true,
});
const intl = useIntl();
const { hasPermission } = useUser();
const { locale } = useContext(LanguageContext);
const url =
request.type === 'movie'
? `/api/v1/movie/${request.media.tmdbId}`
: `/api/v1/tv/${request.media.tmdbId}`;
const { data: title, error } = useSWR<MovieDetails | TvDetails>(
inView ? `${url}?language=${locale}` : null
);
const { data: requestData, revalidate } = useSWR<MediaRequest>(
`/api/v1/request/${request.id}`,
{
initialData: request,
}
);
const modifyRequest = async (type: 'approve' | 'decline') => {
const response = await axios.get(`/api/v1/request/${request.id}/${type}`);
if (response) {
revalidate();
}
};
const deleteRequest = async () => {
await axios.delete(`/api/v1/request/${request.id}`);
onDelete();
};
if (!title && !error) {
return (
<tr className="w-full bg-gray-800 animate-pulse h-24" ref={ref}>
<td colSpan={6}></td>
</tr>
);
}
if (!title || !requestData) {
return (
<tr className="w-full bg-gray-800 animate-pulse h-24">
<td colSpan={6}></td>
</tr>
);
}
return (
<tr className="w-full bg-gray-800 h-24 p-2 relative text-white">
<Table.TD
noPadding
className="w-20 px-4 relative hidden sm:table-cell align-middle"
>
<Link
href={
request.type === 'movie'
? `/movie/${request.media.tmdbId}`
: `/tv/${request.media.tmdbId}`
}
>
<a>
<img
src={`//image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`}
alt=""
className="rounded-md shadow-sm cursor-pointer transition transform-gpu duration-300 scale-100 hover:scale-105 hover:shadow-md"
/>
</a>
</Link>
</Table.TD>
<Table.TD>
<Link
href={
requestData.type === 'movie'
? `/movie/${requestData.media.tmdbId}`
: `/tv/${requestData.media.tmdbId}`
}
>
<a className="text-white text-xl mr-2 hover:underline">
{isMovie(title) ? title.title : title.name}
</a>
</Link>
<div className="text-sm">
{intl.formatMessage(messages.requestedby, {
username: requestData.requestedBy.username,
})}
</div>
{requestData.seasons.length > 0 && (
<div className="hidden mt-2 text-sm sm:flex items-center">
<span className="mr-2">{intl.formatMessage(messages.seasons)}</span>
{requestData.seasons.map((season) => (
<span key={`season-${season.id}`} className="mr-2">
<Badge>{season.seasonNumber}</Badge>
</span>
))}
</div>
)}
</Table.TD>
<Table.TD>
<StatusBadge status={requestData.media.status} />
</Table.TD>
<Table.TD>
<div className="flex flex-col">
<span className="text-sm text-gray-300">
<FormattedDate value={requestData.createdAt} />
</span>
</div>
</Table.TD>
<Table.TD>
<div className="flex flex-col">
{requestData.modifiedBy ? (
<span className="text-sm text-gray-300">
{requestData.modifiedBy.username} (
<FormattedRelativeTime
value={Math.floor(
(new Date(requestData.updatedAt).getTime() - Date.now()) /
1000
)}
updateIntervalInSeconds={1}
/>
)
</span>
) : (
<span className="text-sm text-gray-300">N/A</span>
)}
</div>
</Table.TD>
<Table.TD alignText="right">
{requestData.status !== MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => deleteRequest()}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.delete)}
</span>
</Button>
)}
{requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && (
<>
<span className="mr-2">
<Button
buttonType="success"
buttonSize="sm"
onClick={() => modifyRequest('approve')}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.approve)}
</span>
</Button>
</span>
<span>
<Button
buttonType="danger"
buttonSize="sm"
onClick={() => modifyRequest('decline')}
>
<svg
className="w-4 h-4 mr-0 sm:mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<span className="hidden sm:block">
{intl.formatMessage(globalMessages.decline)}
</span>
</Button>
</span>
</>
)}
</Table.TD>
</tr>
);
};
export default RequestItem;

View File

@@ -0,0 +1,109 @@
import React, { useState } from 'react';
import useSWR from 'swr';
import type { RequestResultsResponse } from '../../../server/interfaces/api/requestInterfaces';
import LoadingSpinner from '../Common/LoadingSpinner';
import RequestItem from './RequestItem';
import Header from '../Common/Header';
import Table from '../Common/Table';
import Button from '../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
requests: 'Requests',
mediaInfo: 'Media Info',
status: 'Status',
requestedAt: 'Requested At',
modifiedBy: 'Last Modified By',
showingresults:
'Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results',
next: 'Next',
previous: 'Previous',
});
const RequestList: React.FC = () => {
const intl = useIntl();
const [pageIndex, setPageIndex] = useState(0);
const { data, error, revalidate } = useSWR<RequestResultsResponse>(
`/api/v1/request?take=10&skip=${pageIndex * 10}`
);
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <LoadingSpinner />;
}
const hasNextPage = data.pageInfo.pages > pageIndex + 1;
const hasPrevPage = pageIndex > 0;
return (
<>
<Header>{intl.formatMessage(messages.requests)}</Header>
<Table>
<thead>
<Table.TH className="hidden sm:table-cell"></Table.TH>
<Table.TH>{intl.formatMessage(messages.mediaInfo)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.status)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.requestedAt)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.modifiedBy)}</Table.TH>
<Table.TH></Table.TH>
</thead>
<Table.TBody>
{data.results.map((request) => {
return (
<RequestItem
request={request}
key={`request-list-${request.id}`}
onDelete={() => revalidate()}
/>
);
})}
<tr>
<Table.TD colSpan={6} noPadding>
<nav
className="bg-gray-700 px-4 py-3 flex items-center justify-between text-white sm:px-6"
aria-label="Pagination"
>
<div className="hidden sm:block">
<p className="text-sm">
{intl.formatMessage(messages.showingresults, {
from: pageIndex * 10,
to:
data.results.length < 10
? pageIndex * 10 + data.results.length
: pageIndex + 1 * 10,
total: data.pageInfo.results,
strong: function strong(msg) {
return <span className="font-medium">{msg}</span>;
},
})}
</p>
</div>
<div className="flex-1 flex justify-start sm:justify-end">
<span className="mr-2">
<Button
disabled={!hasPrevPage}
onClick={() => setPageIndex((current) => current - 1)}
>
{intl.formatMessage(messages.previous)}
</Button>
</span>
<Button
disabled={!hasNextPage}
onClick={() => setPageIndex((current) => current + 1)}
>
{intl.formatMessage(messages.next)}
</Button>
</div>
</nav>
</Table.TD>
</tr>
</Table.TBody>
</Table>
</>
);
};
export default RequestList;

View File

@@ -43,7 +43,6 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
onComplete,
tmdbId,
onUpdating,
...props
}) => {
const [isUpdating, setIsUpdating] = useState(false);
const { addToast } = useToasts();
@@ -140,7 +139,6 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
okButtonType={'danger'}
cancelText={intl.formatMessage(messages.close)}
iconSvg={<DownloadIcon className="w-6 h-6" />}
{...props}
>
{intl.formatMessage(messages.requestfrom, {
username: activeRequest.requestedBy.username,
@@ -164,7 +162,6 @@ const MovieRequestModal: React.FC<RequestModalProps> = ({
}
okButtonType={'primary'}
iconSvg={<DownloadIcon className="w-6 h-6" />}
{...props}
>
{text}
</Modal>

View File

@@ -11,10 +11,10 @@ import {
MediaStatus,
MediaRequestStatus,
} from '../../../server/constants/media';
import { TvDetails, SeasonWithEpisodes } from '../../../server/models/Tv';
import type SeasonRequest from '../../../server/entity/SeasonRequest';
import { TvDetails } from '../../../server/models/Tv';
import Badge from '../Common/Badge';
import globalMessages from '../../i18n/globalMessages';
import SeasonRequest from '../../../server/entity/SeasonRequest';
const messages = defineMessages({
requestadmin: 'Your request will be immediately approved.',
@@ -47,7 +47,6 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
onComplete,
tmdbId,
onUpdating,
...props
}) => {
const { addToast } = useToasts();
const { data, error } = useSWR<TvDetails>(`/api/v1/tv/${tmdbId}`);
@@ -222,7 +221,6 @@ const TvRequestModal: React.FC<RequestModalProps> = ({
/>
</svg>
}
{...props}
>
<div className="flex flex-col">
<div className="-mx-4 sm:mx-0 overflow-auto max-h-96">

View File

@@ -1,10 +1,8 @@
import React from 'react';
import useSWR from 'swr';
import MovieRequestModal from './MovieRequestModal';
import type { MediaRequest } from '../../../server/entity/MediaRequest';
import type { MediaStatus } from '../../../server/constants/media';
import TvRequestModal from './TvRequestModal';
import { useTransition, animated } from 'react-spring';
import Transition from '../Transition';
interface RequestModalProps {
show: boolean;
@@ -21,53 +19,47 @@ const RequestModal: React.FC<RequestModalProps> = ({
show,
tmdbId,
onComplete,
onError,
onUpdating,
onCancel,
}) => {
const transitions = useTransition(show, null, {
from: { opacity: 0, backdropFilter: 'blur(0px)' },
enter: { opacity: 1, backdropFilter: 'blur(3px)' },
leave: { opacity: 0, backdropFilter: 'blur(0px)' },
config: { tension: 500, velocity: 40, friction: 60 },
});
if (type === 'tv') {
return (
<>
{transitions.map(
({ props, item, key }) =>
item && (
<TvRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
style={props}
key={key}
/>
)
)}
</>
<Transition
enter="transition opacity-0 duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={show}
>
<TvRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
/>
</Transition>
);
}
return (
<>
{transitions.map(
({ props, item, key }) =>
item && (
<MovieRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
style={props}
key={key}
/>
)
)}
</>
<Transition
enter="transition opacity-0 duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition opacity-100 duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={show}
>
<MovieRequestModal
onComplete={onComplete}
onCancel={onCancel}
tmdbId={tmdbId}
onUpdating={onUpdating}
/>
</Transition>
);
};

View File

@@ -10,7 +10,6 @@ import ListView from '../Common/ListView';
import { LanguageContext } from '../../context/LanguageContext';
import { defineMessages, useIntl } from 'react-intl';
import Header from '../Common/Header';
import globalMessages from '../../i18n/globalMessages';
const messages = defineMessages({
searchresults: 'Search Results',
@@ -66,25 +65,7 @@ const Search: React.FC = () => {
return (
<>
<div className="md:flex md:items-center md:justify-between mb-6 sm:mb-0 mt-6">
<Header>{intl.formatMessage(messages.searchresults)}</Header>
<div className="mt-4 flex md:mt-0 md:ml-4">
<span className="relative z-0 inline-flex shadow-sm rounded-md">
<button
type="button"
className="relative inline-flex items-center px-4 py-2 rounded-l-md border border-indigo-900 bg-indigo-500 hover:bg-indigo-400 text-sm leading-5 font-medium text-gray-100 hover:text-white focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
{intl.formatMessage(globalMessages.movies)}
</button>
<button
type="button"
className="-ml-px relative inline-flex items-center px-4 py-2 rounded-r-md border border-indigo-900 bg-indigo-500 text-sm leading-5 font-medium text-gray-100 hover:text-white focus:z-10 focus:outline-none focus:border-blue-300 focus:ring-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
{intl.formatMessage(globalMessages.tvshows)}
</button>
</span>
</div>
</div>
<Header>{intl.formatMessage(messages.searchresults)}</Header>
<ListView
items={titles}
isEmpty={isEmpty}

View File

@@ -25,7 +25,10 @@ const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
return (
<button
onClick={setCopied}
onClick={(e) => {
e.preventDefault();
setCopied();
}}
className="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-500 text-sm leading-5 font-medium text-white bg-indigo-500 hover:bg-indigo-400 focus:outline-none focus:ring-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150"
>
<svg

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { Field, Form, Formik } from 'formik';
import useSWR from 'swr';
import LoadingSpinner from '../../Common/LoadingSpinner';
@@ -6,6 +6,7 @@ import Button from '../../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import Axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
const messages = defineMessages({
save: 'Save Changes',
@@ -14,10 +15,13 @@ const messages = defineMessages({
webhookUrl: 'Webhook URL',
validationWebhookUrlRequired: 'You must provide a webhook URL',
webhookUrlPlaceholder: 'Server Settings -> Integrations -> Webhooks',
discordsettingssaved: 'Discord notification settings saved!',
discordsettingsfailed: 'Discord notification settings failed to save.',
});
const NotificationsDiscord: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/discord'
);
@@ -49,8 +53,15 @@ const NotificationsDiscord: React.FC = () => {
webhookUrl: values.webhookUrl,
},
});
addToast(intl.formatMessage(messages.discordsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
// TODO show error
addToast(intl.formatMessage(messages.discordsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { Field, Form, Formik } from 'formik';
import useSWR from 'swr';
import LoadingSpinner from '../../Common/LoadingSpinner';
@@ -6,6 +6,7 @@ import Button from '../../Common/Button';
import { defineMessages, useIntl } from 'react-intl';
import Axios from 'axios';
import * as Yup from 'yup';
import { useToasts } from 'react-toast-notifications';
const messages = defineMessages({
save: 'Save Changes',
@@ -20,10 +21,13 @@ const messages = defineMessages({
enableSsl: 'Enable SSL',
authUser: 'Auth User',
authPass: 'Auth Pass',
emailsettingssaved: 'Email notification settings saved!',
emailsettingsfailed: 'Email notification settings failed to save.',
});
const NotificationsEmail: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR(
'/api/v1/settings/notifications/email'
);
@@ -65,14 +69,21 @@ const NotificationsEmail: React.FC = () => {
options: {
emailFrom: values.emailFrom,
smtpHost: values.smtpHost,
smtpPort: values.smtpPort,
smtpPort: Number(values.smtpPort),
secure: values.secure,
authUser: values.authUser,
authPass: values.authPass,
},
});
addToast(intl.formatMessage(messages.emailsettingssaved), {
appearance: 'success',
autoDismiss: true,
});
} catch (e) {
// TODO show error
addToast(intl.formatMessage(messages.emailsettingsfailed), {
appearance: 'error',
autoDismiss: true,
});
} finally {
revalidate();
}
@@ -86,7 +97,7 @@ const NotificationsEmail: React.FC = () => {
htmlFor="isDefault"
className="block text-sm font-medium leading-5 text-gray-400 sm:mt-px sm:pt-2"
>
Agent Enabled
{intl.formatMessage(messages.agentenabled)}
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Field

View File

@@ -11,11 +11,13 @@ import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
createradarr: 'Create New Radarr Server',
editradarr: 'Edit Radarr Server',
validationNameRequired: 'You must provide a server name',
validationHostnameRequired: 'You must provide a hostname/IP',
validationPortRequired: 'You must provide a port',
validationApiKeyRequired: 'You must provide an API key',
validationRootFolderRequired: 'You must select a root folder',
validationProfileRequired: 'You must select a profile',
validationMinimumAvailabilityRequired: 'You must select minimum availability',
toastRadarrTestSuccess: 'Radarr connection established!',
toastRadarrTestFailure: 'Failed to connect to Radarr Server',
saving: 'Saving...',
@@ -40,6 +42,10 @@ const messages = defineMessages({
selectQualityProfile: 'Select a Quality Profile',
selectRootFolder: 'Select a Root Folder',
selectMinimumAvailability: 'Select minimum availability',
loadingprofiles: 'Loading quality profiles…',
testFirstQualityProfiles: 'Test your connection to load quality profiles',
loadingrootfolders: 'Loading root folders…',
testFirstRootFolders: 'Test your connection to load root folders',
});
interface TestResponse {
@@ -74,6 +80,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
rootFolders: [],
});
const RadarrSettingsSchema = Yup.object().shape({
name: Yup.string().required(
intl.formatMessage(messages.validationNameRequired)
),
hostname: Yup.string().required(
intl.formatMessage(messages.validationHostnameRequired)
),
@@ -81,10 +90,15 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
intl.formatMessage(messages.validationPortRequired)
),
apiKey: Yup.string().required(intl.formatMessage(messages.apiKey)),
rootFolder: Yup.string().required(intl.formatMessage(messages.rootfolder)),
rootFolder: Yup.string().required(
intl.formatMessage(messages.validationRootFolderRequired)
),
activeProfileId: Yup.string().required(
intl.formatMessage(messages.validationProfileRequired)
),
minimumAvailability: Yup.string().required(
intl.formatMessage(messages.validationMinimumAvailabilityRequired)
),
});
const testConnection = useCallback(
@@ -108,7 +122,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
{
hostname,
apiKey,
port,
port: Number(port),
baseUrl,
useSsl,
}
@@ -171,7 +185,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
baseUrl: radarr?.baseUrl,
activeProfileId: radarr?.activeProfileId,
rootFolder: radarr?.activeDirectory,
minimumAvailability: radarr?.minimumAvailability,
minimumAvailability: radarr?.minimumAvailability ?? 'released',
isDefault: radarr?.isDefault ?? false,
is4k: radarr?.is4k ?? false,
}}
@@ -185,11 +199,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
const submission = {
name: values.name,
hostname: values.hostname,
port: values.port,
port: Number(values.port),
apiKey: values.apiKey,
useSsl: values.ssl,
baseUrl: values.baseUrl,
activeProfileId: values.activeProfileId,
activeProfileId: Number(values.activeProfileId),
activeProfileName: profileName,
activeDirectory: values.rootFolder,
is4k: values.is4k,
@@ -218,6 +232,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
handleSubmit,
setFieldValue,
isSubmitting,
isValid,
}) => {
return (
<Modal
@@ -226,7 +241,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
okText={
isSubmitting
? intl.formatMessage(messages.saving)
: !!radarr
: radarr
? intl.formatMessage(messages.save)
: intl.formatMessage(messages.add)
}
@@ -250,7 +265,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
secondaryDisabled={
!values.apiKey || !values.hostname || !values.port || isTesting
}
okDisabled={!isValidated || isSubmitting || isTesting}
okDisabled={!isValidated || isSubmitting || isTesting || !isValid}
onOk={() => handleSubmit()}
title={
!radarr
@@ -312,6 +327,9 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div className="max-w-lg flex rounded-md shadow-sm">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-500 bg-gray-600 text-gray-100 sm:text-sm cursor-default">
{values.ssl ? 'https://' : 'http://'}
</span>
<Field
id="hostname"
name="hostname"
@@ -321,7 +339,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
setIsValidated(false);
setFieldValue('hostname', e.target.value);
}}
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
className="flex-1 form-input block w-full min-w-0 rounded-r-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-gray-700 border border-gray-500"
/>
</div>
{errors.hostname && touched.hostname && (
@@ -442,10 +460,17 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
as="select"
id="activeProfileId"
name="activeProfileId"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
disabled={!isValidated || isTesting}
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="">
{intl.formatMessage(messages.selectQualityProfile)}
{isTesting
? intl.formatMessage(messages.loadingprofiles)
: !isValidated
? intl.formatMessage(
messages.testFirstQualityProfiles
)
: intl.formatMessage(messages.selectQualityProfile)}
</option>
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
@@ -478,10 +503,15 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
as="select"
id="rootFolder"
name="rootFolder"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
disabled={!isValidated || isTesting}
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5 disabled:opacity-50"
>
<option value="">
{intl.formatMessage(messages.selectRootFolder)}
{isTesting
? intl.formatMessage(messages.loadingrootfolders)
: !isValidated
? intl.formatMessage(messages.testFirstRootFolders)
: intl.formatMessage(messages.selectRootFolder)}
</option>
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
@@ -516,17 +546,18 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
name="minimumAvailability"
className="mt-1 form-select rounded-md block w-full pl-3 pr-10 py-2 text-base leading-6 bg-gray-700 border-gray-500 focus:outline-none focus:ring-blue focus:border-gray-500 sm:text-sm sm:leading-5"
>
<option value="">
{intl.formatMessage(
messages.selectMinimumAvailability
)}
</option>
<option value="announced">Announced</option>
<option value="inCinemas">In Cinemas</option>
<option value="released">Released</option>
<option value="preDB">PreDB</option>
</Field>
</div>
{errors.minimumAvailability &&
touched.minimumAvailability && (
<div className="text-red-500 mt-2">
{errors.minimumAvailability}
</div>
)}
</div>
</div>
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">

View File

@@ -0,0 +1,186 @@
import React, { useState } from 'react';
import useSWR from 'swr';
import ReactMarkdown from 'react-markdown';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import Alert from '../../../Common/Alert';
import Badge from '../../../Common/Badge';
import Button from '../../../Common/Button';
import Modal from '../../../Common/Modal';
import Transition from '../../../Transition';
import { defineMessages, useIntl } from 'react-intl';
import globalMessages from '../../../../i18n/globalMessages';
const messages = defineMessages({
releases: 'Releases',
releasedataMissing: 'Release data missing. Is GitHub down?',
versionChangelog: 'Version Changelog',
viewongithub: 'View on GitHub',
latestversion: 'Latest Version',
currentversion: 'Current Version',
viewchangelog: 'View Changelog',
runningDevelop: 'You are running a develop version of Overseerr!',
runningDevelopMessage:
'The changes in your version will not be available below. Please look at the <GithubLink>GitHub repository</GithubLink> for latest updates.',
});
const REPO_RELEASE_API =
'https://api.github.com/repos/sct/overseerr/releases?per_page=20';
interface GitHubRelease {
url: string;
assets_url: string;
upload_url: string;
html_url: string;
id: number;
node_id: string;
tag_name: string;
target_commitish: string;
name: string;
draft: boolean;
prerelease: boolean;
created_at: string;
published_at: string;
tarball_url: string;
zipball_url: string;
body: string;
}
interface ReleaseProps {
release: GitHubRelease;
isLatest: boolean;
currentVersion: string;
}
const Release: React.FC<ReleaseProps> = ({
currentVersion,
release,
isLatest,
}) => {
const intl = useIntl();
const [isModalOpen, setModalOpen] = useState(false);
return (
<div className="bg-gray-800 rounded-md flex flex-col sm:flex-row px-4 py-2">
<Transition
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="opacity-100 transition duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={isModalOpen}
>
<Modal
onCancel={() => setModalOpen(false)}
iconSvg={
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
}
title={intl.formatMessage(messages.versionChangelog)}
cancelText={intl.formatMessage(globalMessages.close)}
okText={intl.formatMessage(messages.viewongithub)}
onOk={() => {
window.open(release.html_url, '_blank');
}}
>
<div className="prose">
<ReactMarkdown>{release.body}</ReactMarkdown>
</div>
</Modal>
</Transition>
<div className="flex mb-4 sm:mb-0 items-center justify-center sm:justify-start">
<span className="text-xl">{release.name}</span>
{isLatest && (
<span className="ml-2">
<Badge badgeType="primary">
{intl.formatMessage(messages.latestversion)}
</Badge>
</span>
)}
{release.name.includes(currentVersion) && (
<span className="ml-2">
<Badge badgeType="success">
{intl.formatMessage(messages.currentversion)}
</Badge>
</span>
)}
</div>
<div className="flex-1 text-center sm:text-right">
<Button buttonType="primary" onClick={() => setModalOpen(true)}>
{intl.formatMessage(messages.viewchangelog)}
</Button>
</div>
</div>
);
};
interface ReleasesProps {
currentVersion: string;
}
const Releases: React.FC<ReleasesProps> = ({ currentVersion }) => {
const intl = useIntl();
const { data, error } = useSWR<GitHubRelease[]>(REPO_RELEASE_API);
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return (
<div className="text-gray-300">
{intl.formatMessage(messages.releasedataMissing)}
</div>
);
}
return (
<div>
<div className="text-xl pb-4 mb-4 border-b border-gray-800">
{intl.formatMessage(messages.releases)}
</div>
{currentVersion.startsWith('develop-') && (
<Alert title={intl.formatMessage(messages.runningDevelop)}>
{intl.formatMessage(messages.runningDevelopMessage, {
GithubLink: function GithubLink(msg) {
return (
<a
href="https://github.com/sct/overseerr"
target="_blank"
rel="noreferrer"
className="text-yellow-100 underline hover:text-white transition duration-300"
>
{msg}
</a>
);
},
})}
</Alert>
)}
{data?.map((release, index) => {
return (
<div key={`release-${release.id}`} className="mb-2">
<Release
release={release}
currentVersion={currentVersion}
isLatest={index === 0}
/>
</div>
);
})}
</div>
);
};
export default Releases;

View File

@@ -0,0 +1,108 @@
import React from 'react';
import useSWR from 'swr';
import Error from '../../../pages/_error';
import List from '../../Common/List';
import LoadingSpinner from '../../Common/LoadingSpinner';
import { SettingsAboutResponse } from '../../../../server/interfaces/api/settingsInterfaces';
import { defineMessages, FormattedNumber, useIntl } from 'react-intl';
import Releases from './Releases';
const messages = defineMessages({
overseerrinformation: 'Overseerr Information',
version: 'Version',
totalmedia: 'Total Media',
totalrequests: 'Total Requests',
gettingsupport: 'Getting Support',
githubdiscussions: 'GitHub Discussions',
clickheretojoindiscord: 'Click here to join our Discord server.',
timezone: 'Timezone',
supportoverseerr: 'Support Overseerr',
helppaycoffee: 'Help pay for coffee',
});
const SettingsAbout: React.FC = () => {
const intl = useIntl();
const { data, error } = useSWR<SettingsAboutResponse>(
'/api/v1/settings/about'
);
if (error) {
return <Error statusCode={500} />;
}
if (!data && !error) {
return <LoadingSpinner />;
}
if (!data) {
return <LoadingSpinner />;
}
return (
<>
<div className="mb-8">
<List title={intl.formatMessage(messages.overseerrinformation)}>
<List.Item title={intl.formatMessage(messages.version)}>
{data.version}
</List.Item>
<List.Item title={intl.formatMessage(messages.totalmedia)}>
<FormattedNumber value={data.totalMediaItems} />
</List.Item>
<List.Item title={intl.formatMessage(messages.totalrequests)}>
<FormattedNumber value={data.totalRequests} />
</List.Item>
{data.tz && (
<List.Item title={intl.formatMessage(messages.timezone)}>
{data.tz}
</List.Item>
)}
</List>
</div>
<div className="mb-8">
<List title={intl.formatMessage(messages.gettingsupport)}>
<List.Item title={intl.formatMessage(messages.githubdiscussions)}>
<a
href="https://github.com/sct/overseerr/discussions"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://github.com/sct/overseerr/discussions
</a>
</List.Item>
<List.Item title="Discord">
<a
href="https://discord.gg/PkCWJSeCk7"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
{intl.formatMessage(messages.clickheretojoindiscord)}
</a>
</List.Item>
</List>
</div>
<div className="mb-8">
<List title={intl.formatMessage(messages.supportoverseerr)}>
<List.Item
title={`☕️ ${intl.formatMessage(messages.helppaycoffee)}`}
>
<a
href="https://patreon.com/overseerr"
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:underline"
>
https://patreon.com/overseerr
</a>
</List.Item>
</List>
</div>
<div className="mb-8">
<Releases currentVersion={data.version} />
</div>
</>
);
};
export default SettingsAbout;

View File

@@ -3,6 +3,7 @@ import useSWR from 'swr';
import LoadingSpinner from '../Common/LoadingSpinner';
import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
import Button from '../Common/Button';
import Table from '../Common/Table';
const messages = defineMessages({
jobname: 'Job Name',
@@ -21,55 +22,38 @@ const SettingsJobs: React.FC = () => {
}
return (
<div className="flex flex-col">
<div className="my-2 overflow-x-auto -mx-6 sm:-mx-6 md:mx-4 lg:mx-4">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden sm:rounded-lg">
<table className="min-w-full">
<thead>
<tr>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
{intl.formatMessage(messages.jobname)}
</th>
<th className="px-6 py-3 bg-gray-500 text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider">
{intl.formatMessage(messages.nextexecution)}
</th>
<th className="px-6 py-3 bg-gray-500"></th>
</tr>
</thead>
<tbody className="bg-gray-600 divide-y divide-gray-700">
{data?.map((job, index) => (
<tr key={`job-list-${index}`}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm leading-5 text-white">
{job.name}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm leading-5 text-white">
<FormattedRelativeTime
value={Math.floor(
(new Date(job.nextExecutionTime).getTime() -
Date.now()) /
1000
)}
updateIntervalInSeconds={1}
/>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm leading-5 font-medium">
<Button buttonType="primary">
{intl.formatMessage(messages.runnow)}
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
<Table>
<thead>
<Table.TH>{intl.formatMessage(messages.jobname)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.nextexecution)}</Table.TH>
<Table.TH></Table.TH>
</thead>
<Table.TBody>
{data?.map((job, index) => (
<tr key={`job-list-${index}`}>
<Table.TD>
<div className="text-sm leading-5 text-white">{job.name}</div>
</Table.TD>
<Table.TD>
<div className="text-sm leading-5 text-white">
<FormattedRelativeTime
value={Math.floor(
(new Date(job.nextExecutionTime).getTime() - Date.now()) /
1000
)}
updateIntervalInSeconds={1}
/>
</div>
</Table.TD>
<Table.TD alignText="right">
<Button buttonType="primary" disabled>
{intl.formatMessage(messages.runnow)}
</Button>
</Table.TD>
</tr>
))}
</Table.TBody>
</Table>
);
};

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