Compare commits
420 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31ac11f4eb | ||
|
|
f072a4e11d | ||
|
|
911faeff56 | ||
|
|
18ce349faa | ||
|
|
1d88be9341 | ||
|
|
b0b04ca1c7 | ||
|
|
1df67baf9e | ||
|
|
c886ea6c05 | ||
|
|
fe8d34607b | ||
|
|
86d6a25e5b | ||
|
|
d524371ace | ||
|
|
d8bcb99b2f | ||
|
|
55b18f0023 | ||
|
|
13e1595c6e | ||
|
|
f4bed9a63b | ||
|
|
1c8968fd26 | ||
|
|
d3a25b935a | ||
|
|
a908c07670 | ||
|
|
5c135c9974 | ||
|
|
77911c03e9 | ||
|
|
72931f3035 | ||
|
|
1b129c0b38 | ||
|
|
5544e4b65e | ||
|
|
285f8aa389 | ||
|
|
a06a85e329 | ||
|
|
67fbb401ac | ||
|
|
a9461f760d | ||
|
|
49782c0b73 | ||
|
|
e16277c07d | ||
|
|
2042a54f82 | ||
|
|
f990585914 | ||
|
|
c8f67cf866 | ||
|
|
bec1d3dde8 | ||
|
|
9ac0295986 | ||
|
|
3b003b7701 | ||
|
|
16a611b9df | ||
|
|
2059fc1cd4 | ||
|
|
7230915094 | ||
|
|
ab559a3dde | ||
|
|
02d324df01 | ||
|
|
e9b1a9e80e | ||
|
|
3f1f85a80e | ||
|
|
e393b53b29 | ||
|
|
544c2d9442 | ||
|
|
8546b0ef53 | ||
|
|
6c75c88228 | ||
|
|
a65e3d5bb6 | ||
|
|
173408a1f2 | ||
|
|
f5f8269cd2 | ||
|
|
1d7a938ef8 | ||
|
|
aee43cecfd | ||
|
|
232def972b | ||
|
|
f7146e4189 | ||
|
|
bc9477465d | ||
|
|
cb5ca7acf3 | ||
|
|
5f940c38bb | ||
|
|
aa205ffa97 | ||
|
|
6210f12e8e | ||
|
|
0de2b63903 | ||
|
|
dabd32a18b | ||
|
|
958cdf98fd | ||
|
|
8ebc829250 | ||
|
|
3642b1e84a | ||
|
|
f2f477649e | ||
|
|
d4c08d4d1c | ||
|
|
80165038fd | ||
|
|
380d36119f | ||
|
|
55e1b9cd0e | ||
|
|
015671f5be | ||
|
|
4a00617fe4 | ||
|
|
c1975b33f1 | ||
|
|
665e16475f | ||
|
|
2d737f2760 | ||
|
|
8c51c28f54 | ||
|
|
e2b8745fdc | ||
|
|
0974a4c971 | ||
|
|
c3c622793a | ||
|
|
3dc27ffd9b | ||
|
|
4ee78ab2fe | ||
|
|
4a5a9cce48 | ||
|
|
b53d099211 | ||
|
|
49dad53016 | ||
|
|
69561b1be5 | ||
|
|
3c89010629 | ||
|
|
75c72b987e | ||
|
|
cb852fded1 | ||
|
|
6d7671dd80 | ||
|
|
0ca3d43749 | ||
|
|
dfd4ff9229 | ||
|
|
ea1863ac3a | ||
|
|
9ec2c468cb | ||
|
|
f49a02449c | ||
|
|
eb6807b9e2 | ||
|
|
cbf45196b0 | ||
|
|
d226fc79b8 | ||
|
|
4a6151873a | ||
|
|
54429bbc1d | ||
|
|
d76bf32c9d | ||
|
|
1178ebff73 | ||
|
|
36c00fde27 | ||
|
|
6f1a31de47 | ||
|
|
b767a58b01 | ||
|
|
d71b48ad4e | ||
|
|
28a6a70e1e | ||
|
|
1c4515a1ae | ||
|
|
8dc69bddd9 | ||
|
|
1ea3c41bac | ||
|
|
b24c23e549 | ||
|
|
e602e9f758 | ||
|
|
ebc285c758 | ||
|
|
afd46ebee8 | ||
|
|
be47d1e56c | ||
|
|
2f2e00237d | ||
|
|
3d6b343413 | ||
|
|
9e5adeb610 | ||
|
|
ae29e5c5a2 | ||
|
|
dc8576768f | ||
|
|
422ef2cb76 | ||
|
|
001dcd328c | ||
|
|
0bd0912613 | ||
|
|
3148d31214 | ||
|
|
36584eee85 | ||
|
|
8634081c86 | ||
|
|
f7d2723fab | ||
|
|
1be97fe7fb | ||
|
|
71773c91c6 | ||
|
|
6e95c8b7a1 | ||
|
|
b36aff912a | ||
|
|
e4686d664b | ||
|
|
18a7876668 | ||
|
|
d48edeb5a9 | ||
|
|
a51d2a24d5 | ||
|
|
fc73592b69 | ||
|
|
60d453b0bb | ||
|
|
45886ccef1 | ||
|
|
84ef40ea8e | ||
|
|
512809f7c0 | ||
|
|
b85cd5376f | ||
|
|
1fcd18f0b0 | ||
|
|
dbea4f0194 | ||
|
|
75011610e5 | ||
|
|
109aca8229 | ||
|
|
3c1a72b038 | ||
|
|
3384eb1c47 | ||
|
|
e5f5bdb95c | ||
|
|
36e614f4f3 | ||
|
|
f5fc0545a4 | ||
|
|
69bf817f59 | ||
|
|
1c6914f5ce | ||
|
|
57bc340840 | ||
|
|
48387e5b2f | ||
|
|
1a7dc1acf5 | ||
|
|
d0fa5f3dbe | ||
|
|
f900d953a0 | ||
|
|
74e39b0b45 | ||
|
|
239202d9c1 | ||
|
|
85076919c6 | ||
|
|
9d0b52a242 | ||
|
|
f86d907c74 | ||
|
|
9966632c64 | ||
|
|
7aa0239cd9 | ||
|
|
7289872937 | ||
|
|
1f8b03ff6f | ||
|
|
e98f2b9605 | ||
|
|
92508b3f42 | ||
|
|
4f1a8a0a78 | ||
|
|
b5ce7f0cab | ||
|
|
778dda67d5 | ||
|
|
cd21865c4d | ||
|
|
0c4637f779 | ||
|
|
420038d5ff | ||
|
|
bdf67e732b | ||
|
|
f6d00d8d15 | ||
|
|
6072e8aa9a | ||
|
|
63c122e5e0 | ||
|
|
ed0a7fbdf5 | ||
|
|
b57645d382 | ||
|
|
f28112f057 | ||
|
|
436523139e | ||
|
|
0fa005a99c | ||
|
|
6a1e3891aa | ||
|
|
1629d02f3d | ||
|
|
594aad9d3a | ||
|
|
1d45f427e5 | ||
|
|
253ec15bc4 | ||
|
|
b146d11e2f | ||
|
|
bbc0b74b9d | ||
|
|
1b55d2dfbc | ||
|
|
ddfc5e6aa8 | ||
|
|
0e612f01f1 | ||
|
|
cd9d9ae8d2 | ||
|
|
2771376ecc | ||
|
|
eefcbcd3dd | ||
|
|
3fed26cfbe | ||
|
|
3f9a116b17 | ||
|
|
aeac15c63c | ||
|
|
abcd7c9975 | ||
|
|
9ce88abcc8 | ||
|
|
084917f02d | ||
|
|
69294a7c4c | ||
|
|
537850f414 | ||
|
|
e9f2fe910d | ||
|
|
5560abf459 | ||
|
|
ea2765f8c7 | ||
|
|
448e7c3a7d | ||
|
|
09b5019e95 | ||
|
|
6ab3cd77a7 | ||
|
|
970da664b2 | ||
|
|
552a7e30da | ||
|
|
4296765ad6 | ||
|
|
6ecd202607 | ||
|
|
da5ca02f81 | ||
|
|
0acad8e9fa | ||
|
|
84b950edc0 | ||
|
|
45cb977eb2 | ||
|
|
c4af4c42ab | ||
|
|
f9c83e14e5 | ||
|
|
29757db3a9 | ||
|
|
c88fcb2e2d | ||
|
|
8589eabfff | ||
|
|
b557c06b0a | ||
|
|
8701fb20d0 | ||
|
|
40b07c35d4 | ||
|
|
b05bbb8db5 | ||
|
|
34618edc13 | ||
|
|
5acd91bc8e | ||
|
|
bbea522499 | ||
|
|
139341b043 | ||
|
|
2e35aff903 | ||
|
|
c7765528b0 | ||
|
|
263c31c8b5 | ||
|
|
5b64655776 | ||
|
|
4e2706b421 | ||
|
|
c700aa938f | ||
|
|
57c00c1ea7 | ||
|
|
b4bac6a915 | ||
|
|
e70a4ecae6 | ||
|
|
17983837fc | ||
|
|
621db89328 | ||
|
|
77b2d9ea22 | ||
|
|
bdb3cb2025 | ||
|
|
bddea7ab24 | ||
|
|
8a9d794fec | ||
|
|
956ccb6a56 | ||
|
|
a1765c4ac2 | ||
|
|
9b73423d49 | ||
|
|
8a27c70625 | ||
|
|
2bcc3e3419 | ||
|
|
bbb683e637 | ||
|
|
29b97ef6d8 | ||
|
|
c9a150b1db | ||
|
|
e28dfadaf5 | ||
|
|
f1c335815f | ||
|
|
019622aab1 | ||
|
|
5e37a96bc0 | ||
|
|
8a7fa00164 | ||
|
|
2c403e8e13 | ||
|
|
577cbcc7fa | ||
|
|
e118501bf1 | ||
|
|
50c143d959 | ||
|
|
03cecb3355 | ||
|
|
53b7425f67 | ||
|
|
28090c30a7 | ||
|
|
fb9c878db4 | ||
|
|
9757e3ae0c | ||
|
|
ef5d019c18 | ||
|
|
04fa9f79e2 | ||
|
|
2768155bba | ||
|
|
7d3da1a1d2 | ||
|
|
85486a6719 | ||
|
|
bc4849ef55 | ||
|
|
1d7ef6d44f | ||
|
|
e156acc1ae | ||
|
|
e037ba48f1 | ||
|
|
034968e437 | ||
|
|
7e906e2d46 | ||
|
|
c8b2a57721 | ||
|
|
4e03c1efcf | ||
|
|
ff0b5ed441 | ||
|
|
d163e29459 | ||
|
|
ee5076146e | ||
|
|
8056187c3c | ||
|
|
a3042f8e1b | ||
|
|
282f28f2b9 | ||
|
|
7c0ddad653 | ||
|
|
53f6f59798 | ||
|
|
8956cb3915 | ||
|
|
baad19a2c9 | ||
|
|
3dfbb0e0a5 | ||
|
|
726f62b9b6 | ||
|
|
e5966bd3fb | ||
|
|
81c30347f6 | ||
|
|
c0ea2bd189 | ||
|
|
a397893217 | ||
|
|
12c5bbd778 | ||
|
|
c5be00eebf | ||
|
|
4eae02a7e1 | ||
|
|
b531a642f6 | ||
|
|
e4f9b8a984 | ||
|
|
7456bea2ae | ||
|
|
38c76b55e0 | ||
|
|
eedadb8884 | ||
|
|
3f9bfeb01a | ||
|
|
b9d14a9fd0 | ||
|
|
026795d4c9 | ||
|
|
ee0a7bd8c0 | ||
|
|
b658ddf5cf | ||
|
|
63d7e2b398 | ||
|
|
033ba9d41b | ||
|
|
06e941171a | ||
|
|
e270999745 | ||
|
|
9e5e4c22f5 | ||
|
|
fcd1dfc675 | ||
|
|
b14abbfe08 | ||
|
|
1e5f88f462 | ||
|
|
035534e9fe | ||
|
|
c97c96a30c | ||
|
|
b116281196 | ||
|
|
e4a50c33f1 | ||
|
|
59150f955f | ||
|
|
c0769d4f8f | ||
|
|
35c6bfc021 | ||
|
|
3ffd5ab0ee | ||
|
|
f54df214af | ||
|
|
2ad85d5fdc | ||
|
|
9d0d5b86aa | ||
|
|
b576f0734f | ||
|
|
42295e076a | ||
|
|
f338b34b74 | ||
|
|
5d40ae6e22 | ||
|
|
cb123cee83 | ||
|
|
d957730a39 | ||
|
|
77bf730f15 | ||
|
|
95c2a21697 | ||
|
|
6e9ac275e1 | ||
|
|
1ccafc0ebd | ||
|
|
7c192d54f4 | ||
|
|
cf7ebc488d | ||
|
|
e34fbf72fd | ||
|
|
bfe25d9755 | ||
|
|
3052f12c91 | ||
|
|
a3b00c3458 | ||
|
|
a7314f876e | ||
|
|
5b39911e02 | ||
|
|
ec44841356 | ||
|
|
43c989e423 | ||
|
|
9979941081 | ||
|
|
3a926b34b0 | ||
|
|
239f8e40c7 | ||
|
|
92be2ee42b | ||
|
|
d20bd530ed | ||
|
|
72cffd74a7 | ||
|
|
6c437c515f | ||
|
|
cfacb151b5 | ||
|
|
996bd9f14e | ||
|
|
3c5ae360fd | ||
|
|
20289b5960 | ||
|
|
b239598e64 | ||
|
|
e2b800000d | ||
|
|
e34482bc3f | ||
|
|
06261c1118 | ||
|
|
686fe6fdf8 | ||
|
|
5bb2d75fe1 | ||
|
|
c80a1fa261 | ||
|
|
6f3f24a1d7 | ||
|
|
71875efb48 | ||
|
|
2e724b5bd8 | ||
|
|
136d874cba | ||
|
|
f1dd5e7e12 | ||
|
|
67f8aef00d | ||
|
|
b5fd1d520c | ||
|
|
c43cd16eff | ||
|
|
3812989a1c | ||
|
|
1d7150c24e | ||
|
|
946bd2db5e | ||
|
|
0c6d4780c3 | ||
|
|
45a277964b | ||
|
|
ec5fb83678 | ||
|
|
86efcd82c3 | ||
|
|
f05d4a0d0b | ||
|
|
9342a40bbc | ||
|
|
20ca3f2f5f | ||
|
|
82ac76b054 | ||
|
|
b3313a7f4d | ||
|
|
6e2589178b | ||
|
|
4b0241c3b3 | ||
|
|
2f75c4c6ae | ||
|
|
21db3676d1 | ||
|
|
d0c830e80d | ||
|
|
660ada0b20 | ||
|
|
2d31ea940a | ||
|
|
3ebb7d087f | ||
|
|
5e352c201f | ||
|
|
e044146aa5 | ||
|
|
083a74a686 | ||
|
|
58dbdad2b2 | ||
|
|
32f4916c4a | ||
|
|
6d0f7d4b50 | ||
|
|
f90e43b01a | ||
|
|
9bb11afc6b | ||
|
|
059995e0ef | ||
|
|
c2a3e8ed52 | ||
|
|
4f94d227fc | ||
|
|
9aa5c12164 | ||
|
|
4789583d66 | ||
|
|
98570c920e | ||
|
|
f001fb3b33 | ||
|
|
e1accbcef0 | ||
|
|
ae9a1b3e94 | ||
|
|
0b055458d0 | ||
|
|
be650aae37 | ||
|
|
a42f3115d3 | ||
|
|
2da0da826a | ||
|
|
a3fe4e6321 | ||
|
|
421f4c17f0 | ||
|
|
e97228899a | ||
|
|
2bfab5e99c | ||
|
|
0483becbfa | ||
|
|
3a1f6d5706 | ||
|
|
6c1742e94c |
@@ -225,7 +225,8 @@
|
||||
"profile": "https://github.com/ankarhem",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
"code",
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -262,6 +263,172 @@
|
||||
"profile": "https://github.com/TheCatLady",
|
||||
"contributions": [
|
||||
"code",
|
||||
"translation",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chriscpritchard",
|
||||
"name": "Chris Pritchard",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1839074?v=4",
|
||||
"profile": "https://github.com/chriscpritchard",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Tamberlox",
|
||||
"name": "Tamberlox",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/56069014?v=4",
|
||||
"profile": "https://github.com/Tamberlox",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hmnd",
|
||||
"name": "David",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12853597?v=4",
|
||||
"profile": "https://hmnd.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "douglasparker",
|
||||
"name": "Douglas Parker",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18235822?v=4",
|
||||
"profile": "https://www.douglas-parker.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dancarter",
|
||||
"name": "Daniel Carter",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4387516?v=4",
|
||||
"profile": "https://github.com/dancarter",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "NuroDev",
|
||||
"name": "nuro",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4991309?v=4",
|
||||
"profile": "https://nuro.dev",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "onedr0p",
|
||||
"name": "ᗪєνιη ᗷυнʟ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/213795?v=4",
|
||||
"profile": "https://github.com/onedr0p",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JonnyWong16",
|
||||
"name": "JonnyWong16",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9099342?v=4",
|
||||
"profile": "https://github.com/JonnyWong16",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Roxedus",
|
||||
"name": "Roxedus",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7110194?v=4",
|
||||
"profile": "https://github.com/Roxedus",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "WoisWoi",
|
||||
"name": "WoisWoi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/75491231?v=4",
|
||||
"profile": "https://github.com/WoisWoi",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "HubDuck",
|
||||
"name": "HubDuck",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/77843475?v=4",
|
||||
"profile": "https://github.com/HubDuck",
|
||||
"contributions": [
|
||||
"translation",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "costaht",
|
||||
"name": "costaht",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/50637431?v=4",
|
||||
"profile": "https://github.com/costaht",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Shjosan",
|
||||
"name": "Shjosan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20847626?v=4",
|
||||
"profile": "https://github.com/Shjosan",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kobaubarr",
|
||||
"name": "kobaubarr",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28481522?v=4",
|
||||
"profile": "https://github.com/kobaubarr",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "notorius28",
|
||||
"name": "Ricardo González",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1621513?v=4",
|
||||
"profile": "https://github.com/notorius28",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Torkiliuz",
|
||||
"name": "Torkil",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/460764?v=4",
|
||||
"profile": "http://torkili.uz",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JagandeepBrar",
|
||||
"name": "Jagandeep Brar",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3048295?v=4",
|
||||
"profile": "https://www.jagandeepbrar.io",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dtalens",
|
||||
"name": "dtalens",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6631832?v=4",
|
||||
"profile": "http://dtalens.com",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.gitconfig
|
||||
.gitignore
|
||||
.github
|
||||
.all-contributorsrc
|
||||
.editorconfig
|
||||
.prettierignore
|
||||
**/README.md
|
||||
**/*.md
|
||||
**/.gitkeep
|
||||
**/.vscode
|
||||
config/db/db.sqlite3
|
||||
config/db/logs/overseerr.log
|
||||
Dockerfil**
|
||||
**.md
|
||||
.all-contributorsrc
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.eslintrc.js
|
||||
.git
|
||||
.gitbook.yaml
|
||||
.gitconfig
|
||||
.github
|
||||
.gitignore
|
||||
.next
|
||||
.prettierignore
|
||||
config/db/*
|
||||
config/logs/*
|
||||
config/*.json
|
||||
dist
|
||||
Dockerfile*
|
||||
docker-compose.yml
|
||||
docs
|
||||
LICENSE
|
||||
node_modules
|
||||
public/os_logo_square.png
|
||||
public/preview.jpg
|
||||
snap
|
||||
stylelint.config.js
|
||||
|
||||
@@ -4,12 +4,10 @@ module.exports = {
|
||||
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.
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier/react',
|
||||
'prettier',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
@@ -32,6 +30,7 @@ module.exports = {
|
||||
'formatjs/no-offset': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error'],
|
||||
'jsx-a11y/no-onchange': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
12
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Global code ownership
|
||||
* @sct
|
||||
|
||||
# Documentation
|
||||
docs/ @TheCatLady @samwiseg0
|
||||
|
||||
# Snap-related files
|
||||
.github/workflows/snap.yaml @samwiseg0
|
||||
snap/ @samwiseg0
|
||||
|
||||
# i18n locale files
|
||||
src/i18n/locale/ @sct @TheCatLady
|
||||
54
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,45 +1,45 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
about: Submit a report to help us improve
|
||||
title: ''
|
||||
labels: 'awaiting-triage, type:bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
#### Description
|
||||
|
||||
**What version of Overseerr are you running?**
|
||||
Please fill in the version you are currently running.
|
||||
Please provide a clear and concise description of the bug or issue.
|
||||
|
||||
You can find it under: Settings -> About -> Version
|
||||
#### Version
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
What version of Overseerr are you running? (You can find this in Settings → About → Version.)
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
#### Steps to Reproduce
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
Please tell us how we can reproduce the undesired behavior.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
1. Go to [...]
|
||||
2. Click on [...]
|
||||
3. Scroll down to [...]
|
||||
4. See error in [...]
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
#### Expected Behavior
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
Please provide a clear and concise description of what you expected to happen.
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
#### Screenshots
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
If applicable, please provide screenshots depicting the problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
#### Device
|
||||
|
||||
What device were you using when you encountered this issue? Please provide this information to help us reproduce and investigate the bug.
|
||||
|
||||
- **Platform:** [e.g., desktop, smartphone, tablet]
|
||||
- **Device:** [e.g., iPhone X, Surface Pro, Samsung Galaxy Tab]
|
||||
- **OS:** [e.g., iOS 8.1, Windows 10, Android 11]
|
||||
- **Browser:** [e.g., Chrome, Safari, Edge, Firefox]
|
||||
|
||||
#### Additional Context
|
||||
|
||||
Please provide any additional information that may be relevant or helpful.
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -6,14 +6,14 @@ labels: 'awaiting-triage, type: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 [...]
|
||||
#### Description
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
Is your feature request related to a problem? If so, please provide a clear and concise description of the problem. E.g., "I'm always frustrated when [...]."
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
#### Desired Behavior
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
Provide a clear and concise description of what you want to happen.
|
||||
|
||||
#### Additional Context
|
||||
|
||||
Provide any additional information or screenshots that may be relevant or helpful.
|
||||
|
||||
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,13 +1,13 @@
|
||||
#### Description
|
||||
|
||||
#### Screenshot (if UI related)
|
||||
#### Screenshot (if UI-related)
|
||||
|
||||
#### Todos
|
||||
#### To-Dos
|
||||
|
||||
- [ ] Sucessfully builds `yarn build`
|
||||
- [ ] Translation Keys `yarn i18n:extract`
|
||||
- [ ] Database migration created (if required)
|
||||
- [ ] Successful build `yarn build`
|
||||
- [ ] Translation keys `yarn i18n:extract`
|
||||
- [ ] Database migration (if required)
|
||||
|
||||
#### Issues Fixed or Closed by this PR
|
||||
#### Issues Fixed or Closed
|
||||
|
||||
- Fixes #XXXX
|
||||
|
||||
6
.github/dependabot.yml
vendored
@@ -6,3 +6,9 @@ updates:
|
||||
interval: daily
|
||||
time: '20:00'
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: github-actions
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: daily
|
||||
time: '20:00'
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
2
.github/stale.yml
vendored
@@ -8,7 +8,7 @@ exemptLabels:
|
||||
- security
|
||||
- dependencies
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
staleLabel: stale
|
||||
# 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
|
||||
|
||||
25
.github/workflows/ci.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:12.18-alpine
|
||||
container: node:14.16-alpine
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -24,6 +24,7 @@ jobs:
|
||||
run: yarn lint
|
||||
- name: build
|
||||
run: yarn build
|
||||
|
||||
build_and_push:
|
||||
name: Build & Publish to Docker Hub
|
||||
needs: test
|
||||
@@ -36,6 +37,13 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -52,6 +60,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
@@ -60,6 +69,16 @@ jobs:
|
||||
sctx/overseerr:${{ github.sha }}
|
||||
ghcr.io/sct/overseerr:develop
|
||||
ghcr.io/sct/overseerr:${{ github.sha }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
- # Temporary fix
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# https://github.com/moby/buildkit/issues/1896
|
||||
name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
needs: build_and_push
|
||||
@@ -67,8 +86,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
|
||||
uses: technote-space/workflow-conclusion-action@v2.1.5
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
@@ -78,7 +96,6 @@ jobs:
|
||||
else
|
||||
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
||||
fi
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
|
||||
23
.github/workflows/deploy_docs.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Deploy API Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Generate Swagger UI
|
||||
uses: Legion2/swagger-ui-action@v1
|
||||
with:
|
||||
output: swagger-ui
|
||||
spec-file: overseerr-api.yml
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
cname: api-docs.overseerr.dev
|
||||
6
.github/workflows/invalid_template.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
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
|
||||
:wave: @{issue-author}, please follow the template provided.
|
||||
close-issue: true
|
||||
lock-issue: true
|
||||
issue-lock-reason: 'resolved'
|
||||
|
||||
35
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:12.18-alpine
|
||||
container: node:14.16-alpine
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -31,9 +31,24 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: 14
|
||||
- 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_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: Install dependencies
|
||||
run: yarn
|
||||
- name: Release
|
||||
@@ -56,11 +71,17 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Switch to master branch
|
||||
run: git checkout master
|
||||
- name: Pull latest changes
|
||||
run: git pull
|
||||
|
||||
- name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
git fetch --prune --tags
|
||||
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||
echo ::set-output name=RELEASE::stable
|
||||
else
|
||||
@@ -69,6 +90,8 @@ jobs:
|
||||
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
|
||||
|
||||
- name: Build Snap Package
|
||||
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
@@ -83,7 +106,7 @@ jobs:
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
|
||||
- name: Review Snap Package
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1.2.0
|
||||
with:
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
|
||||
@@ -100,7 +123,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
uses: technote-space/workflow-conclusion-action@v2.1.5
|
||||
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
|
||||
19
.github/workflows/snap.yaml
vendored
@@ -5,10 +5,19 @@ on:
|
||||
branches: [develop]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
jobs:
|
||||
name: Job Check
|
||||
runs-on: ubuntu-20.04
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.8.0
|
||||
with:
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
needs: jobs
|
||||
runs-on: ubuntu-20.04
|
||||
container: node:12.18-alpine
|
||||
steps:
|
||||
- name: checkout
|
||||
@@ -48,6 +57,8 @@ jobs:
|
||||
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
with:
|
||||
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
|
||||
|
||||
- name: Build Snap Package
|
||||
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
@@ -62,7 +73,7 @@ jobs:
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
|
||||
- name: Review Snap Package
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||
uses: diddlesnaps/snapcraft-review-tools-action@v1.2.0
|
||||
with:
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
|
||||
@@ -79,7 +90,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
uses: technote-space/workflow-conclusion-action@v2.1.5
|
||||
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
|
||||
3
.github/workflows/support.yml
vendored
@@ -19,7 +19,8 @@ jobs:
|
||||
to get help with Overseerr.
|
||||
|
||||
- [Discord](https://discord.gg/PkCWJSeCk7)
|
||||
|
||||
- [GitHub Discussions](https://github.com/sct/overseerr/discussions)
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
lock-issue: true
|
||||
issue-lock-reason: 'off-topic'
|
||||
|
||||
7
.gitignore
vendored
@@ -32,12 +32,14 @@ yarn-error.log*
|
||||
.vercel
|
||||
|
||||
# database
|
||||
config/db/*.sqlite3
|
||||
config/db/*.sqlite3*
|
||||
config/settings.json
|
||||
|
||||
# logs
|
||||
config/logs/*.log*
|
||||
config/logs/*.json
|
||||
config/logs/*.log.gz
|
||||
config/logs/*-audit.json
|
||||
|
||||
# anidb mapping file
|
||||
config/anime-list.xml
|
||||
@@ -47,3 +49,6 @@ dist
|
||||
|
||||
# sqlite journal
|
||||
config/db/db.sqlite3-journal
|
||||
|
||||
# VS Code
|
||||
.vscode/launch.json
|
||||
|
||||
9
.stoplight.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"formats": {
|
||||
"openapi": {
|
||||
"rootDir": ".",
|
||||
"include": ["**"]
|
||||
}
|
||||
},
|
||||
"exclude": ["docs"]
|
||||
}
|
||||
5
.vscode/extensions.json
vendored
@@ -19,6 +19,9 @@
|
||||
|
||||
"stylelint.vscode-stylelint",
|
||||
|
||||
"bradlc.vscode-tailwindcss"
|
||||
"bradlc.vscode-tailwindcss",
|
||||
|
||||
// https://marketplace.visualstudio.com/items?itemName=heybourn.headwind
|
||||
"heybourn.headwind"
|
||||
]
|
||||
}
|
||||
|
||||
6
.vscode/settings.json
vendored
@@ -14,5 +14,9 @@
|
||||
"name": "Local SQLite",
|
||||
"database": "./config/db/db.sqlite3"
|
||||
}
|
||||
]
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
|
||||
349
CHANGELOG.md
@@ -1,3 +1,352 @@
|
||||
# [1.22.0](https://github.com/sct/overseerr/compare/v1.21.1...v1.22.0) (2021-04-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **android:** adaptive icons for Android devices ([#1274](https://github.com/sct/overseerr/issues/1274)) ([a65e3d5](https://github.com/sct/overseerr/commit/a65e3d5bb6924cbde30b26ff8acf535e5274efee))
|
||||
* **backend:** fix getShowByTvdbId() error message ([#1314](https://github.com/sct/overseerr/issues/1314)) [skip ci] ([fe8d346](https://github.com/sct/overseerr/commit/fe8d34607b07095dce51b29ef7aaae0485573f14))
|
||||
* **db:** enable WAL journal mode ([aa205ff](https://github.com/sct/overseerr/commit/aa205ffa975d02ef0be30626e7c946a42679a847))
|
||||
* **frontend:** 'Recent Requests' slider should link to request list w/ same filter ([#1235](https://github.com/sct/overseerr/issues/1235)) ([49782c0](https://github.com/sct/overseerr/commit/49782c0b730cce9f0bad14e9c83842b5b0bfe11e))
|
||||
* **frontend:** call mutate after changing public settings ([#1302](https://github.com/sct/overseerr/issues/1302)) ([c8f67cf](https://github.com/sct/overseerr/commit/c8f67cf866ada791e4129a0bbae16b9eac41f32e))
|
||||
* **frontend:** include language parameter in TMDb links ([#1344](https://github.com/sct/overseerr/issues/1344)) ([1d88be9](https://github.com/sct/overseerr/commit/1d88be9341a8ff9e1f39b02556b489cdbd06392b))
|
||||
* **frontend:** redirect from /setup if already initialized ([#1238](https://github.com/sct/overseerr/issues/1238)) ([8016503](https://github.com/sct/overseerr/commit/80165038fd214897e3520a420f971341e7b94865))
|
||||
* **frontend:** use correct path to user profile in request modal quota dropdown ([#1307](https://github.com/sct/overseerr/issues/1307)) ([f990585](https://github.com/sct/overseerr/commit/f9905859148088afec53549b81611b07bf19d3b9))
|
||||
* **frontend:** use HTTPS to fetch TMDb assets for network/studio sliders ([#1343](https://github.com/sct/overseerr/issues/1343)) ([c886ea6](https://github.com/sct/overseerr/commit/c886ea6c0578cb7532d6c09266a76bfad8598b9d))
|
||||
* **frontend:** use next/image to serve login page images ([cbf4519](https://github.com/sct/overseerr/commit/cbf45196b023f60c8e4cf7602c0295f886fe610c)), closes [#1207](https://github.com/sct/overseerr/issues/1207)
|
||||
* **lang:** allow proper localization of comma-delimited lists ([#1264](https://github.com/sct/overseerr/issues/1264)) ([173408a](https://github.com/sct/overseerr/commit/173408a1f269f09c724843ba087ef3f85b2832ad))
|
||||
* **lang:** change 'Extra Data' string to 'Additional Data' ([#1226](https://github.com/sct/overseerr/issues/1226)) ([665e164](https://github.com/sct/overseerr/commit/665e16475f3fa2ea6118340d9ea2d30b98abb238))
|
||||
* **lang:** correct mismatched language strings ([#1246](https://github.com/sct/overseerr/issues/1246)) ([8ebc829](https://github.com/sct/overseerr/commit/8ebc8292504cdc57a148ab69bcb4e1514ef018c6))
|
||||
* **lang:** correct strings for library sync button & user import toast ([#1252](https://github.com/sct/overseerr/issues/1252)) ([cb5ca7a](https://github.com/sct/overseerr/commit/cb5ca7acf38dcc2e27ec31d88434a11757cdb469))
|
||||
* **lang:** edit setting label strings for verb tense consistency ([#1214](https://github.com/sct/overseerr/issues/1214)) ([6d7671d](https://github.com/sct/overseerr/commit/6d7671dd80fea632e5cef29fc0b4968bffe231b0))
|
||||
* **lang:** fix overwritten/shared string ([#1212](https://github.com/sct/overseerr/issues/1212)) ([dfd4ff9](https://github.com/sct/overseerr/commit/dfd4ff9229822b0ce79ba322376194cbb6fd233d))
|
||||
* **lang:** remove 'requires and' ([#1215](https://github.com/sct/overseerr/issues/1215)) ([cb852fd](https://github.com/sct/overseerr/commit/cb852fded18f53806c23ec6f215385072b2a867b))
|
||||
* **lang:** remove unused strings ([#1330](https://github.com/sct/overseerr/issues/1330)) ([13e1595](https://github.com/sct/overseerr/commit/13e1595c6ebff32ca905d9bd3dd781e241545e83))
|
||||
* **lang:** UI string edits, round 2 ([#1202](https://github.com/sct/overseerr/issues/1202)) ([ea1863a](https://github.com/sct/overseerr/commit/ea1863ac3a5d3051e07815d07df0d3f2abd9166f))
|
||||
* **log:** fix typo in base scanner logging ([#1329](https://github.com/sct/overseerr/issues/1329)) [skip ci] ([b0b04ca](https://github.com/sct/overseerr/commit/b0b04ca1c7218ad5b67d9ec8b3fac5af78a4c132))
|
||||
* **logs:** add i18n strings for new log page changes ([8c51c28](https://github.com/sct/overseerr/commit/8c51c28f546b9c2d38ff7f20d59bb08a599e8146))
|
||||
* **notifications:** correctly send notifications for users that do not have any user settings yet ([d3a25b9](https://github.com/sct/overseerr/commit/d3a25b935aae35dd97ef0f168ac7e2898126a9a5)), closes [#1324](https://github.com/sct/overseerr/issues/1324)
|
||||
* **overseerr-api.yml:** fixed pushbullet & webhook API definition refs and descriptions ([#1288](https://github.com/sct/overseerr/issues/1288)) [skip ci] ([3b003b7](https://github.com/sct/overseerr/commit/3b003b770120f7d150c64ff098b626015c030794))
|
||||
* **plex:** always send Overseerr for the device name to the plex.tv api ([f7146e4](https://github.com/sct/overseerr/commit/f7146e41899a59f75b963e1cc9dac9eddf24aebe)), closes [#1244](https://github.com/sct/overseerr/issues/1244)
|
||||
* **ui:** add validation to hostname/IP fields ([#1206](https://github.com/sct/overseerr/issues/1206)) ([f49a024](https://github.com/sct/overseerr/commit/f49a02449c4928aef56cecbf908cf585ea0d4fca))
|
||||
* **ui:** better regex matching when parsing logs ([#1225](https://github.com/sct/overseerr/issues/1225)) ([2d737f2](https://github.com/sct/overseerr/commit/2d737f276095a8ca9abea360ef29134e9f639a39))
|
||||
* **ui:** button w/ dropdown z-indices ([#1230](https://github.com/sct/overseerr/issues/1230)) ([015671f](https://github.com/sct/overseerr/commit/015671f5be7a9f0f5c38db5a11a4b3c788dfaade))
|
||||
* **ui:** center role under title cards on person detail pages ([#1205](https://github.com/sct/overseerr/issues/1205)) ([4a61518](https://github.com/sct/overseerr/commit/4a6151873a3a3c5e45f9817131774a2c52957138))
|
||||
* **ui:** correctly enable the request button when partial requests are disabled with no quota ([16a611b](https://github.com/sct/overseerr/commit/16a611b9dfc3c66483640f4f5364646f41d37159))
|
||||
* **ui:** correctly paginate request list ([67fbb40](https://github.com/sct/overseerr/commit/67fbb401ac6ba05e58b8dfefd5954b28316254f2))
|
||||
* **ui:** correctly show quota display on tv request modal when only series quota is set ([3f1f85a](https://github.com/sct/overseerr/commit/3f1f85a80edfd2a4e9627162ff29ca6bcf2d8583))
|
||||
* **ui:** display asterisk indicator on required field labels ([#1236](https://github.com/sct/overseerr/issues/1236)) ([380d361](https://github.com/sct/overseerr/commit/380d36119f19a20ad67f79b3fb5db4036a093cac))
|
||||
* **ui:** do not check isValid on Sonarr/Radarr modals for the test button ([0974a4c](https://github.com/sct/overseerr/commit/0974a4c971358b7a64668f9a63fc356234a656c9))
|
||||
* **ui:** do not require numeric value in FormattedRelativeTime ([#1234](https://github.com/sct/overseerr/issues/1234)) ([3642b1e](https://github.com/sct/overseerr/commit/3642b1e84a20fef72428b3e240c86d35be8be8a2))
|
||||
* **ui:** filter out server options that do not match request type (non-4K or 4K) ([#1183](https://github.com/sct/overseerr/issues/1183)) ([28a6a70](https://github.com/sct/overseerr/commit/28a6a70e1ecc125f4cf4900e599ad0d4d7b55e3b))
|
||||
* **ui:** fix label formatting in general user settings ([#1275](https://github.com/sct/overseerr/issues/1275)) ([8546b0e](https://github.com/sct/overseerr/commit/8546b0ef53d232256b62cf08466e692a6971c16b))
|
||||
* **ui:** fix regex matching when parsing label from logs ([#1231](https://github.com/sct/overseerr/issues/1231)) ([4a00617](https://github.com/sct/overseerr/commit/4a00617fe47064ea50f95a02f29832a419ab13a3))
|
||||
* **ui:** gracefully handle lengthy titles & long words in overviews ([#1338](https://github.com/sct/overseerr/issues/1338)) ([d8bcb99](https://github.com/sct/overseerr/commit/d8bcb99b2fd3b24a5119ba5ff213a640425ff553))
|
||||
* **ui:** hide 'show details' button if there are no additional details ([#1254](https://github.com/sct/overseerr/issues/1254)) ([6210f12](https://github.com/sct/overseerr/commit/6210f12e8e9f593d629d22278d78310482ca0cfa))
|
||||
* **ui:** increase page size dropdown width when necessary ([#1216](https://github.com/sct/overseerr/issues/1216)) ([75c72b9](https://github.com/sct/overseerr/commit/75c72b987eb52b907ffd8af33f15ecc58213fc12))
|
||||
* **ui:** restore saved states of quota override checkboxes ([#1282](https://github.com/sct/overseerr/issues/1282)) ([2059fc1](https://github.com/sct/overseerr/commit/2059fc1cd4d48c7d80e761b7d41b7ec122d82769))
|
||||
* **ui:** sort regions & languages by their localized names rather than their TMDb English names ([#1157](https://github.com/sct/overseerr/issues/1157)) ([d76bf32](https://github.com/sct/overseerr/commit/d76bf32c9dcc83ebd0bae979726b1456a9028d8b))
|
||||
* **ui:** tweak request list design ([#1201](https://github.com/sct/overseerr/issues/1201)) ([d226fc7](https://github.com/sct/overseerr/commit/d226fc79b8d5f1263d4b80a7a1772074020ec94f))
|
||||
* **ui:** use appropriate cursor type for disabled UI elements ([#1184](https://github.com/sct/overseerr/issues/1184)) ([b767a58](https://github.com/sct/overseerr/commit/b767a58b011cc317a889cb8c2889b3210bec5fae))
|
||||
* **ui:** use appropriate cursor type for readonly input fields ([#1208](https://github.com/sct/overseerr/issues/1208)) ([9ec2c46](https://github.com/sct/overseerr/commit/9ec2c468cbbcbd41b94bbf9f3cfeb43eed09f36e))
|
||||
* **ui:** use correct colspan for 'No results.' message in Settings > Logs ([#1325](https://github.com/sct/overseerr/issues/1325)) ([5c135c9](https://github.com/sct/overseerr/commit/5c135c9974ebfcbdb434dafd459d1035624df6ed))
|
||||
* fetch localized person details from TMDb ([#1243](https://github.com/sct/overseerr/issues/1243)) ([1d7a938](https://github.com/sct/overseerr/commit/1d7a938ef8b0b8c20fda5024121de2a217ef4127))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add apple splash for pwa ([232def9](https://github.com/sct/overseerr/commit/232def972b9156afcbd83592708dbf8b5866ee24))
|
||||
* **frontend:** add apple tv+ to network slider ([3dc27ff](https://github.com/sct/overseerr/commit/3dc27ffd9bb054e6cda58872939dbc352877d184)), closes [#1219](https://github.com/sct/overseerr/issues/1219)
|
||||
* **frontend:** allow selecting multiple original languages ([a908c07](https://github.com/sct/overseerr/commit/a908c07670532b0ca7f766065bb4653ce2376e6f))
|
||||
* **lang:** add Catalan to language picker ([#1309](https://github.com/sct/overseerr/issues/1309)) ([77911c0](https://github.com/sct/overseerr/commit/77911c03e98aa3c2c6c062a01c22b030704309c2))
|
||||
* **lang:** translations update from Weblate ([#1178](https://github.com/sct/overseerr/issues/1178)) ([3c89010](https://github.com/sct/overseerr/commit/3c89010629bc16f225f1d3936abe9f4e47a0d7c7))
|
||||
* **lang:** translations update from Weblate ([#1224](https://github.com/sct/overseerr/issues/1224)) ([c1975b3](https://github.com/sct/overseerr/commit/c1975b33f1115a95068be000b7f479a401f0f0ae))
|
||||
* **lang:** translations update from Weblate ([#1237](https://github.com/sct/overseerr/issues/1237)) ([dabd32a](https://github.com/sct/overseerr/commit/dabd32a18b42980059c7a7a7450514ca827a5d3b))
|
||||
* **lang:** translations update from Weblate ([#1256](https://github.com/sct/overseerr/issues/1256)) ([e9b1a9e](https://github.com/sct/overseerr/commit/e9b1a9e80e6b8285fa451a8551c5832a850c1746))
|
||||
* **lang:** translations update from Weblate ([#1281](https://github.com/sct/overseerr/issues/1281)) ([bec1d3d](https://github.com/sct/overseerr/commit/bec1d3dde834b9a50e24c5894c362e5982ff3bd5))
|
||||
* **lang:** translations update from Weblate ([#1305](https://github.com/sct/overseerr/issues/1305)) ([1b129c0](https://github.com/sct/overseerr/commit/1b129c0b3863ea3c5ad34c66b3ace5d09cd4e391))
|
||||
* **lang:** translations update from Weblate ([#1313](https://github.com/sct/overseerr/issues/1313)) ([18ce349](https://github.com/sct/overseerr/commit/18ce349faac6ee560b9c92374039954f2365a8d1))
|
||||
* **logs:** add copy to clipboard button to logs page ([e2b8745](https://github.com/sct/overseerr/commit/e2b8745fdc192f3d49872625652184005a760885))
|
||||
* **notif:** include requested season numbers in notifications ([#1211](https://github.com/sct/overseerr/issues/1211)) ([4ee78ab](https://github.com/sct/overseerr/commit/4ee78ab2fe0359df6baa58f0986687f05a8392a2))
|
||||
* **requests:** add request quotas ([#1277](https://github.com/sct/overseerr/issues/1277)) ([6c75c88](https://github.com/sct/overseerr/commit/6c75c8822842514ffd31864992e8d3ce686fea1b))
|
||||
* **settings:** logs viewer ([#997](https://github.com/sct/overseerr/issues/997)) ([54429bb](https://github.com/sct/overseerr/commit/54429bbc1d765d0e50486a42749f9bbd4e5b3386))
|
||||
* **ui:** add movie/series genre list pages ([#1194](https://github.com/sct/overseerr/issues/1194)) ([6f1a31d](https://github.com/sct/overseerr/commit/6f1a31de473d1a25bc77e0961a52b07050b64c51))
|
||||
* **ui:** add option to only allow complete series requests ([#1164](https://github.com/sct/overseerr/issues/1164)) ([36c00fd](https://github.com/sct/overseerr/commit/36c00fde273799a56ec42ce6177ff44fed0904c3))
|
||||
* **ui:** Add user requests page ([#936](https://github.com/sct/overseerr/issues/936)) ([a9461f7](https://github.com/sct/overseerr/commit/a9461f760d8112f2ae16183e796f706d3392f8ec))
|
||||
* **ui:** allow any value 1-100 for quota limit/days ([#1337](https://github.com/sct/overseerr/issues/1337)) ([f4bed9a](https://github.com/sct/overseerr/commit/f4bed9a63b6b856ebedca9eb7662cd00038d7f7c))
|
||||
* **ui:** display movie/series original title ([#1240](https://github.com/sct/overseerr/issues/1240)) ([7230915](https://github.com/sct/overseerr/commit/723091509414465e98d870b3dc943f41b9ac590d))
|
||||
* **ui:** experimental status bar style change for ios pwa app ([958cdf9](https://github.com/sct/overseerr/commit/958cdf98fd1cb7c1bdb33aebb6c061750e9ab331))
|
||||
* **ui:** store sort order and page size of userlist in localstorage ([#1262](https://github.com/sct/overseerr/issues/1262)) ([f5f8269](https://github.com/sct/overseerr/commit/f5f8269cd28ee792120060f4f38ef09d571fb8d5))
|
||||
* add option to cache images locally ([#1213](https://github.com/sct/overseerr/issues/1213)) ([0ca3d43](https://github.com/sct/overseerr/commit/0ca3d4374942b54b59a19d017ab4ae14ba7019c1))
|
||||
* genre sliders (experiment) ([#1182](https://github.com/sct/overseerr/issues/1182)) ([1c4515a](https://github.com/sct/overseerr/commit/1c4515a1ae6097f3948aaa0d0ed210831581fd98))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* **ui:** remove local image cache option from settings page ([911faef](https://github.com/sct/overseerr/commit/911faeff562b737a2d18a395fcd90bf354af0cc4))
|
||||
* remove experimental tailwind jit compiler until title card hover is fixed ([1df67ba](https://github.com/sct/overseerr/commit/1df67baf9e7cdabc4045a0c115735797e8081bca))
|
||||
* **deps:** revert react-intl to 5.13.5 ([e16277c](https://github.com/sct/overseerr/commit/e16277c07d58ddbb749f4a60bc05924f4a5af146))
|
||||
|
||||
## [1.21.1](https://github.com/sct/overseerr/compare/v1.21.0...v1.21.1) (2021-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **lang:** translations update from Weblate ([#1155](https://github.com/sct/overseerr/issues/1155)) ([ebc285c](https://github.com/sct/overseerr/commit/ebc285c758f69846e4a5cb74bb42ca5924d166d4))
|
||||
|
||||
# [1.21.0](https://github.com/sct/overseerr/compare/v1.20.1...v1.21.0) (2021-03-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not allow editing of user settings under certain conditions ([#1168](https://github.com/sct/overseerr/issues/1168)) ([001dcd3](https://github.com/sct/overseerr/commit/001dcd328c8d3b1c417fd7c7ee2aa20183b08eef))
|
||||
* **frontend:** check for ID instead of email after initial setup Plex login ([#1097](https://github.com/sct/overseerr/issues/1097)) ([778dda6](https://github.com/sct/overseerr/commit/778dda67d54df87347dd79577ef1bdc88d3c1d3f))
|
||||
* **frontend:** check if swr is validating to determine if we should fetch new data ([e5f5bdb](https://github.com/sct/overseerr/commit/e5f5bdb95c62eba31a3321a7457d354f0226bf85)), closes [#719](https://github.com/sct/overseerr/issues/719)
|
||||
* **frontend:** never hide available content in search results ([d48edeb](https://github.com/sct/overseerr/commit/d48edeb5a9bd8e2edce8bca0fea50e300bb7a1ae))
|
||||
* **lang:** add missing i18n strings ([6072e8a](https://github.com/sct/overseerr/commit/6072e8aa9a0f84e50c44a92af303aad15b5f3021))
|
||||
* **lang:** edit new Telegram-related strings to conform to style guide ([#1093](https://github.com/sct/overseerr/issues/1093)) ([bdf67e7](https://github.com/sct/overseerr/commit/bdf67e732b6c77cbae768a25edfc9a663ef0108b))
|
||||
* **notif:** loosen input validation on Pushover settings ([#1166](https://github.com/sct/overseerr/issues/1166)) ([3148d31](https://github.com/sct/overseerr/commit/3148d312141248653c5d1e42cd2882a67a339163))
|
||||
* **notif:** set URL for Discord embeds rather than adding a field for the link ([#1167](https://github.com/sct/overseerr/issues/1167)) ([0bd0912](https://github.com/sct/overseerr/commit/0bd0912613f0db24bd0da4ec956b5119133e35d4))
|
||||
* correctly send auto-approval notifictions for series ([8634081](https://github.com/sct/overseerr/commit/8634081c869a2078793ecf06b1b7e249bba0a2f8))
|
||||
* **lang:** fix singular form of season count ([#1080](https://github.com/sct/overseerr/issues/1080)) ([b57645d](https://github.com/sct/overseerr/commit/b57645d382361c856281e7a74295afe16c5390f2))
|
||||
* **requests:** add plex url to request item ([#1088](https://github.com/sct/overseerr/issues/1088)) ([420038d](https://github.com/sct/overseerr/commit/420038d5ffdd4070df03e5c5cb6ef8d6208fddb5))
|
||||
* **sonarr:** correctly search when updating existing sonarr series ([ed0a7fb](https://github.com/sct/overseerr/commit/ed0a7fbdf5122a26fa936e83b76a97c55781782d)), closes [#588](https://github.com/sct/overseerr/issues/588)
|
||||
* **ui:** add alt prop to studio/network logos & fix blinking text cursor ([#1095](https://github.com/sct/overseerr/issues/1095)) ([0c4637f](https://github.com/sct/overseerr/commit/0c4637f779d8904037b9cbd5fe9166cf05a891c5))
|
||||
* **ui:** add link to poster image on request items ([7289872](https://github.com/sct/overseerr/commit/7289872937d5bb94d027424760ee1ceb94095604))
|
||||
* **ui:** correct language usage re: "sync" vs. "scan" ([#1079](https://github.com/sct/overseerr/issues/1079)) ([e98f2b9](https://github.com/sct/overseerr/commit/e98f2b96058fb9c5af77be2e8a1bd07fb8fcca06))
|
||||
* **ui:** display "Season" vs. "Seasons" as appropriate, and fix request block "Seasons" formatting ([#1127](https://github.com/sct/overseerr/issues/1127)) ([45886cc](https://github.com/sct/overseerr/commit/45886ccef1bee57dc555060a491834567e45b59c))
|
||||
* **ui:** request list button sizes ([#1152](https://github.com/sct/overseerr/issues/1152)) ([fc73592](https://github.com/sct/overseerr/commit/fc73592b69c38191f91a68a020868b8e5ec2e2e2))
|
||||
* fix language filter link on movie detail pages ([#1142](https://github.com/sct/overseerr/issues/1142)) ([60d453b](https://github.com/sct/overseerr/commit/60d453b0bbba5e2060f72f40d1dde85ec6b05af4))
|
||||
* remove language/region filtering on studio/network results ([#1129](https://github.com/sct/overseerr/issues/1129)) ([109aca8](https://github.com/sct/overseerr/commit/109aca8229dc7b81cac314d84591f1c04c12ac2e))
|
||||
* **api:** check correct permissions for auto approve when requests are created ([3c1a72b](https://github.com/sct/overseerr/commit/3c1a72b038fd178b4be4dc082cd1496474148d7e))
|
||||
* **frontend:** status, requested by, and modified alignment fix ([#1109](https://github.com/sct/overseerr/issues/1109)) ([1a7dc1a](https://github.com/sct/overseerr/commit/1a7dc1acf57888d3d0285b58c1c97a824a232216))
|
||||
* **ui:** don't show "Password" user settings tab if current user lacks perms to modify the password ([#1063](https://github.com/sct/overseerr/issues/1063)) ([b146d11](https://github.com/sct/overseerr/commit/b146d11e2ffecedae76472b0491a4662ca4a4a4e))
|
||||
* **ui:** fix Radarr logo alignment ([#1068](https://github.com/sct/overseerr/issues/1068)) ([0fa005a](https://github.com/sct/overseerr/commit/0fa005a99cd868b5a235ae9ce65b4c64b05d0f47))
|
||||
* **ui:** fix request list UI behavior when season list is too long ([#1106](https://github.com/sct/overseerr/issues/1106)) ([8507691](https://github.com/sct/overseerr/commit/85076919c6ccbf052699b7d5f4ba8b6e5e5af74d))
|
||||
* **ui:** improve responsive design on new request list UI ([#1105](https://github.com/sct/overseerr/issues/1105)) ([1f8b03f](https://github.com/sct/overseerr/commit/1f8b03ff6f67ce76051667de05166da54ed3dc89))
|
||||
* **ui:** list all movie studios instead of just the first result ([#1110](https://github.com/sct/overseerr/issues/1110)) ([239202d](https://github.com/sct/overseerr/commit/239202d9c11f27410b0fa084bcc4c824b7136081))
|
||||
* add correct permission checks to modifying user password/permissions ([ddfc5e6](https://github.com/sct/overseerr/commit/ddfc5e6aa8fc636931f495d6f23d56367466e3b5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add tagline, episode runtime, genres list to media details & clean/refactor CSS into globals ([#1160](https://github.com/sct/overseerr/issues/1160)) ([2f2e002](https://github.com/sct/overseerr/commit/2f2e00237d43bdab85bfadc3c4f2fbcdde4c2e90))
|
||||
* **docker:** add tini to docker image ([#1017](https://github.com/sct/overseerr/issues/1017)) ([1629d02](https://github.com/sct/overseerr/commit/1629d02f3d8368bfd5f6fed05382974ae6fce51f))
|
||||
* **email:** add pgp support ([#1138](https://github.com/sct/overseerr/issues/1138)) ([9e5adeb](https://github.com/sct/overseerr/commit/9e5adeb610bdc4800ff536412d0ae8a11fb4338d))
|
||||
* **frontend:** add loading bar indicator ([#1170](https://github.com/sct/overseerr/issues/1170)) ([3d6b343](https://github.com/sct/overseerr/commit/3d6b3434138fec49c58f2bf74f781d5e2fc2911f))
|
||||
* **lang:** localize job names ([#1043](https://github.com/sct/overseerr/issues/1043)) ([594aad9](https://github.com/sct/overseerr/commit/594aad9d3ae9b323677f3af8c434d7664526593d))
|
||||
* **lang:** translations update from Weblate ([#1051](https://github.com/sct/overseerr/issues/1051)) ([69bf817](https://github.com/sct/overseerr/commit/69bf817f598babed99964f073259f827b60bd014))
|
||||
* **lang:** Translations update from Weblate ([#1131](https://github.com/sct/overseerr/issues/1131)) ([e4686d6](https://github.com/sct/overseerr/commit/e4686d664b52448e32488ff1c4236f72e01e9a29))
|
||||
* **notif:** add "Media Automatically Approved" notification type ([#1137](https://github.com/sct/overseerr/issues/1137)) ([f7d2723](https://github.com/sct/overseerr/commit/f7d2723fab2c30564fd23945709cd39b178a6eef))
|
||||
* **notif:** add settings for Discord bot username & avatar URL ([#1113](https://github.com/sct/overseerr/issues/1113)) ([3384eb1](https://github.com/sct/overseerr/commit/3384eb1c479114c0246cb22f9a933aa79fb95fcf))
|
||||
* **notif:** include poster image in Telegram notifications ([#1112](https://github.com/sct/overseerr/issues/1112)) ([48387e5](https://github.com/sct/overseerr/commit/48387e5b2f26c0c33acd436c6e1cf902d6c32101))
|
||||
* **scan:** add support for new plex tv agent ([#1144](https://github.com/sct/overseerr/issues/1144)) ([a51d2a2](https://github.com/sct/overseerr/commit/a51d2a24d51d092a0c6da608e3322f19a37c2d28))
|
||||
* **ui:** add user ID to profile header ([6e95c8b](https://github.com/sct/overseerr/commit/6e95c8b7a10e3467bfd2c3df84ccf886fe01ca5c))
|
||||
* add genre/studio/network view to Discover results ([#1067](https://github.com/sct/overseerr/issues/1067)) ([f28112f](https://github.com/sct/overseerr/commit/f28112f057df2589f31ae0d0b14e8b50e479fdb7))
|
||||
* add language-filtered Discover pages ([#1111](https://github.com/sct/overseerr/issues/1111)) ([7501161](https://github.com/sct/overseerr/commit/75011610e57f03098c8be9375d0c9ba1e3647e9b))
|
||||
* add studio/network sliders to discover ([1c6914f](https://github.com/sct/overseerr/commit/1c6914f5ce5c0d171c4609813915b50233a8e3ad))
|
||||
* **telegram:** add support for individual chat notifications ([#1027](https://github.com/sct/overseerr/issues/1027)) ([f6d00d8](https://github.com/sct/overseerr/commit/f6d00d8d1559879189f83739193c6e2acafde51d))
|
||||
* **ui:** display "Owner" role instead of "Admin" for user ID 1 ([#1050](https://github.com/sct/overseerr/issues/1050)) ([1b55d2d](https://github.com/sct/overseerr/commit/1b55d2dfbc06d900e7370a4ddfd81789a25bf00c))
|
||||
* **ui:** display season count on TV details page ([#1078](https://github.com/sct/overseerr/issues/1078)) ([4365231](https://github.com/sct/overseerr/commit/436523139e8f1594c352b17032734b4498d3994f))
|
||||
* **ui:** in Settings > Services, make Radarr/Sonarr server names and logos clickable links ([#1008](https://github.com/sct/overseerr/issues/1008)) ([6a1e389](https://github.com/sct/overseerr/commit/6a1e3891aa5f84b6adb1e475a6658a8cd4e34c22))
|
||||
* **ui:** request list redesign ([#1099](https://github.com/sct/overseerr/issues/1099)) ([cd21865](https://github.com/sct/overseerr/commit/cd21865c4d5be00c13c372e0b7a058f61ec855a2))
|
||||
|
||||
## [1.20.1](https://github.com/sct/overseerr/compare/v1.20.0...v1.20.1) (2021-02-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **notif:** escape application title in Telegram notifications ([#1012](https://github.com/sct/overseerr/issues/1012)) ([5560abf](https://github.com/sct/overseerr/commit/5560abf459b0350ff30b5e71d4208418fc8f3b3e))
|
||||
* **notif:** fixed typo in pushover hint ([#1029](https://github.com/sct/overseerr/issues/1029)) ([e9f2fe9](https://github.com/sct/overseerr/commit/e9f2fe910d72fa41bc27673ed43291211c3cac65))
|
||||
* **notifications:** correctly send tv auto approval notifications ([537850f](https://github.com/sct/overseerr/commit/537850f414a88df24c78794a2fd68e1e24ff73d1)), closes [#1041](https://github.com/sct/overseerr/issues/1041)
|
||||
* **plex-sync:** no longer incorrectly sets 4k availability when there isnt any ([3f9a116](https://github.com/sct/overseerr/commit/3f9a116b17d78eeb04f0f125a4f3af6f907c83dd)), closes [#990](https://github.com/sct/overseerr/issues/990)
|
||||
* **ui:** for server default options, display "All" region/language option instead of empty string ([#1042](https://github.com/sct/overseerr/issues/1042)) ([3fed26c](https://github.com/sct/overseerr/commit/3fed26cfbe74cb662ca531fd37b69f159a051ac1))
|
||||
* **ui:** show translated string on sonarr sucesss/failure toast messages ([#1035](https://github.com/sct/overseerr/issues/1035)) ([eefcbcd](https://github.com/sct/overseerr/commit/eefcbcd3ddfa5258ee24dbbbd79de5bf50310f27))
|
||||
* **ui:** use country-flag-icons instead of country-flag-emoji for RegionSelector ([#1011](https://github.com/sct/overseerr/issues/1011)) ([abcd7c9](https://github.com/sct/overseerr/commit/abcd7c997584c1310bd8b313ac38f30e335af8d7))
|
||||
* add missing default value for settings context ([084917f](https://github.com/sct/overseerr/commit/084917f02d399e2d29bb9927e033c2e6533f586c))
|
||||
* added missing language default for ssr context defaults ([9ce88ab](https://github.com/sct/overseerr/commit/9ce88abcc85d744d77172cd2357fdb4ff60dc5e4))
|
||||
* allow users to override language/region settings ([69294a7](https://github.com/sct/overseerr/commit/69294a7c4c5bbe55c5cd276786cdfd48ddbff889)), closes [#1013](https://github.com/sct/overseerr/issues/1013)
|
||||
|
||||
# [1.20.0](https://github.com/sct/overseerr/compare/v1.19.1...v1.20.0) (2021-02-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** add isAuthenticated middleware to base user route ([8a27c70](https://github.com/sct/overseerr/commit/8a27c7062599ea23dca115e6e6e95a594e1b219a))
|
||||
* **api:** sort users requests by most recent ([1798383](https://github.com/sct/overseerr/commit/17983837fc10661a59d29fc1531530fca0d77825))
|
||||
* **api:** Use POST instead of GET for API endpoints that mutate state ([#877](https://github.com/sct/overseerr/issues/877)) ([ff0b5ed](https://github.com/sct/overseerr/commit/ff0b5ed44132cc5a0cd178035796d042ba735a8d))
|
||||
* **auth:** handle sign-in attempts from emails with no password ([#933](https://github.com/sct/overseerr/issues/933)) ([5e37a96](https://github.com/sct/overseerr/commit/5e37a96bc017471f8dc4cbdd57f2e8c3568bd97f))
|
||||
* **frontend:** changed plex, request, and cog buttons to align properly on smaller mobile UIs ([#928](https://github.com/sct/overseerr/issues/928)) ([f1c3358](https://github.com/sct/overseerr/commit/f1c335815f2f17465cdd36ceb223e78a58149b3b))
|
||||
* **frontend:** check for id instead of email after logging in ([c4af4c4](https://github.com/sct/overseerr/commit/c4af4c42ab00f1a63a2f5326c9cd8b26c19f4f14))
|
||||
* **frontend:** Do not allow user w/ ID 1 to disable 'Admin' permission ([#965](https://github.com/sct/overseerr/issues/965)) ([77b2d9e](https://github.com/sct/overseerr/commit/77b2d9ea22a2f70cff58ac9421f3f6231bc93059))
|
||||
* **frontend:** handle empty array of media attributes ([#922](https://github.com/sct/overseerr/issues/922)) ([04fa9f7](https://github.com/sct/overseerr/commit/04fa9f79e2ec90082b3fa15590dd170f7d68ad52))
|
||||
* **frontend:** request and cog button would be misaligned without play on plex/watch trailer button ([#956](https://github.com/sct/overseerr/issues/956)) ([e28dfad](https://github.com/sct/overseerr/commit/e28dfadaf57d47887013c31dc5006332473156e3))
|
||||
* **frontend:** Update AdvancedRequester to reflect new /user API response ([#970](https://github.com/sct/overseerr/issues/970)) ([b4bac6a](https://github.com/sct/overseerr/commit/b4bac6a9157119a4f234933245944e133c127bd0))
|
||||
* **frontend:** use region settings instead of hardcoded 'US' value for movie/TV ratings ([#1006](https://github.com/sct/overseerr/issues/1006)) ([6ecd202](https://github.com/sct/overseerr/commit/6ecd202607cb48d559440da810ecc585e740542b))
|
||||
* **lang:** formatMessage should not use an object spread ([8a7fa00](https://github.com/sct/overseerr/commit/8a7fa00164fd5c5501da525baa29be97bac7e7c4))
|
||||
* **lang:** Remove unused strings and correct spelling of 'canceling'/'canceled' ([#981](https://github.com/sct/overseerr/issues/981)) ([5b64655](https://github.com/sct/overseerr/commit/5b646557765d1ad75e44e1c0e60e0291313c7746))
|
||||
* **login:** fix the gap when 'use your overseer account' was selected ([#870](https://github.com/sct/overseerr/issues/870)) ([d163e29](https://github.com/sct/overseerr/commit/d163e294599c4bd9bdc0a148db15c8e8541410d8))
|
||||
* **notif:** Do not HTML-escape email subjects ([#931](https://github.com/sct/overseerr/issues/931)) ([019622a](https://github.com/sct/overseerr/commit/019622aab1b94cc4d71cacbf0dc5cf64b62c8623))
|
||||
* **notif:** Remove extra newlines from Telegram notifications ([#973](https://github.com/sct/overseerr/issues/973)) ([bbea522](https://github.com/sct/overseerr/commit/bbea52249950eb98a8d3886f2bd7648a7d669bf4))
|
||||
* **plex:** Check Plex server access on user import ([#955](https://github.com/sct/overseerr/issues/955)) ([bdb3cb2](https://github.com/sct/overseerr/commit/bdb3cb202550e34d8951ac2b5015f97f6a5c1ebf))
|
||||
* **plex-sync:** get correct Plex metadata for Hama movie items ([#901](https://github.com/sct/overseerr/issues/901)) ([03cecb3](https://github.com/sct/overseerr/commit/03cecb33559e27199c5a174fc86de0c4550fe666)), closes [#898](https://github.com/sct/overseerr/issues/898)
|
||||
* **requests:** correctly filter requests out for users without view requests permission ([e118501](https://github.com/sct/overseerr/commit/e118501bf1dfa8dada2c57090e62631de620f3dd))
|
||||
* **requests:** correctly handle when tvdbid is missing ([#891](https://github.com/sct/overseerr/issues/891)) ([e037ba4](https://github.com/sct/overseerr/commit/e037ba48f173c06b0c9c8b03085edf832d770c06))
|
||||
* **search:** Handle search errors and escape * ([#893](https://github.com/sct/overseerr/issues/893)) ([034968e](https://github.com/sct/overseerr/commit/034968e4370eaea726c94730274349c083856813))
|
||||
* **services:** update all radarr/sonarr endpoints to use v3 ([da5ca02](https://github.com/sct/overseerr/commit/da5ca02f81fe91070afbda3e1ebc8d869fe39a8f))
|
||||
* **sonarr:** use qualityProfileId instad of profileId when adding series ([552a7e3](https://github.com/sct/overseerr/commit/552a7e30da5fc2cc0bd43b5aef79a0225c75d233))
|
||||
* **sync:** fix sonarr/plex sync fighting over availability ([9b73423](https://github.com/sct/overseerr/commit/9b73423d49e1e799cd82764a9ade8c75d92a28a2)), closes [#872](https://github.com/sct/overseerr/issues/872)
|
||||
* **ui:** add fallback for region display name ([f9c83e1](https://github.com/sct/overseerr/commit/f9c83e14e52a57d6865307b3324a61c04a77a541))
|
||||
* **ui:** add missing string for default Discover Language & edit string for default Discover Region ([#1004](https://github.com/sct/overseerr/issues/1004)) ([0acad8e](https://github.com/sct/overseerr/commit/0acad8e9fa65a9de6cecac9b6a4a5b2313ba8f06))
|
||||
* **ui:** Add tip & validation for Discord ID input ([#966](https://github.com/sct/overseerr/issues/966)) ([e70a4ec](https://github.com/sct/overseerr/commit/e70a4ecae613e045977e262fd7f9643f30985ab7))
|
||||
* **ui:** also allow 17 digit discord ids ([57c00c1](https://github.com/sct/overseerr/commit/57c00c1ea71c1229d5a59e1b8dadd84a646772b9)), closes [#971](https://github.com/sct/overseerr/issues/971)
|
||||
* **ui:** Automatically disable and uncheck user permissions with unmet requirements ([#941](https://github.com/sct/overseerr/issues/941)) ([c9a150b](https://github.com/sct/overseerr/commit/c9a150b1db2adbb305cf1a448489d7a8c14cf1cb))
|
||||
* **ui:** change font size in request list/user list dropdowns to prevent zoom on mobile ([fb9c878](https://github.com/sct/overseerr/commit/fb9c878db49c01d13773e8d2f94c93f840be0b82))
|
||||
* **ui:** Display 4K download status on 4K status badge ([#988](https://github.com/sct/overseerr/issues/988)) ([40b07c3](https://github.com/sct/overseerr/commit/40b07c35d40c03039e4bfa5ed1e73af7e8aa6a7d))
|
||||
* **ui:** Fix card sizes on person detail pages ([#881](https://github.com/sct/overseerr/issues/881)) ([a3042f8](https://github.com/sct/overseerr/commit/a3042f8e1b05a91d98f48a4aecb08e831a48fc56))
|
||||
* **ui:** Fix settings navigation horizontal scroll issues ([#987](https://github.com/sct/overseerr/issues/987)) ([8701fb2](https://github.com/sct/overseerr/commit/8701fb20d07773f4cc32e857b68575a813cf7e21))
|
||||
* **ui:** fix webhook URL validation regex ([baad19a](https://github.com/sct/overseerr/commit/baad19a2c94728313ee996fe1a0ffc64fbd9aaa3))
|
||||
* **ui:** fixed anime language profile typo ([#879](https://github.com/sct/overseerr/issues/879)) ([ee50761](https://github.com/sct/overseerr/commit/ee5076146ef3c5e8baba197a5b397d3c3f575262))
|
||||
* **ui:** Handle missing movie/series data ([#862](https://github.com/sct/overseerr/issues/862)) ([7c0ddad](https://github.com/sct/overseerr/commit/7c0ddad653393327226a877692f046d8693ddc66))
|
||||
* **ui:** Notification-related string/UI edits and field validation ([#985](https://github.com/sct/overseerr/issues/985)) ([c88fcb2](https://github.com/sct/overseerr/commit/c88fcb2e2d1c4b84527844a80680c15337626e72))
|
||||
* **ui:** rename global group class to form-group ([8056187](https://github.com/sct/overseerr/commit/8056187c3c0ea464a8f751aa6347ea1d35c01aac))
|
||||
* **ui:** Size cards appropriately based on base font size ([#871](https://github.com/sct/overseerr/issues/871)) ([282f28f](https://github.com/sct/overseerr/commit/282f28f2b9d0cc8c9105d01b43d4e1f730320b8b))
|
||||
* **ui/notif:** Custom application title in password-related emails and UI messages ([#979](https://github.com/sct/overseerr/issues/979)) ([4e2706b](https://github.com/sct/overseerr/commit/4e2706b4211b06f364910c327d84c2ceb45b2fe3))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **lang:** translated using Weblate (French) ([#1007](https://github.com/sct/overseerr/issues/1007)) ([970da66](https://github.com/sct/overseerr/commit/970da664b2700b8cd9ad8dce0cbca1d37820eceb))
|
||||
* **lang:** translations update from Weblate ([#853](https://github.com/sct/overseerr/issues/853)) ([e156acc](https://github.com/sct/overseerr/commit/e156acc1ae2fa86b4441faacc0b58e1e993e0edc))
|
||||
* **lang:** translations update from Weblate ([#986](https://github.com/sct/overseerr/issues/986)) ([4296765](https://github.com/sct/overseerr/commit/4296765ad61bac09c2317b71b763366d328733e4))
|
||||
* **notif:** Add Pushbullet notification agent ([#950](https://github.com/sct/overseerr/issues/950)) ([29b97ef](https://github.com/sct/overseerr/commit/29b97ef6d85bbea31dd59b7ad857b0d8ab30bff0))
|
||||
* **notif:** Notification improvements ([#914](https://github.com/sct/overseerr/issues/914)) ([2768155](https://github.com/sct/overseerr/commit/2768155bbabe121a4c51fc1472461cd5114c4300))
|
||||
* **regions:** add region/original language setting for filtering Discover ([#732](https://github.com/sct/overseerr/issues/732)) ([#942](https://github.com/sct/overseerr/issues/942)) ([b557c06](https://github.com/sct/overseerr/commit/b557c06b0a78f5df5f64a05dc1e4511dae72df4f))
|
||||
* **requests:** add language profile support ([#860](https://github.com/sct/overseerr/issues/860)) ([53f6f59](https://github.com/sct/overseerr/commit/53f6f59798fa7e3f95959990a3df555db3c1c51e))
|
||||
* **ui:** Add 'Available' filter to request list and remove unused MediaRequestStatus.AVAILABLE enum value ([#905](https://github.com/sct/overseerr/issues/905)) ([9757e3a](https://github.com/sct/overseerr/commit/9757e3ae0c572fb46177e25154b29e0ceced665f))
|
||||
* **ui:** Add 'Page Size' setting for request/user list pages ([#957](https://github.com/sct/overseerr/issues/957)) ([621db89](https://github.com/sct/overseerr/commit/621db893281f0280fe773ac7dbdc44434895242c))
|
||||
* **ui:** Add separate permissions for 4K auto approval ([#908](https://github.com/sct/overseerr/issues/908)) ([53b7425](https://github.com/sct/overseerr/commit/53b7425f6711e250935e7bb024c38ff6c62e07d9))
|
||||
* **ui:** Add sort options to user list ([#913](https://github.com/sct/overseerr/issues/913)) ([ef5d019](https://github.com/sct/overseerr/commit/ef5d019c18d7f6cdbbb1e1b7f8ff7816ed9b117b))
|
||||
* **ui:** Add support for requesting collections in 4K ([#968](https://github.com/sct/overseerr/issues/968)) ([139341b](https://github.com/sct/overseerr/commit/139341b0434b41e7c31af36baacd8d65566a6a0c))
|
||||
* user profile/settings pages ([#958](https://github.com/sct/overseerr/issues/958)) ([bbb683e](https://github.com/sct/overseerr/commit/bbb683e637386ad8bbeb44dca97aac9cdaf11349))
|
||||
* **ui:** added content ratings for tv shows and movie ratings ([#878](https://github.com/sct/overseerr/issues/878)) ([c8b2a57](https://github.com/sct/overseerr/commit/c8b2a57721a51adcc7f90ec1acb48b127991d467))
|
||||
* **users:** add reset password flow ([#772](https://github.com/sct/overseerr/issues/772)) ([e5966bd](https://github.com/sct/overseerr/commit/e5966bd3fbfe172f264f4e986ad2aecf29ae1510))
|
||||
|
||||
## [1.19.1](https://github.com/sct/overseerr/compare/v1.19.0...v1.19.1) (2021-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ui:** Fix webhook URL validation regex ([#864](https://github.com/sct/overseerr/issues/864)) ([726f62b](https://github.com/sct/overseerr/commit/726f62b9b69b5078e718f129e26abdf358f5cb06))
|
||||
|
||||
# [1.19.0](https://github.com/sct/overseerr/compare/v1.18.0...v1.19.0) (2021-02-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** filter out adult content from combined credits ([3052f12](https://github.com/sct/overseerr/commit/3052f12c91b3ce86128324e3698fff61bbce3f2a))
|
||||
* **cache:** use formatted numbers for displaying cache counts ([6c437c5](https://github.com/sct/overseerr/commit/6c437c515fc01b9fe4461968875e23542bae7542))
|
||||
* **email:** make image a link to the action url in request template ([ee0a7bd](https://github.com/sct/overseerr/commit/ee0a7bd8c0b3a79c292b0abceb2f780f3889e49f)), closes [#834](https://github.com/sct/overseerr/issues/834)
|
||||
* **frontend:** add github sponsor link to about page ([7c192d5](https://github.com/sct/overseerr/commit/7c192d54f422a5f2b55750535d2382e313f1d011))
|
||||
* **frontend:** correctly show 4k download tracker activity ([a7314f8](https://github.com/sct/overseerr/commit/a7314f876ea528fdec0fb0a2adaa36a01afcdf38))
|
||||
* **frontend:** fix possible division by zero in download status ([#839](https://github.com/sct/overseerr/issues/839)) ([c97c96a](https://github.com/sct/overseerr/commit/c97c96a30c50db7735f06c6d2d2f6193fb7da55e))
|
||||
* **frontend:** match request button color on titlecards to other request buttons ([5b39911](https://github.com/sct/overseerr/commit/5b39911e024513fab7a62948e653cee08fd166c7))
|
||||
* **frontend:** set 4k status on RequestItem when request is for 4k ([a3b00c3](https://github.com/sct/overseerr/commit/a3b00c3458b868506d4158fb24f0369fa5daefc5))
|
||||
* **frontend:** use consistent spinner style on TitleCard/Plex Presets ([cf7ebc4](https://github.com/sct/overseerr/commit/cf7ebc488db33725444c428b4244d780ab9d123b))
|
||||
* **html:** th elements should be nested under tr, not directly under thead ([#801](https://github.com/sct/overseerr/issues/801)) ([6e9ac27](https://github.com/sct/overseerr/commit/6e9ac275e19d56de8c7a366db970c7321f26fc8a))
|
||||
* **lang:** Add missing source strings & remove local user sign-in setting tip ([#828](https://github.com/sct/overseerr/issues/828)) ([c0769d4](https://github.com/sct/overseerr/commit/c0769d4f8f2bad88e4638d8c3cbcc0414b3ef6fb))
|
||||
* **lang:** Edit English language strings ([#820](https://github.com/sct/overseerr/issues/820)) ([f54df21](https://github.com/sct/overseerr/commit/f54df214af86d90ea8d7cfcd4e39022215c3568c))
|
||||
* **lang:** translate language names & change zh-Hant language code to zh-TW ([#793](https://github.com/sct/overseerr/issues/793)) ([3c5ae36](https://github.com/sct/overseerr/commit/3c5ae360fd179d794a78cc918fe97a09216ca6b2))
|
||||
* **notif/ui:** Use custom application title in notifications & sign-in page ([#849](https://github.com/sct/overseerr/issues/849)) ([38c76b5](https://github.com/sct/overseerr/commit/38c76b55e0039c489cb6a4a0a298aa6385406db4))
|
||||
* **radarr:** correctly set requested status after sending to radarr (with auto approve) ([ec44841](https://github.com/sct/overseerr/commit/ec448413569ddc2f24bb856d29084169979f9f05))
|
||||
* **sonarr-sync:** sonarr sync will no longer set shows with no episodes to partially available ([d20bd53](https://github.com/sct/overseerr/commit/d20bd530edaadc5887b0361358da80153e36505c)), closes [#796](https://github.com/sct/overseerr/issues/796)
|
||||
* **ui:** Add additional URL & email input validation ([#843](https://github.com/sct/overseerr/issues/843)) ([3f9bfeb](https://github.com/sct/overseerr/commit/3f9bfeb01a67b2b587c7548b02ee826722e65c0f))
|
||||
* **ui:** Don't display empty dropdown when no trailer available ([#804](https://github.com/sct/overseerr/issues/804)) ([95c2a21](https://github.com/sct/overseerr/commit/95c2a2169799d96413b47ab24506b330435643eb))
|
||||
* **ui:** dont show bulk edit options on user list if there is only one user ([b658ddf](https://github.com/sct/overseerr/commit/b658ddf5cf61b2bb9b93cb1a4ca716cd75e18bb4))
|
||||
* **ui:** Dynamically generate path to config in warning message ([#851](https://github.com/sct/overseerr/issues/851)) ([b531a64](https://github.com/sct/overseerr/commit/b531a642f601f4ef9bf39c2f5915402157e55372))
|
||||
* **ui:** fix tables extending outside viewport in mobile formats ([e270999](https://github.com/sct/overseerr/commit/e270999745f97c2860f6a5b84e897dc6da8d6001))
|
||||
* **ui:** Hide 'Mark 4k as Available' button if 4k not enabled ([#833](https://github.com/sct/overseerr/issues/833)) ([e4a50c3](https://github.com/sct/overseerr/commit/e4a50c33f105b440243885d72a9e96595a525447))
|
||||
* **ui:** Limit max width of forms & lists ([#845](https://github.com/sct/overseerr/issues/845)) ([b9d14a9](https://github.com/sct/overseerr/commit/b9d14a9fd0f3c94d8267755147a87fe3b77fa2c3))
|
||||
* **ui:** prevent names from getting squished in AdvancedRequester user selector ([06e9411](https://github.com/sct/overseerr/commit/06e941171a1d019fbb178624167c026f6df5271c))
|
||||
* **ui:** remove yup validation from display name on user edit page ([63d7e2b](https://github.com/sct/overseerr/commit/63d7e2b39858fcb1cc0819a680eebccded7f4451))
|
||||
* **ui:** Restore original port input size ([#814](https://github.com/sct/overseerr/issues/814)) ([1ccafc0](https://github.com/sct/overseerr/commit/1ccafc0ebd368d798f9571b83910336efa317e37))
|
||||
* **ui:** show request as option even if there are no radarr/sonarr servers ([b116281](https://github.com/sct/overseerr/commit/b116281196c264b4ec35b07f1b4ffa717e50ade5))
|
||||
* **ui:** uniform-size checkboxes, vertically-aligned form labels, and fixes for other UI imperfections/inconsistencies ([#737](https://github.com/sct/overseerr/issues/737)) ([e34fbf7](https://github.com/sct/overseerr/commit/e34fbf72fda34d69b9f25563fa81f88b3c20912a))
|
||||
* **ui:** Use minimum char validation message ([#850](https://github.com/sct/overseerr/issues/850)) ([7456bea](https://github.com/sct/overseerr/commit/7456bea2ae600a28cb933278ffb310b63a474d6a))
|
||||
* **ui:** validate application url and service external urls ([026795d](https://github.com/sct/overseerr/commit/026795d4c940cb4797d3e68089456a4c3defbb21))
|
||||
* **ui:** when PersonCard has no profilePath, correctly position name/role content ([3ffd5ab](https://github.com/sct/overseerr/commit/3ffd5ab0ee8ffa63199d1428e37206f9b59fb7a5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cache:** add cache table and flush cache option to settings ([996bd9f](https://github.com/sct/overseerr/commit/996bd9f14ed0f56767892c169b071be4f0f628d0))
|
||||
* **cache:** external API cache ([#786](https://github.com/sct/overseerr/issues/786)) ([20289b5](https://github.com/sct/overseerr/commit/20289b5960a93545cdff9331a1a7b613f382e702))
|
||||
* **docker:** Check for /app/config volume mount during setup ([#826](https://github.com/sct/overseerr/issues/826)) ([1e5f88f](https://github.com/sct/overseerr/commit/1e5f88f462b0c69db5f6ab8e0249a5905bc6952a))
|
||||
* **frontend:** add TheTVDB external link ([#800](https://github.com/sct/overseerr/issues/800)) ([72cffd7](https://github.com/sct/overseerr/commit/72cffd74a75984ba98c456c0ec006ec378a8dcec))
|
||||
* **lang:** add support for Hungarian language ([cfacb15](https://github.com/sct/overseerr/commit/cfacb151b52d08e19d2fcd603fb4bbcd78707cdf))
|
||||
* **lang:** translations update from Weblate ([#791](https://github.com/sct/overseerr/issues/791)) ([42295e0](https://github.com/sct/overseerr/commit/42295e076a7579b226d57407a20cb0ba044e9ec1))
|
||||
* **lang:** translations update from Weblate ([#819](https://github.com/sct/overseerr/issues/819)) ([9e5e4c2](https://github.com/sct/overseerr/commit/9e5e4c22f5b25df96f47875d599ed8685791382a))
|
||||
* **lang:** translations update from Weblate ([#841](https://github.com/sct/overseerr/issues/841)) ([e4f9b8a](https://github.com/sct/overseerr/commit/e4f9b8a9848f3af00e86fc7108c823ed0584609f))
|
||||
* **lang:** translations update from Weblate ([#852](https://github.com/sct/overseerr/issues/852)) ([c5be00e](https://github.com/sct/overseerr/commit/c5be00eebfd2b0e65295edbe282cbba22fffa660))
|
||||
* **ui:** Add local login setting ([#817](https://github.com/sct/overseerr/issues/817)) ([9d0d5b8](https://github.com/sct/overseerr/commit/9d0d5b86aae025e4647bb664c6412d42192e2fe7))
|
||||
* **ui:** added next airing date to TV Shows ([#842](https://github.com/sct/overseerr/issues/842)) ([4eae02a](https://github.com/sct/overseerr/commit/4eae02a7e14e377fd69ddd4a43774cb7e3d1855b))
|
||||
* new permission to allow users to see other users requests ([033ba9d](https://github.com/sct/overseerr/commit/033ba9d41bddf6dc1c4512d8404f747e57923bca)), closes [#840](https://github.com/sct/overseerr/issues/840)
|
||||
* request as another user ([59150f9](https://github.com/sct/overseerr/commit/59150f955f7003672ef19eb9d37156e93b79c97d))
|
||||
* **tv:** show cast for the entire show instead of only the last season ([#778](https://github.com/sct/overseerr/issues/778)) ([b239598](https://github.com/sct/overseerr/commit/b239598e64d33b78dc5d7972878840149aff360a)), closes [#775](https://github.com/sct/overseerr/issues/775)
|
||||
* **ui:** Add custom title functionality ([#825](https://github.com/sct/overseerr/issues/825)) ([35c6bfc](https://github.com/sct/overseerr/commit/35c6bfc0216bf879353b3ee546b439a06c8e6121))
|
||||
|
||||
# [1.18.0](https://github.com/sct/overseerr/compare/v1.17.2...v1.18.0) (2021-01-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** prevent duplicate movie requests ([421f4c1](https://github.com/sct/overseerr/commit/421f4c17f0f206bbe7bfcbf2819014b8c7f55b6a)), closes [#705](https://github.com/sct/overseerr/issues/705)
|
||||
* **build:** fix sqlite3 build error ([#691](https://github.com/sct/overseerr/issues/691)) ([3a1f6d5](https://github.com/sct/overseerr/commit/3a1f6d5706c8fc100e88425f3d89a26a0325af79))
|
||||
* **frontend:** add poster not found image to request card and request list item ([ae9a1b3](https://github.com/sct/overseerr/commit/ae9a1b3e940ac2abf6e842d91f458daab3dd0f0d))
|
||||
* **frontend:** add poster not found image to tv details page ([0b05545](https://github.com/sct/overseerr/commit/0b055458d0ddbfd4c87ebf9b0562f161fa3445a3))
|
||||
* **frontend:** dont show external links unless slug is set ([946bd2d](https://github.com/sct/overseerr/commit/946bd2db5ecde0748b2e9bc5edfe7ca6000ec3d5))
|
||||
* **frontend:** fix server name position on plex settings page ([86efcd8](https://github.com/sct/overseerr/commit/86efcd82c34ad6490f2899ebf6f84cdd1bffc498))
|
||||
* **frontend:** fixed mismatched rounded sizing on new login ([5e352c2](https://github.com/sct/overseerr/commit/5e352c201fc2f731ca5f713ecb6901527ef354da)), closes [#721](https://github.com/sct/overseerr/issues/721)
|
||||
* **ip logging:** add env var for proxy to fix ip logging on failed logins ([#756](https://github.com/sct/overseerr/issues/756)) ([9342a40](https://github.com/sct/overseerr/commit/9342a40bbc03f7fdda23e3876b3a4a81ea8532c0))
|
||||
* **lang:** add missing i18n strings for notification settings ([2f75c4c](https://github.com/sct/overseerr/commit/2f75c4c6aed42a15bb47d3652272de8f852ec79f))
|
||||
* **notifications:** only send a single notification when standard media becomes available ([b5fd1d5](https://github.com/sct/overseerr/commit/b5fd1d520cd2a7be6e6356a25129e93af1caf542)), closes [#770](https://github.com/sct/overseerr/issues/770)
|
||||
* **permissions:** use default user permissions when creating a local user ([#713](https://github.com/sct/overseerr/issues/713)) ([660ada0](https://github.com/sct/overseerr/commit/660ada0b2025eb2c06d9054fd0a7b5a632af6af2))
|
||||
* **radarr:** fix request bug which made it unable to be added to radarr ([#760](https://github.com/sct/overseerr/issues/760)) ([45a2779](https://github.com/sct/overseerr/commit/45a277964b0c39346d7216873812e0ebe505cb79))
|
||||
* **radarr:** return the updated data when updating radarr request ([#765](https://github.com/sct/overseerr/issues/765)) ([0c6d478](https://github.com/sct/overseerr/commit/0c6d4780c355ffe1a951268fb6949491d435bbf1))
|
||||
* **requests:** handle when tvdbid is null ([#657](https://github.com/sct/overseerr/issues/657)) ([2da0da8](https://github.com/sct/overseerr/commit/2da0da826ae1d73467bc8a671fda7cc5ca1f14c9))
|
||||
* **sonarr-sync:** correctly set series with no seasons to requested status ([3812989](https://github.com/sct/overseerr/commit/3812989a1ce1e07d4af09149008043a6e2e94060)), closes [#762](https://github.com/sct/overseerr/issues/762)
|
||||
* **sync:** do not update series status if already available and no new seasons ([136d874](https://github.com/sct/overseerr/commit/136d874cba37babf9c0670844b002871710e6d99)), closes [#777](https://github.com/sct/overseerr/issues/777)
|
||||
* **ui:** Capitalization, punctuation, and grammar inconsistences & errors ([#731](https://github.com/sct/overseerr/issues/731)) ([f05d4a0](https://github.com/sct/overseerr/commit/f05d4a0d0b42905fcaee49b2471bb1f4ee77fffe))
|
||||
* lookup movie by imdbid if tmdbid does not exits for plex movie agent ([#711](https://github.com/sct/overseerr/issues/711)) ([e972288](https://github.com/sct/overseerr/commit/e97228899a5936b2525c8060abfa14b5ce31658d))
|
||||
* show recently added series even if they are not complete ([d0c830e](https://github.com/sct/overseerr/commit/d0c830e80d389f9e0f48a9b83659331f54630d03))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **lang:** translated using Weblate (Dutch) ([059995e](https://github.com/sct/overseerr/commit/059995e0ef3370a3192bd386fa6875ca0f58690a))
|
||||
* **lang:** translated using Weblate (French) ([4789583](https://github.com/sct/overseerr/commit/4789583d66305ac7b3d393659b2f3604c0acc576))
|
||||
* **lang:** translations update from Weblate ([#727](https://github.com/sct/overseerr/issues/727)) ([71875ef](https://github.com/sct/overseerr/commit/71875efb48246dbb0139ad15a4261a5661fcfe17))
|
||||
* **lang:** update languages and fix merge conflict ([083a74a](https://github.com/sct/overseerr/commit/083a74a686d202cce5775bf9752caaa9a626cf45))
|
||||
* **ui:** Move PROXY setting to UI ([#782](https://github.com/sct/overseerr/issues/782)) ([f1dd5e7](https://github.com/sct/overseerr/commit/f1dd5e7e12c1f602449c4769173dbce71e3569d0))
|
||||
* add manual availability buttons to manage slideover ([67f8aef](https://github.com/sct/overseerr/commit/67f8aef00d98c834b60cb6152ccd5cb7b5709d12)), closes [#672](https://github.com/sct/overseerr/issues/672)
|
||||
* **media:** add link to the item on plex ([#735](https://github.com/sct/overseerr/issues/735)) ([1d7150c](https://github.com/sct/overseerr/commit/1d7150c24ec5ad347093889bfceab61b664900d5))
|
||||
* Radarr & Sonarr Sync ([#734](https://github.com/sct/overseerr/issues/734)) ([ec5fb83](https://github.com/sct/overseerr/commit/ec5fb836785855eb4846fd33b49faeb94c40506a))
|
||||
* **frontend:** add option to hide all available items from discovery ([#699](https://github.com/sct/overseerr/issues/699)) ([6c1742e](https://github.com/sct/overseerr/commit/6c1742e94ccfc6c13cf1d25fd9e893ee1f431aae))
|
||||
* **lang:** add support for Portuguese (Portugal) language ([e044146](https://github.com/sct/overseerr/commit/e044146aa55109a1eccfde9650b26beb0d5ec9a6))
|
||||
* **lang:** translated using Weblate (Dutch) ([6d0f7d4](https://github.com/sct/overseerr/commit/6d0f7d4b50370c420c1017f32d48313074543743))
|
||||
* **lang:** translated using Weblate (Italian) ([9aa5c12](https://github.com/sct/overseerr/commit/9aa5c121644518c1fbb308a487c26d8998bb5a36))
|
||||
* **lang:** translated using Weblate (Portuguese (Portugal)) ([f001fb3](https://github.com/sct/overseerr/commit/f001fb3b33d4fb749acb70c45b8a55a5bbef570c))
|
||||
* **lang:** translated using Weblate (Spanish) ([4f94d22](https://github.com/sct/overseerr/commit/4f94d227fc3096bcb8a1e5cf12fe9222d6c6b711))
|
||||
* **login:** add request ip to the failed request log ([#714](https://github.com/sct/overseerr/issues/714)) ([2d31ea9](https://github.com/sct/overseerr/commit/2d31ea940ac0a1a84d2150743798b41ff6490317))
|
||||
* **users:** add editable usernames ([#715](https://github.com/sct/overseerr/issues/715)) ([20ca3f2](https://github.com/sct/overseerr/commit/20ca3f2f5fcf4a9eb0d6a8be671bb4fb1f5e6178))
|
||||
* pre-populate server info from plex.tv API ([#563](https://github.com/sct/overseerr/issues/563)) ([82ac76b](https://github.com/sct/overseerr/commit/82ac76b0540ba1133cb5384744d2499c2488a4e8))
|
||||
* **auth:** Add optional CSRF protection ([#697](https://github.com/sct/overseerr/issues/697)) ([6e25891](https://github.com/sct/overseerr/commit/6e2589178b99f8f32f0ded9a7cfd9921c33e9b60))
|
||||
* ability to edit user settings in bulk ([#597](https://github.com/sct/overseerr/issues/597)) ([4b0241c](https://github.com/sct/overseerr/commit/4b0241c3b34d4229f928c21defb10a1c051264d1))
|
||||
* **lang:** translated using Weblate (English) ([9bb11af](https://github.com/sct/overseerr/commit/9bb11afc6b4a109ae1e14d41c9fe2b71f19c470a))
|
||||
* **lang:** translated using Weblate (German) ([c2a3e8e](https://github.com/sct/overseerr/commit/c2a3e8ed5243925dce991ec7995ae831702dbc7b))
|
||||
* **lang:** translated using Weblate (Portuguese (Brazil)) ([32f4916](https://github.com/sct/overseerr/commit/32f4916c4a926097f31ed472aee031536b847bb7))
|
||||
* **lang:** translated using Weblate (Portuguese (Brazil)) ([98570c9](https://github.com/sct/overseerr/commit/98570c920e4904a594bb7464161b985094958f84))
|
||||
* **notifications:** add option to send notifications for auto-approved requests ([21db367](https://github.com/sct/overseerr/commit/21db3676d1464b63384b04c0c2926cb2a6252e9b)), closes [#267](https://github.com/sct/overseerr/issues/267)
|
||||
|
||||
## [1.17.2](https://github.com/sct/overseerr/compare/v1.17.1...v1.17.2) (2021-01-20)
|
||||
|
||||
|
||||
|
||||
101
CONTRIBUTING.md
@@ -1,78 +1,105 @@
|
||||
# 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:
|
||||
All help is welcome and greatly appreciated! If you would like to contribute to the project, the following instructions should 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)
|
||||
- HTML/Typescript/Javascript editor
|
||||
- [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 14.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.
|
||||
```
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) the repository to your own GitHub account and [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/overseerr.git
|
||||
cd overseerr/
|
||||
```
|
||||
2. Add the remote upstream.
|
||||
|
||||
```
|
||||
2. Add the remote `upstream`:
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/sct/overseerr.git
|
||||
```
|
||||
|
||||
3. Create a new branch
|
||||
3. Create a new branch:
|
||||
|
||||
```
|
||||
```bash
|
||||
git checkout -b BRANCH_NAME develop
|
||||
```
|
||||
|
||||
- Its recommended to name the branch something relevant to the feature or fix you are working on.
|
||||
- An example of this would be `fix-title-cards` or `feature-new-system`.
|
||||
- Bad examples would be `patch` or `bug`.
|
||||
- It is recommended to give your branch a meaningful name, relevant to the feature or fix you are working on.
|
||||
- Good examples:
|
||||
- `docs-docker`
|
||||
- `feature-new-system`
|
||||
- `fix-title-cards`
|
||||
- Bad examples:
|
||||
- `bug`
|
||||
- `docs`
|
||||
- `feature`
|
||||
- `fix`
|
||||
- `patch`
|
||||
|
||||
4. Run development environment
|
||||
4. Run the development environment:
|
||||
|
||||
```
|
||||
```bash
|
||||
yarn
|
||||
yarn dev
|
||||
```
|
||||
|
||||
- Alternatively you can run using [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||
|
||||
5. Create your patch and run appropriate tests.
|
||||
5. Create your patch and test your changes.
|
||||
|
||||
6. Follow the [guidelines](#contributing-code).
|
||||
|
||||
7. Should you need to update your fork you can do so by rebasing from upstream.
|
||||
|
||||
```
|
||||
git fetch upstream
|
||||
git rebase upstream/develop
|
||||
git push origin BRANCH_NAME -f
|
||||
```
|
||||
- Be sure to follow both the [code](#contributing-code) and [UI text](#ui-text-style) guidelines.
|
||||
- Should you need to update your fork, you can do so by rebasing from `upstream`:
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/develop
|
||||
git push origin BRANCH_NAME -f
|
||||
```
|
||||
|
||||
### 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.
|
||||
- If you are taking on an existing bug or feature ticket, please comment on the [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.
|
||||
- It is okay to squash your pull request down into a single commit that fits this standard.
|
||||
- Pull requests 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. Your work will **not** be merged unless it is 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 code base. It should automatically run with a Git hook, but it is recommended to have the Prettier extension installed in your editor and format on save.
|
||||
- If you have questions or need help, you can reach out via [Discussions](https://github.com/sct/overseerr/discussions) or our [Discord server](https://discord.gg/PkCWJSeCk7).
|
||||
- Only open pull requests to `develop`, never `master`! Any pull requests opened to `master` will be closed.
|
||||
|
||||
### UI Text Style
|
||||
|
||||
When adding new UI text, please try to adhere to the following guidelines:
|
||||
|
||||
1. Be concise and clear, and use as few words as possible to make your point.
|
||||
2. Use the Oxford comma where appropriate.
|
||||
3. Use the appropriate Unicode characters for ellipses, arrows, and other special characters/symbols.
|
||||
4. Capitalize proper nouns, such as Plex, Radarr, Sonarr, Telegram, Slack, Pushover, etc. Be sure to also use the official capitalization for any abbreviations; e.g., TMDb and IMDb have a lowercase 'b', whereas TheTVDB has a capital 'B'.
|
||||
5. Title case headings, button text, and form labels. Note that verbs such as "is" should be capitalized, whereas prepositions like "from" should be lowercase (unless as the first or last word of the string, in which case they are also capitalized).
|
||||
6. Capitalize the first word in validation error messages, dropdowns, and form "tips." These strings should not end in punctuation.
|
||||
7. Ensure that toast notification strings are complete sentences ending in punctuation.
|
||||
8. If an additional description or "tip" is required for a form field, it should be styled using the global CSS class `label-tip`.
|
||||
9. In full sentences, abbreviations like "info" or "auto" should not be used in place of full words, unless referencing the name/label of a specific setting or option which has an abbreviation in its name.
|
||||
10. Do your best to check for spelling errors and grammatical mistakes.
|
||||
11. Do not misspell "Overseerr."
|
||||
|
||||
## Translation
|
||||
|
||||
We use [Weblate](https://hosted.weblate.org/engage/overseerr/) for our translations so please feel free to contribute to localizing Overseerr!
|
||||
We use [Weblate](https://hosted.weblate.org/engage/overseerr/) for our translations, and your help with localizing Overseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/sct/overseerr/issues/new/choose).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/overseerr/"><img src="https://hosted.weblate.org/widgets/overseerr/-/overseerr-frontend/multi-auto.svg" alt="Translation status" /></a>
|
||||
|
||||
## Attribution
|
||||
|
||||
|
||||
42
Dockerfile
@@ -1,4 +1,7 @@
|
||||
FROM node:12.18-alpine AS BUILD_IMAGE
|
||||
FROM node:14.16-alpine AS BUILD_IMAGE
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
||||
|
||||
ARG COMMIT_TAG
|
||||
ENV COMMIT_TAG=${COMMIT_TAG}
|
||||
@@ -6,30 +9,35 @@ ENV COMMIT_TAG=${COMMIT_TAG}
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn --frozen-lockfile && \
|
||||
RUN \
|
||||
case "${TARGETPLATFORM}" in \
|
||||
'linux/arm64') apk add --no-cache python make g++ ;; \
|
||||
'linux/arm/v7') apk add --no-cache python make g++ ;; \
|
||||
esac
|
||||
|
||||
RUN yarn --frozen-lockfile --network-timeout 1000000 && \
|
||||
yarn build
|
||||
|
||||
# remove development dependencies
|
||||
RUN yarn install --production --ignore-scripts --prefer-offline
|
||||
RUN yarn cache clean
|
||||
|
||||
FROM node:12.18-alpine
|
||||
RUN rm -rf src && \
|
||||
rm -rf server
|
||||
|
||||
ARG COMMIT_TAG
|
||||
ENV COMMIT_TAG=${COMMIT_TAG}
|
||||
|
||||
RUN apk add tzdata
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
# copy from build image
|
||||
COPY --from=BUILD_IMAGE /app/dist ./dist
|
||||
COPY --from=BUILD_IMAGE /app/.next ./.next
|
||||
COPY --from=BUILD_IMAGE /app/node_modules ./node_modules
|
||||
RUN touch config/DOCKER
|
||||
|
||||
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
||||
|
||||
CMD yarn start
|
||||
|
||||
FROM node:14.16-alpine
|
||||
|
||||
RUN apk add --no-cache tzdata tini
|
||||
|
||||
# copy from build image
|
||||
COPY --from=BUILD_IMAGE /app /app
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||
CMD [ "yarn", "start" ]
|
||||
|
||||
EXPOSE 5055
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:12.18-alpine
|
||||
FROM node:14.16-alpine
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
90
README.md
@@ -6,56 +6,48 @@
|
||||
<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://discord.gg/PkCWJSeCk7"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
||||
<a href="https://hub.docker.com/r/sctx/overseerr"><img src="https://img.shields.io/docker/pulls/sctx/overseerr" alt="Docker pulls"></a>
|
||||
<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">
|
||||
<a href="https://github.com/sct/overseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/sct/overseerr"></a>
|
||||
<!-- 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-28-orange.svg"/></a>
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-46-orange.svg"/></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
</p>
|
||||
|
||||
**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**!
|
||||
**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](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**, and **[Plex](https://www.plex.tv/)**!
|
||||
|
||||
## 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 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
|
||||
|
||||
- User profiles.
|
||||
- User settings page (to give users the ability to modify their Overseerr experience to their liking).
|
||||
- Local user system (for those who don't use Plex).
|
||||
- Full Plex integration. Authenticate and manage user access with Plex!
|
||||
- Easy integration with your existing services. Currently, Overseerr supports Sonarr and Radarr. More to come!
|
||||
- Plex library sync, to keep track of the titles which are already available.
|
||||
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
|
||||
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
|
||||
- Granular permission system.
|
||||
- Support for various notification agents.
|
||||
- Mobile-friendly design, for when you need to approve requests on the go!
|
||||
|
||||
## Planned Features
|
||||
|
||||
- More notification types.
|
||||
- Additional notification types.
|
||||
- Issues system. This will allow users to report issues with content on your media server.
|
||||
- And a ton more! Check out our [issue tracker](https://github.com/sct/overseerr/issues) to see what features people have already requested.
|
||||
- And a ton more! Check out our [issue tracker](https://github.com/sct/overseerr/issues) to see the features which have already been requested.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Check out our documenation for steps on how to install and run Overseerr:
|
||||
Check out our documentation for instructions on how to install and run Overseerr:
|
||||
|
||||
https://docs.overseerr.dev/getting-started/installation
|
||||
|
||||
## Running Overseerr
|
||||
|
||||
Currently, Overseerr is only distributed through Docker images. If you have Docker, you can run Overseerr as per:
|
||||
Currently, Overseerr is primarily distributed as Docker images. If you have Docker installed, you can simply run Overseerr with:
|
||||
|
||||
```
|
||||
docker run -d \
|
||||
--name overseerr \
|
||||
-e LOG_LEVEL=info \
|
||||
-e TZ=Asia/Tokyo \
|
||||
-p 5055:5055 \
|
||||
@@ -64,9 +56,11 @@ docker run -d \
|
||||
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.
|
||||
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! ⚠️
|
||||
For more information and alternative installation methods, please see the [Overseerr documentation](https://docs.overseerr.dev/getting-started/installation).
|
||||
|
||||
⚠️ 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 `sctx/overseerr:develop` image instead! ⚠️
|
||||
|
||||
## Preview
|
||||
|
||||
@@ -77,23 +71,25 @@ After running Overseerr for the first time, configure it by visiting the web UI
|
||||
- Check out the [Overseerr Documentation](https://docs.overseerr.dev/) before asking for help. Your question might already be in the [FAQ](https://docs.overseerr.dev/support/faq).
|
||||
- You can get support on [Discord](https://discord.gg/PkCWJSeCk7).
|
||||
- You can ask questions in the Help category of our [GitHub Discussions](https://github.com/sct/overseerr/discussions).
|
||||
- Bugs/Feature Requests can be opened via a [GitHub issue](https://github.com/sct/overseerr/issues).
|
||||
- Bug reports and feature requests can be submitted via [GitHub Issues](https://github.com/sct/overseerr/issues).
|
||||
|
||||
## API Documentation
|
||||
|
||||
Full API documentation will soon be published automatically and available outside of running the app. Currently, you can access the API docs by running Overseerr locally and visiting http://localhost:5055/api-docs
|
||||
Our documentation is built on every commit and hosted at https://api-docs.overseerr.dev
|
||||
|
||||
You can also access the API documentation from your local Overseerr install at http://localhost:5055/api-docs
|
||||
|
||||
## Community
|
||||
|
||||
You can ask questions, share ideas, and more in [GitHub Discussions](https://github.com/sct/overseerr/discussions).
|
||||
|
||||
If you would like to chat with community members you can join the [Overseerr Discord](https://discord.gg/PkCWJSeCk7).
|
||||
If you would like to chat with other members of our growing community, [join the Overseerr Discord server](https://discord.gg/PkCWJSeCk7)!
|
||||
|
||||
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.
|
||||
You can help improve Overseerr too! Check out our [Contribution Guide](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md) to get started.
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
@@ -133,11 +129,35 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/doob187"><img src="https://avatars1.githubusercontent.com/u/60312740?v=4?s=100" width="100px;" alt=""/><br /><sub><b>doob187</b></sub></a><br /><a href="#infra-doob187" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/johnpyp"><img src="https://avatars2.githubusercontent.com/u/20625636?v=4?s=100" width="100px;" alt=""/><br /><sub><b>johnpyp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=johnpyp" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ankarhem"><img src="https://avatars1.githubusercontent.com/u/14110063?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jakob Ankarhem</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Documentation">📖</a> <a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ankarhem"><img src="https://avatars1.githubusercontent.com/u/14110063?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jakob Ankarhem</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Documentation">📖</a> <a href="https://github.com/sct/overseerr/commits?author=ankarhem" title="Code">💻</a> <a href="#translation-ankarhem" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/jayesh100"><img src="https://avatars1.githubusercontent.com/u/8022175?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jayesh</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jayesh100" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/flying-sausages"><img src="https://avatars1.githubusercontent.com/u/23618693?v=4?s=100" width="100px;" alt=""/><br /><sub><b>flying-sausages</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=flying-sausages" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/hirenshah"><img src="https://avatars2.githubusercontent.com/u/418112?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hirenshah</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=hirenshah" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/TheCatLady"><img src="https://avatars0.githubusercontent.com/u/52870424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TheCatLady</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=TheCatLady" title="Code">💻</a> <a href="#translation-TheCatLady" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/TheCatLady"><img src="https://avatars0.githubusercontent.com/u/52870424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TheCatLady</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=TheCatLady" title="Code">💻</a> <a href="#translation-TheCatLady" title="Translation">🌍</a> <a href="https://github.com/sct/overseerr/commits?author=TheCatLady" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/chriscpritchard"><img src="https://avatars1.githubusercontent.com/u/1839074?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chris Pritchard</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=chriscpritchard" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=chriscpritchard" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Tamberlox"><img src="https://avatars3.githubusercontent.com/u/56069014?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tamberlox</b></sub></a><br /><a href="#translation-Tamberlox" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://hmnd.io"><img src="https://avatars.githubusercontent.com/u/12853597?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=hmnd" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.douglas-parker.com"><img src="https://avatars.githubusercontent.com/u/18235822?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Douglas Parker</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=douglasparker" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/dancarter"><img src="https://avatars.githubusercontent.com/u/4387516?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Carter</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=dancarter" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://nuro.dev"><img src="https://avatars.githubusercontent.com/u/4991309?v=4?s=100" width="100px;" alt=""/><br /><sub><b>nuro</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=NuroDev" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/onedr0p"><img src="https://avatars.githubusercontent.com/u/213795?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ᗪєνιη ᗷυнʟ</b></sub></a><br /><a href="#infra-onedr0p" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/JonnyWong16"><img src="https://avatars.githubusercontent.com/u/9099342?v=4?s=100" width="100px;" alt=""/><br /><sub><b>JonnyWong16</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JonnyWong16" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Roxedus"><img src="https://avatars.githubusercontent.com/u/7110194?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Roxedus</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Roxedus" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/WoisWoi"><img src="https://avatars.githubusercontent.com/u/75491231?v=4?s=100" width="100px;" alt=""/><br /><sub><b>WoisWoi</b></sub></a><br /><a href="#translation-WoisWoi" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/HubDuck"><img src="https://avatars.githubusercontent.com/u/77843475?v=4?s=100" width="100px;" alt=""/><br /><sub><b>HubDuck</b></sub></a><br /><a href="#translation-HubDuck" title="Translation">🌍</a> <a href="https://github.com/sct/overseerr/commits?author=HubDuck" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/costaht"><img src="https://avatars.githubusercontent.com/u/50637431?v=4?s=100" width="100px;" alt=""/><br /><sub><b>costaht</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=costaht" title="Documentation">📖</a> <a href="#translation-costaht" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/Shjosan"><img src="https://avatars.githubusercontent.com/u/20847626?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Shjosan</b></sub></a><br /><a href="#translation-Shjosan" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://github.com/kobaubarr"><img src="https://avatars.githubusercontent.com/u/28481522?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kobaubarr</b></sub></a><br /><a href="#translation-kobaubarr" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/notorius28"><img src="https://avatars.githubusercontent.com/u/1621513?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ricardo González</b></sub></a><br /><a href="#translation-notorius28" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="http://torkili.uz"><img src="https://avatars.githubusercontent.com/u/460764?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Torkil</b></sub></a><br /><a href="#translation-Torkiliuz" title="Translation">🌍</a></td>
|
||||
<td align="center"><a href="https://www.jagandeepbrar.io"><img src="https://avatars.githubusercontent.com/u/3048295?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jagandeep Brar</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JagandeepBrar" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://dtalens.com"><img src="https://avatars.githubusercontent.com/u/6631832?v=4?s=100" width="100px;" alt=""/><br /><sub><b>dtalens</b></sub></a><br /><a href="#translation-dtalens" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -19,4 +19,6 @@ The primary motivation for starting this project was to have an incredibly perfo
|
||||
|
||||
## We need your help!
|
||||
|
||||
Overseerr is an ambitious project. We have already poured a lot of work into this, with more coming. We need your valuable feedback and help with finding bugs. Also, being that this is an open-source project, anyone is welcome to contribute. Contribution includes building features, patching bugs, or even translating the application. You can find the contribution guide on our GitHub.
|
||||
Overseerr is an ambitious project. We have already poured a lot of work into this, and have a lot more to do. We need your valuable feedback and help to find and fix bugs. Also, with Overseerr being an open-source project, anyone is welcome to contribute. Contribution includes building new features, patching bugs, translating the application, or even just writing documentation.
|
||||
|
||||
If you would like to contribute, please be sure to review our [contribution guidelines](https://github.com/sct/overseerr/blob/develop/CONTRIBUTING.md).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Table of contents
|
||||
# Table of Contents
|
||||
|
||||
- [Introduction](README.md)
|
||||
|
||||
@@ -8,14 +8,18 @@
|
||||
|
||||
## Using Overseerr
|
||||
|
||||
- [Settings](using-overseerr/settings/README.md)
|
||||
- [Notifications](using-overseerr/notifications/README.md)
|
||||
- [Custom Webhooks](using-overseerr/notifications/webhooks.md)
|
||||
- [Email](using-overseerr/notifications/email.md)
|
||||
- [Discord](using-overseerr/notifications/discord.md)
|
||||
- [Webhooks](using-overseerr/notifications/webhooks.md)
|
||||
|
||||
## Support
|
||||
|
||||
- [Frequently Asked Questions](support/faq.md)
|
||||
- [Frequently Asked Questions (FAQ)](support/faq.md)
|
||||
- [Asking for Support](support/asking-for-support.md)
|
||||
|
||||
## Extending Overseerr
|
||||
|
||||
- [Reverse Proxy Examples](extending-overseerr/reverse-proxy-examples.md)
|
||||
- [Fail2ban Filter](extending-overseerr/fail2ban.md)
|
||||
|
||||
14
docs/extending-overseerr/fail2ban.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Fail2ban Filter
|
||||
|
||||
{% hint style="warning" %}
|
||||
If you are running Overseerr behind a reverse proxy, make sure that the **Enable Proxy Support** setting is **enabled**.
|
||||
{% endhint %}
|
||||
|
||||
To use Fail2ban with Overseerr, create a new file named `overseerr.local` in your Fail2ban `filter.d` directory with the following filter definition:
|
||||
|
||||
```
|
||||
[Definition]
|
||||
failregex = .*\[info\]\[Auth\]\: Failed sign-in attempt.*"ip":"<HOST>"
|
||||
```
|
||||
|
||||
You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documetation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.
|
||||
@@ -1,18 +1,14 @@
|
||||
# Reverse Proxy Examples
|
||||
|
||||
{% hint style="warning" %}
|
||||
Base URLs cannot be configured in Overseerr. With this limitation, only subdomain configurations are supported.
|
||||
Base URLs cannot be configured in Overseerr. With this limitation, only subdomain configurations are supported. However, a Nginx subfolder workaround configuration is provided below to use at your own risk.
|
||||
{% endhint %}
|
||||
|
||||
## LE/SWAG
|
||||
## SWAG
|
||||
|
||||
### Subdomain
|
||||
A sample proxy configuration is included in [SWAG (Secure Web Application Gateway)](https://github.com/linuxserver/docker-swag). However, this page is still the only source of truth, so the SWAG sample configuration is not guaranteed to be up-to-date. If you find an inconsistency, please [report it to the LinuxServer team](https://github.com/linuxserver/reverse-proxy-confs/issues/new) or [submit a pull request to update it](https://github.com/linuxserver/reverse-proxy-confs/pulls).
|
||||
|
||||
A sample is bundled in SWAG. This page is still the only source of truth, so the sample is not guaranteed to be up to date. If you catch an inconsistency, report it to the linuxserver team, or do a pull-request against the proxy-confs repository to update the sample.
|
||||
|
||||
Rename the sample file `overseerr.subdomain.conf.sample` to `overseerr.subdomain.conf` in the `proxy-confs`folder, or create `overseerr.subdomain.conf` in the same folder with the example below.
|
||||
|
||||
Example Configuration:
|
||||
To use the bundled configuration file, simply rename `overseerr.subdomain.conf.sample` in the `proxy-confs` folder to `overseerr.subdomain.conf`. Alternatively, create a new file `overseerr.subdomain.conf` in `proxy-confs` with the following configuration:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -41,11 +37,7 @@ server {
|
||||
|
||||
## Traefik \(v2\)
|
||||
|
||||
Add the labels to the Overseerr service in your `docker-compose` file. A basic example for a `docker-compose` file using Traefik can be found [here](https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/).
|
||||
|
||||
### Subdomain
|
||||
|
||||
Example Configuration:
|
||||
Add the following labels to the Overseerr service in your `docker-compose.yml` file:
|
||||
|
||||
```text
|
||||
labels:
|
||||
@@ -59,33 +51,16 @@ labels:
|
||||
- "traefik.http.services.overseerr-svc.loadbalancer.server.port=5055"
|
||||
```
|
||||
|
||||
## LE/NGINX
|
||||
For more information, see the Traefik documentation for a [basic example](https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/).
|
||||
|
||||
### Subdomain
|
||||
## Nginx
|
||||
|
||||
Take the configuration below and place it in `/etc/nginx/sites-available/overseerr.example.com.conf`.
|
||||
{% tabs %}
|
||||
{% tab title="Subdomain" %}
|
||||
|
||||
Create a symlink to `/etc/nginx/sites-enabled`:
|
||||
Add the following configuration to a new file `/etc/nginx/sites-available/overseerr.example.com.conf`:
|
||||
|
||||
```text
|
||||
sudo ln -s /etc/nginx/sites-available/overseerr.example.com.conf /etc/nginx/sites-enabled/overseerr.example.com.conf
|
||||
```
|
||||
|
||||
Test the configuration:
|
||||
|
||||
```text
|
||||
sudo nginx -t
|
||||
```
|
||||
|
||||
Reload your configuration for NGINX:
|
||||
|
||||
```text
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Example Configuration:
|
||||
|
||||
```text
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name overseerr.example.com;
|
||||
@@ -115,13 +90,15 @@ server {
|
||||
# HTTP Strict Transport Security
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||
# Reduce XSS risks (Content-Security-Policy) - uncomment to use and add URLs whenever necessary
|
||||
# add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://plex.tv; style-src 'self' 'unsafe-inline' https://rsms.me/inter/inter.css; script-src 'self'; img-src 'self' data: https://plex.tv https://assets.plex.tv https://gravatar.com https://i2.wp.com https://image.tmdb.org; font-src 'self' https://rsms.me/inter/font-files/" always;
|
||||
# add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://plex.tv; style-src 'self' 'unsafe-inline' https://rsms.me/inter/inter.css; script-src 'self' 'unsafe-inline'; img-src 'self' data: https://plex.tv https://assets.plex.tv https://gravatar.com https://secure.gravatar.com https://i2.wp.com https://image.tmdb.org; font-src 'self' https://rsms.me/inter/font-files/" always;
|
||||
# Prevent some categories of XSS attacks (X-XSS-Protection)
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
# Provide clickjacking protection (X-Frame-Options)
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
# Prevent Sniff Mimetype (X-Content-Type-Options)
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
# Tell crawling bots to not index the site
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
|
||||
access_log /var/log/nginx/overseerr.example.com-access.log;
|
||||
error_log /var/log/nginx/overseerr.example.com-error.log;
|
||||
@@ -132,3 +109,60 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
Then, create a symlink to `/etc/nginx/sites-enabled`:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/overseerr.example.com.conf /etc/nginx/sites-enabled/overseerr.example.com.conf
|
||||
```
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="Subfolder" %}
|
||||
|
||||
{% hint style="warning" %}
|
||||
Nginx subfolder reverse proxy is unsupported. The sub filters may stop working when Overseerr is updated. Use at your own risk!
|
||||
{% endhint %}
|
||||
|
||||
Add the following location block to your existing `nginx.conf` file.
|
||||
|
||||
```nginx
|
||||
location ^~ /overseerr {
|
||||
set $app 'overseerr';
|
||||
# Remove /overseerr path to pass to the app
|
||||
rewrite ^/overseerr/?(.*)$ /$1 break;
|
||||
proxy_pass http://127.0.0.1:5055; # NO TRAILING SLASH
|
||||
# Redirect location headers
|
||||
proxy_redirect ^ /$app;
|
||||
proxy_redirect /setup /$app/setup;
|
||||
proxy_redirect /login /$app/login;
|
||||
# Sub filters to replace hardcoded paths
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once off;
|
||||
sub_filter_types *;
|
||||
sub_filter 'href="/"' 'href="/$app"';
|
||||
sub_filter 'href="/login"' 'href="/$app/login"';
|
||||
sub_filter 'href:"/"' 'href:"/$app"';
|
||||
sub_filter '/_next' '/$app/_next';
|
||||
sub_filter '/api/v1' '/$app/api/v1';
|
||||
sub_filter '/login/plex/loading' '/$app/login/plex/loading';
|
||||
sub_filter '/images/' '/$app/images/';
|
||||
sub_filter '/android-' '/$app/android-';
|
||||
sub_filter '/apple-' '/$app/apple-';
|
||||
sub_filter '/favicon' '/$app/favicon';
|
||||
sub_filter '/logo.png' '/$app/logo.png';
|
||||
sub_filter '/site.webmanifest' '/$app/site.webmanifest';
|
||||
}
|
||||
```
|
||||
{% endtab %}
|
||||
{% endtabs %}
|
||||
|
||||
Next, test the configuration:
|
||||
|
||||
```bash
|
||||
sudo nginx -t
|
||||
```
|
||||
|
||||
Finally, reload `nginx` for the new configuration to take effect:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
@@ -15,6 +15,7 @@ After running Overseerr for the first time, configure it by visiting the web UI
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name overseerr \
|
||||
-e LOG_LEVEL=info \
|
||||
-e TZ=Asia/Tokyo \
|
||||
-p 5055:5055 \
|
||||
@@ -25,10 +26,35 @@ docker run -d \
|
||||
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="Compose" %}
|
||||
|
||||
**docker-compose.yml:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
overseerr:
|
||||
image: sctx/overseerr:latest
|
||||
container_name: overseerr
|
||||
environment:
|
||||
- LOG_LEVEL=info
|
||||
- TZ=Asia/Tokyo
|
||||
ports:
|
||||
- 5055:5055
|
||||
volumes:
|
||||
- /path/to/appdata/config:/app/config
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="UID/GID" %}
|
||||
|
||||
```text
|
||||
docker run -d \
|
||||
--name overseerr \
|
||||
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ] \
|
||||
-e LOG_LEVEL=info \
|
||||
-e TZ=Asia/Tokyo \
|
||||
@@ -121,26 +147,54 @@ This version can break any moment. Be prepared to troubleshoot any issues that a
|
||||
{% tabs %}
|
||||
|
||||
{% tab title="Gentoo" %}
|
||||
Portage overlay [GitHub Repository](https://github.com/chriscpritchard/overseerr-overlay)
|
||||
Portage overlay [GitHub Repository](https://github.com/chriscpritchard/overseerr-overlay).
|
||||
|
||||
This is now included in the list of [Gentoo repositories](https://overlays.gentoo.org/), so can be easily enabled with `eselect repository`
|
||||
|
||||
Efforts will be made to keep up to date with the latest releases, however, this cannot be guaranteed.
|
||||
|
||||
To enable using eselect repository, run:
|
||||
**To enable:**
|
||||
To enable using `eselect repository`, run:
|
||||
|
||||
```bash
|
||||
eselect repository add overseerr-overlay git https://github.com/chriscpritchard/overseerr-overlay.git
|
||||
eselect repository enable overseerr-overlay
|
||||
```
|
||||
|
||||
**To install:**
|
||||
Once complete, you can just run:
|
||||
|
||||
```bash
|
||||
emerge www-apps/overseerr
|
||||
```
|
||||
|
||||
**To install the development build:**
|
||||
A live ebuild (`=www-apps/overseerr-9999`) is also available. To use this, you will need to modify accept_keywords for this package:
|
||||
|
||||
```bash
|
||||
emerge --autounmask --autounmask-write "=www-apps/overseerr-9999"
|
||||
```
|
||||
|
||||
Once installed, you will not be notified of updates, so you can update with:
|
||||
|
||||
```bash
|
||||
emerge @live-rebuild
|
||||
```
|
||||
|
||||
or use `app-portage/smart-live-rebuild`
|
||||
|
||||
{% hint style="danger" %}
|
||||
This version can break any moment. Be prepared to troubleshoot any issues that arise!
|
||||
{% endhint %}
|
||||
|
||||
{% endtab %}
|
||||
|
||||
{% tab title="Swizzin" %}
|
||||
The installation is not implemented via docker, but barebones. The latest released version of overseerr will be used.
|
||||
|
||||
{% hint style="danger" %}
|
||||
This implementation is not yet merged to master due to missing functionality. You can beta test the limited implementation or follow the status on [the pull request](https://github.com/swizzin/swizzin/pull/567).
|
||||
{% endhint %}
|
||||
|
||||
The installation is not implemented via Docker, but barebones. The latest release version of Overseerr will be used.
|
||||
Please see the [swizzin documentation](https://swizzin.ltd/applications/overseerr) for more information.
|
||||
|
||||
To install, run the following:
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
# Asking for Support
|
||||
|
||||
## Before asking for support, make sure you try these things first
|
||||
## Before Asking for Support
|
||||
|
||||
* Make sure you have **updated** to the latest version.
|
||||
* ["Have you tried turning it off and on again?"](https://www.youtube.com/watch?v=nn2FB1P_Mn8)
|
||||
* **Analyzing** your logs, you just might find the solution yourself!
|
||||
* **Search** the [Wiki](../), [Installation Guides](../getting-started/installation.md), and [FAQs](faq.md).
|
||||
* If you have questions, feel free to ask them on [Discord](https://discord.gg/PkCWJSeCk7) \(Please review our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md)\). Please include a link to your logs. See [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) for more details.
|
||||
Before seeking help, please make sure you have tried these following first:
|
||||
|
||||
- **Update** to the latest version.
|
||||
- ["Have you tried turning it off and on again?"](https://www.youtube.com/watch?v=nn2FB1P_Mn8)
|
||||
- **Analyze** your logs, you just might find the solution yourself!
|
||||
- **Search** the [Wiki](../), [Installation Guides](../getting-started/installation.md), and [FAQs](faq.md).
|
||||
- If you have questions, feel free to ask on [Discord](https://discord.gg/PkCWJSeCk7) \(Please review our [Code of Conduct](https://github.com/sct/overseerr/blob/develop/CODE_OF_CONDUCT.md).\) Be sure to include a link to your logs. See [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) below.
|
||||
|
||||
## What should I include when asking for support?
|
||||
|
||||
When you contact support saying something like "it doesn't work" leaves little to go on to figure out what is wrong for you. When contacting support try to include information such as the following:
|
||||
When you contact support, a vague statement like "it doesn't work" leaves little to go on to figure out what is wrong for you. When contacting support, try to include as much information as possible. Try to answer the following questions:
|
||||
|
||||
* What did you try to do? When you describe what you did to reach the state you are in we may notice something you did different from the instructions, or something that your unique setup requires in addition. Some examples of what to provide here:
|
||||
* What command did you enter?
|
||||
* What did you click on?
|
||||
* What settings did you change?
|
||||
* Provide a step-by-step list of what you tried.
|
||||
* What do you see? We cannot see your screen so some of the following is necessary for us to know what is going on:
|
||||
* Did something happen?
|
||||
* Did something not happen?
|
||||
* Are there any error messages showing?
|
||||
* Screenshots can help us see what you are seeing
|
||||
* The Overseerr logs show exactly what happened and are often critical for identifying issues \(see [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) below\).
|
||||
- What did you try to do? When you describe what you did to reach the state you are in, we may notice something you did differently from the official instructions, or something required by your unique setup. The following are questions that should be answered in your request:
|
||||
- What command did you enter?
|
||||
- What did you click on?
|
||||
- What settings did you change?
|
||||
- Provide a step-by-step list of what you tried.
|
||||
- What do you see? We cannot see your screen so some of the following is necessary for us to know what is going on:
|
||||
- Did something happen?
|
||||
- Did something not happen?
|
||||
- Are there any error messages showing?
|
||||
- Provide screenshots to help us see what you are seeing.
|
||||
- Share your Overseerr logs, which show exactly what happened and are often critical for identifying issues \(see [How can I share my logs?](asking-for-support.md#how-can-i-share-my-logs) below\).
|
||||
|
||||
## How can I share my logs?
|
||||
|
||||
First you will need to gather your logs from the install directory.
|
||||
|
||||
1. Collect the log file from `<Overseeerr-install-directory>/logs/overseerr.log`
|
||||
2. Open the log file and **upload the text** by going to [gist.github.com](https://gist.github.com/) and creating a new secret Gist of the contents.
|
||||
3. **Share the link** with support in [Discord](https://discord.gg/PkCWJSeCk7) by copying the URL of the page.
|
||||
|
||||
1. Locate the log file at `<Overseeerr-install-directory>/logs/overseerr.log`
|
||||
2. Open the log file and **copy its contents** into a [**secret gist** on GitHub](https://gist.github.com/). If you upload your logs elsewhere, we may ask you to share them again via GitHub Gist.
|
||||
3. **Share the link/URL to your secret gist** in the [`#support` channel in our Discord server](https://discord.gg/PkCWJSeCk7).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Frequently Asked Questions
|
||||
# Frequently Asked Questions (FAQ)
|
||||
|
||||
{% hint style="info" %}
|
||||
If you can't find a solution here, please ask on [Discord](https://discord.gg/PkCWJSeCk7). Please do not post questions on the GitHub issues tracker.
|
||||
@@ -6,9 +6,9 @@ If you can't find a solution here, please ask on [Discord](https://discord.gg/Pk
|
||||
|
||||
## General
|
||||
|
||||
### I receive 409 or 400 errors when requesting a movie or tv show!
|
||||
### I receive 409 or 400 errors when requesting a movie or TV series!
|
||||
|
||||
**A:** Verify your are running radarr and sonarr v3. Overseerr was developed for v3 and is not currently backward compatible.
|
||||
**A:** Verify you are running Radarr and Sonarr v3. Overseerr was developed for v3 and is not currently backwards-compatible with previous versions.
|
||||
|
||||
### How do I keep Overseerr up-to-date?
|
||||
|
||||
@@ -24,23 +24,23 @@ The most secure method, but also the most inconvenient, is to set up a VPN tunne
|
||||
|
||||
### Overseerr is amazing! But it is not translated in my language yet! Can I help with translations?
|
||||
|
||||
**A:** You sure can! We are using Weblate for translations! Check it out [here](https://hosted.weblate.org/engage/overseerr/). If your language is not listed please open an [enhancement request in issues](https://github.com/sct/overseerr/issues/new/choose).
|
||||
**A:** You sure can! We are using [Weblate](https://hosted.weblate.org/engage/overseerr/) for translations. If your language is not listed, please [open a feature request on GitHub](https://github.com/sct/overseerr/issues/new/choose).
|
||||
|
||||
### Where can I find the changelog?
|
||||
|
||||
**A:** You can find the changelog in the **Settings -> About** page in your instance. You can also find it on github [here](https://github.com/sct/overseerr/releases).
|
||||
**A:** You can find the changelog in the **Settings → About** page in your Overseerr instance. You can also find it on [GitHub](https://github.com/sct/overseerr/releases).
|
||||
|
||||
### Can I make 4K requests?
|
||||
|
||||
**A:** 4K requests are not supported just yet but they will be supported in the future!
|
||||
**A:** Yes! When adding your 4K Sonarr/Radarr server in **Settings → Services**, tick the `4K Server` checkbox. You also need to tick the `Default Server` checkbox if it is the default server you would like to use for 4K content requests. (To enable 4K requests, there need to be default Sonarr/Radarr servers for both 4K content **and** non-4K content.)
|
||||
|
||||
### Some media is missing from Overseerr that I know is in Plex!
|
||||
|
||||
**A:** Overseerr supports the new Plex Movie, Legacy Plex Movie, TheTVDB agent, and the TMDb agent. Please verify that your library is using one of the agents previously listed. If you are changing agents, a full metadata refresh will need to be performed. Caution, this can take a long time depending on how many items you have in your movie library.
|
||||
**A:** Overseerr supports the new Plex Movie, legacy Plex Movie, TheTVDB, and TMDb agents. Please verify that your library is using one of the agents previously listed. If you are changing agents, a full metadata refresh will need to be performed. Caution, this can take a long time depending on how many items you have in your movie library.
|
||||
|
||||
**Troubleshooting Steps:**
|
||||
|
||||
Check the Overseerr logs for media items that are missing. The logs will contain an error as to why that item could not be matched. One example might be `errorMessage":"SQLITE_CONSTRAINT: NOT NULL`. This means that the TMDb ID is missing from the Plex XML for that item.
|
||||
First, check the Overseerr logs for media items that are missing. The logs will contain an error as to why that item could not be matched. One example might be `errorMessage":"SQLITE_CONSTRAINT: NOT NULL`. This means that the TMDb ID is missing from the Plex XML for that item.
|
||||
|
||||
1. Verify that you are using one of the agents mentioned above.
|
||||
2. Refresh the metadata for just that item.
|
||||
@@ -48,48 +48,54 @@ Check the Overseerr logs for media items that are missing. The logs will contain
|
||||
4. If the item is now seen by Overseerr then repeat step 2 for each missing item. If you have a large amount of items missing then a full metadata refresh is recommended for that library.
|
||||
5. Run a full scan on Overseerr after refreshing all unmatched items.
|
||||
|
||||
Perform these steps to verify the media item has a guid Overseerr can match.
|
||||
You can also perform the following to verify the media item has a GUID Overseerr can match:
|
||||
|
||||
1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**.
|
||||
2. Verify that the media item has the same format of one of the examples below.
|
||||
2. Verify that the media item's GUID follows one of the below formats:
|
||||
|
||||
**Examples:**
|
||||
1. TMDb agent `guid="com.plexapp.agents.themoviedb://1705"`
|
||||
2. New Plex Movie agent `<Guid id="tmdb://464052"/>`
|
||||
3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"`
|
||||
4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"`
|
||||
|
||||
1. TMDB agent `guid="com.plexapp.agents.themoviedb://1705"`
|
||||
2. The new Plex Movie agent `<Guid id="tmdb://464052"/>`
|
||||
3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"`
|
||||
4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"`
|
||||
### TV series requests are failing after I updated Overseerr!
|
||||
|
||||
**A:** Language profile support for Sonarr was added in [#860](https://github.com/sct/overseerr/pull/860), along with a new "Language Profile" required setting. If your TV series requests are failing, please make sure that you have a default language profile configured for each of your Sonarr servers in **Settings → Services**.
|
||||
|
||||
### Where can I find the logs?
|
||||
|
||||
**A:** The logs are located at `<Overseeerr-install-directory>/logs/overseerr.log`
|
||||
|
||||
## User Management
|
||||
## User management
|
||||
|
||||
### Why can't I see all my Plex users?
|
||||
|
||||
**A:** Navigate to your **User List** in Overseerr and click **Import Users From Plex** button. Don't forget to check the default user permissions in the **Settings -> General Settings** page beforehand.
|
||||
**A:** Navigate to your **User List** in Overseerr and click **Import Users from Plex** button. Don't forget to check the default user permissions in the **Settings → General Settings** page beforehand.
|
||||
|
||||
### Can I create local users in Overseerr?
|
||||
|
||||
**A:** Not at this time. But it is a planned feature!
|
||||
**A:** Head to the **Users** page and hit **Create Local User**. Keep in mind that local user accounts need a valid email address.
|
||||
|
||||
### Is is possible to set user roles in Overseerr?
|
||||
|
||||
**A:** Unfortunately, this is not possible yet. It is planned!
|
||||
**A:** User roles can be set for each user on the **Users** page. The list of assignable permissions is one that is still growing, so if you have any suggestions, [make a feature request](https://github.com/sct/overseerr/issues/new/choose) on GitHub.
|
||||
|
||||
## Requests
|
||||
|
||||
### I approved a requested movie and radarr didn't search for it!
|
||||
### I approved a requested movie and Radarr didn't search for it!
|
||||
|
||||
**A:** Check your minimum availability in radarr. If an added item does not meet the minimum availability, no search will be performed. Also verify that radarr did not search for it by checking the radarr logs. Lastly, verify the item was not already being monitored by radarr. Currently there is no state sync with radarr.
|
||||
**A:** Check the minimum availability setting in your Radarr server. If a movie does not meet the minimum availability requirement, no search will be performed. Also verify that Radarr did not perform a search, by checking the Radarr logs. Lastly, verify that the item was not already being monitored by Radarr prior to approving the request.
|
||||
|
||||
### Help! My request still shows "requested" even though it's in Plex!?!
|
||||
### Help! My request still shows "requested" even though it is in Plex!
|
||||
|
||||
**A:** See "[Some media is missing from Overseerr that I know is in Plex!](./faq.md#some-media-is-missing-from-overseerr-that-i-know-is-in-plex)" for troubleshooting steps.
|
||||
|
||||
### Approved series requests keep failing!
|
||||
|
||||
**A:** If you configured a base URL in Sonarr, make sure you have set the base URL option appropriately in Overseerr. Also, check that you are using Sonarr v3 and have configured a default language profile in Overseerr.
|
||||
|
||||
## Notifications
|
||||
|
||||
### I am getting "Username and Password not accepted" when sending email notifications to gmail!
|
||||
### I am getting "Username and Password not accepted" when attempting to send email notifications via Gmail!
|
||||
|
||||
**A:** If you have 2-Step Verification enabled on your account you will need to create an app password. More details can be found [here](https://support.google.com/mail/answer/185833).
|
||||
**A:** If you have 2-Step Verification enabled on your account, you will need to create an [app password](https://support.google.com/mail/answer/185833).
|
||||
|
||||
@@ -2,27 +2,24 @@
|
||||
|
||||
Overseerr already supports a good number of notification agents, such as **Discord**, **Slack** and **Pushover**. New agents are always considered for development, if there is enough demand for it.
|
||||
|
||||
## Currently Supported Notification Agents
|
||||
## Supported Notification Agents
|
||||
|
||||
- Email
|
||||
- Discord
|
||||
- [Email](./email.md)
|
||||
- [Discord](./discord.md)
|
||||
- Pushbullet
|
||||
- Pushover
|
||||
- Slack
|
||||
- Telegram
|
||||
- Pushover
|
||||
- [Webhooks](./webhooks.md)
|
||||
|
||||
## Setting up Notifications
|
||||
## Setting Up Notifications
|
||||
|
||||
Configuring your notifications is _very simple_. First, you will need to visit the **Settings** page and click **Notifications** in the menu. This will present you with all of the currently available notification agents. Click on each one individually to configure them.
|
||||
Configuring your notifications is quite simple. First, you will need to visit the **Settings** page and click **Notifications** in the menu. This will present you with all of the currently available notification agents. Click on each one individually to configure them.
|
||||
|
||||
You must configure which type of notifications you want to send _per agent_. If no types are selected, you will not receive any notifications!
|
||||
You must configure which type of notifications you want to send _per agent_. If no types are selected, you will not receive notifications!
|
||||
|
||||
Some agents may have specific configuration gotchas that will be covered in each notification agents documentation page.
|
||||
Note that some notifications are intended for the user who submitted the relevant request, while others are for administrators. For details, please see the documentation for the specific agent you would like to use.
|
||||
|
||||
{% hint style="danger" %}
|
||||
Currently, you will **not receive notifications** for any auto-approved requests. However, you will still receive a notification when the media becomes available.
|
||||
{% endhint %}
|
||||
## Requesting New Notification Agents
|
||||
|
||||
## Requesting new agents
|
||||
|
||||
If we do not currently support a notification agent you would like, feel free to request it on our [GitHub Issues](https://github.com/sct/overseerr/issues). Make sure to search first to see if someone else already requested it!
|
||||
If we do not currently support a notification agent you would like, feel free to request it on [GitHub](https://github.com/sct/overseerr/issues). However, please be sure to search first and confirm that there is not already an existing request for the agent!
|
||||
|
||||
21
docs/using-overseerr/notifications/discord.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Discord
|
||||
|
||||
## Configuration
|
||||
|
||||
{% hint style="info" %}
|
||||
In order to configure Discord notifications, you first need to [create a webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks).
|
||||
|
||||
In order for users to be mentioned in Discord notifications, they must have their [Discord user ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-) configured in their user settings.
|
||||
{% endhint %}
|
||||
|
||||
### Bot Username (optional)
|
||||
|
||||
If you would like to override the name you configured for your bot in Discord, you may set this value to whatever you like!
|
||||
|
||||
### Bot Avatar URL (optional)
|
||||
|
||||
Similar to the bot username, you can override the avatar for your bot.
|
||||
|
||||
### Webhook URL
|
||||
|
||||
You can find the webhook URL in the Discord application, at **Server Settings → Integrations → Webhooks**.
|
||||
54
docs/using-overseerr/notifications/email.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Email
|
||||
|
||||
{% hint style="info" %}
|
||||
The following email notification types are sent to _all_ users with the **Manage Requests** permission, as these notification types are intended for application administrators rather than end users:
|
||||
|
||||
- Media Requested
|
||||
- Media Automatically Approved
|
||||
- Media Failed
|
||||
|
||||
On the other hand, the email notification types below are only sent to the user who submitted the request:
|
||||
|
||||
- Media Approved
|
||||
- Media Declined
|
||||
- Media Available
|
||||
|
||||
{% endhint %}
|
||||
|
||||
## Configuration
|
||||
|
||||
### Sender Address (required)
|
||||
|
||||
Set this to the email address you would like to appear in the "from" field of the email message.
|
||||
|
||||
Depending on your email provider, this may need to be an address you own. For example, Gmail requires this to be your actual email address.
|
||||
|
||||
### Sender Name (optional)
|
||||
|
||||
Configure a friendly name for the email sender.
|
||||
|
||||
### SMTP Host
|
||||
|
||||
Set this to the hostname or IP address of your SMTP host/server.
|
||||
|
||||
### SMTP Port
|
||||
|
||||
Set this to a supported port number for your SMTP host. `465` and `587` are commonly used.
|
||||
|
||||
### Enable SSL (optional)
|
||||
|
||||
This setting should only be enabled for ports that use [implicit SSL/TLS](https://tools.ietf.org/html/rfc8314) (e.g., port `465` in most cases).
|
||||
|
||||
For servers that support [opportunistic TLS/STARTTLS](https://en.wikipedia.org/wiki/Opportunistic_TLS) (typically via port `587`), this setting should **not** be enabled.
|
||||
|
||||
### SMTP Username & Password
|
||||
|
||||
{% hint style="info" %}
|
||||
If your account has two-factor authentication enabled, you may need to create an application password instead of using your account password.
|
||||
{% endhint %}
|
||||
|
||||
Configure these values as appropriate to authenticate with your SMTP host.
|
||||
|
||||
### PGP Private Key & Password (optional)
|
||||
|
||||
Configure these values to enable encrypting and signing of email messages using [OpenPGP](https://www.openpgp.org/). Note that individual users must also have their PGP public keys enabled in their user settings in order for PGP encryption to be used.
|
||||
@@ -1,56 +1,59 @@
|
||||
# Webhooks
|
||||
|
||||
Webhooks let you post a custom JSON payload to any endpoint you like. You can also set an authorization header for security purposes.
|
||||
Webhooks allow you to send a custom JSON payload to any endpoint. You can also set an authorization header for security purposes.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following configuration options are available:
|
||||
|
||||
### Webhook URL (Required)
|
||||
### Webhook URL (required)
|
||||
|
||||
The URL you would like to post notifications to. Your JSON will be sent as the body of the request.
|
||||
|
||||
### Authorization Header
|
||||
### Authorization Header (optional)
|
||||
|
||||
Custom authorization header. Anything entered for this will be sent as an `Authorization` header.
|
||||
{% hint style="info" %}
|
||||
This is typically not needed. Please refer to your webhook provider's documentation for details.
|
||||
{% endhint %}
|
||||
|
||||
### Custom JSON Payload (Required)
|
||||
This value will be sent as an `Authorization` HTTP header.
|
||||
|
||||
Design your JSON payload as you see fit. JSON is validated before you can save or test. Overseerr provides several [template variables](./webhooks.md#template-variables) for use in the payload which will be replaced with actual values when the notifications are sent.
|
||||
### JSON Payload (required)
|
||||
|
||||
You can always reset back to the default custom payload setting by clicking the `Reset to Default JSON Payload` button under the editor.
|
||||
Customize the JSON payload to suit your needs. Overseerr provides several [template variables](./webhooks.md#template-variables) for use in the payload, which will be replaced with the relevant data when the notifications are triggered.
|
||||
|
||||
## Template Variables
|
||||
|
||||
### Main
|
||||
### General
|
||||
|
||||
- `{{notification_type}}` The type of notification. (Ex. `MEDIA_PENDING` or `MEDIA_APPROVED`)
|
||||
- `{{subject}}` The notification subject message. (For request notifications, this is the media title)
|
||||
- `{{message}}` Notification message body. (For request notifications, this is the media's overview/synopsis)
|
||||
- `{{image}}` Associated image with the request. (For request notifications, this is the media's poster)
|
||||
|
||||
### Notify User
|
||||
### User
|
||||
|
||||
These variables are usually the target user of the notification.
|
||||
|
||||
- `{{notifyuser_username}}` Target user's username.
|
||||
- `{{notifyuser_email}}` Target user's email.
|
||||
- `{{notifyuser_avatar}}` Target user's avatar.
|
||||
- `{{notifyuser_settings_discordId}}` Target user's discord ID (if one is set).
|
||||
- `{{notifyuser_settings_telegramChatId}}` Target user's telegram Chat ID (if one is set).
|
||||
|
||||
### Media
|
||||
|
||||
These variables are only included in media related notifications, such as requests.
|
||||
|
||||
- `{{media_type}}` Media type. Either `movie` or `tv`.
|
||||
- `{{media_tmdbid}}` Media's TMDB ID.
|
||||
- `{{media_imdbid}}` Media's IMDB ID.
|
||||
- `{{media_tmdbid}}` Media's TMDb ID.
|
||||
- `{{media_imdbid}}` Media's IMDb ID.
|
||||
- `{{media_tvdbid}}` Media's TVDB ID.
|
||||
- `{{media_status}}` Media's availability status. (Ex. `AVAILABLE` or `PENDING`)
|
||||
- `{{media_status4k}}` Media's 4K availability status. (Ex. `AVAILABLE` or `PENDING`)
|
||||
- `{{media_status}}` Media's availability status (e.g., `AVAILABLE` or `PENDING`).
|
||||
- `{{media_status4k}}` Media's 4K availability status (e.g., `AVAILABLE` or `PENDING`).
|
||||
|
||||
### Special Key Variables
|
||||
### Special
|
||||
|
||||
These variables must be used as a key in the JSON Payload. (Ex, `"{{extra}}": []`).
|
||||
The following variables must be used as a key in the JSON payload (e.g., `"{{extra}}": []`).
|
||||
|
||||
- `{{extra}}` This will override the value of the property to be the pre-formatted "extra" array that can come along with certain notifications. Using this variable is _not required_.
|
||||
- `{{media}}` This will override the value of the property to `null` if there is no media object passed along with the notification.
|
||||
- `{{request}}` This object will be `null` if there is no relevant request object for the notification.
|
||||
- `{{media}}` This object will be `null` if there is no relevant media object for the notification.
|
||||
- `{{extra}}` This object will contain the "extra" array of additional data for certain notifications.
|
||||
|
||||
195
docs/using-overseerr/settings/README.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Settings
|
||||
|
||||
## General
|
||||
|
||||
### API Key
|
||||
|
||||
This is your Overseerr API key, which can be used to integrate Overseerr with third-party applications. Do **not** share this key publicly, as it can be used to gain administrator access!
|
||||
|
||||
If you need to generate a new API key for any reason, simply click the button to the right of the text box.
|
||||
|
||||
### Application Title
|
||||
|
||||
If you aren't a huge fan of the name "Overseerr" and would like to display something different to your users, you can customize the application title!
|
||||
|
||||
### Application URL
|
||||
|
||||
Set this to the externally-accessible URL of your Overseerr instance. If configured, [notifications](../notifications/README.md) will include links!
|
||||
|
||||
### Enable Proxy Support
|
||||
|
||||
If you have Overseerr behind a [reverse proxy](../../extending-overseerr/reverse-proxy-examples.md), enable this setting to allow Overseerr to correctly register client IP addresses. For details, please see the [Express documentation](http://expressjs.com/en/guide/behind-proxies.html).
|
||||
|
||||
This setting is **disabled** by default.
|
||||
|
||||
### Enable CSRF Protection
|
||||
|
||||
{% hint style="danger" %}
|
||||
**This is an advanced setting.** We do not recommend enabling it unless you understand the implications of doing so.
|
||||
{% endhint %}
|
||||
|
||||
CSRF stands for **Cross-Site Request Forgery**. When this setting is enabled, all external API access that alters Overseerr application data is blocked.
|
||||
|
||||
If you do not use Overseerr integrations with third-party applications to add/modify/delete requests or users, you can consider enabling this setting to protect against malicious attacks.
|
||||
|
||||
One caveat, however, is that **HTTPS is required**, meaning that once this setting is enabled, you will no longer be able to access your Overseerr instance over HTTP (including using an IP address and port number).
|
||||
|
||||
If you enable this setting and find yourself unable to access Overseerr, you can disable the setting by modifying `settings.json` in `/app/config`.
|
||||
|
||||
This setting is **disabled** by default.
|
||||
|
||||
### Enable Image Caching
|
||||
|
||||
{% hint style="danger" %}
|
||||
**This feature is experimental.** Enable it at your own risk!
|
||||
{% endhint %}
|
||||
|
||||
When enabled, all images (including media posters from TMDb) will be cached locally on your server. Images will also be optimized for client devices; i.e., if you access Overseerr using a mobile device, smaller versions will be served compared to when accessing Overseerr on desktop.
|
||||
|
||||
Note that this feature requires and will use a significant amount of disk space, and there is currently no automated deletion of old or expired images. If running Overseerr using Docker, it is possible to manually clear the image cache by simply removing and recreating the container.
|
||||
|
||||
This setting is **disabled** by default.
|
||||
|
||||
### Discover Region & Discover Language
|
||||
|
||||
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. Users can override these global settings by configuring these same options in their user settings.
|
||||
|
||||
### Hide Available Media
|
||||
|
||||
When enabled, media which is already available will not appear on the "Discover" home page, or in the "Recommended" or "Similar" categories or other links on media detail pages.
|
||||
|
||||
Available media will still appear in search results, however, so it is possible to locate and view hidden items by searching for them by title.
|
||||
|
||||
This setting is **disabled** by default.
|
||||
|
||||
### Allow Partial Series Requests
|
||||
|
||||
When enabled, users will be able to submit requests for specific seasons of TV series. If disabled, users will only be able to submit requests for all unavailable seasons.
|
||||
|
||||
This setting is **enabled** by default.
|
||||
|
||||
## Users
|
||||
|
||||
### Enable Local User Sign-In
|
||||
|
||||
When enabled, users who have configured passwords will be allowed to sign in using their email address.
|
||||
|
||||
When disabled, Plex OAuth becomes the only sign-in option, and any "local users" you have created will not be able to sign in to Overseerr.
|
||||
|
||||
This setting is **enabled** by default.
|
||||
|
||||
### Default User Permissions
|
||||
|
||||
Select the permissions you would like new users to have by default. It is important to set these, as any user with access to your Plex server will be able to log in to Overseerr, and they will be granted the permissions you select here.
|
||||
|
||||
## Plex
|
||||
|
||||
### Plex Settings
|
||||
|
||||
{% hint style="info" %}
|
||||
To set up Plex, you can either enter your details manually or select a server retrieved from [plex.tv](https://plex.tv/). Press the button to the right of the "Server" dropdown to retrieve available servers.
|
||||
|
||||
Depending on your setup/configuration, you may need to enter your Plex server details manually in order to establish a connection from Overseerr.
|
||||
{% endhint %}
|
||||
|
||||
#### Server Name
|
||||
|
||||
This value is automatically retrieved from Plex, and cannot be edited manually in Overseerr.
|
||||
|
||||
#### Hostname or IP Address
|
||||
|
||||
If you have Overseerr installed on the same network as Plex, you can set this to the local IP address of your Plex server. Otherwise, this should be set to a valid hostname (e.g., `plex.myawesomeserver.com`).
|
||||
|
||||
#### Port
|
||||
|
||||
This value should be set to the port that your Plex server listens on. The default port that Plex uses is `32400`, but you may need to set this to `443` or some other value if your Plex server is hosted on a VPS or cloud provider.
|
||||
|
||||
#### SSL
|
||||
|
||||
Tick this box to connect to Plex via HTTPS rather than HTTP. Note that self-signed certificates are **not** supported.
|
||||
|
||||
### Plex Libraries
|
||||
|
||||
In this section, simply select the libraries you would like Overseerr to scan. Overseerr will periodically check the selected libraries for available content to update the media status that is displayed to users.
|
||||
|
||||
If you do not see your Plex libraries listed, verify your Plex settings and then click the "Scan Plex Libraries" button.
|
||||
|
||||
### Manual Library Scan
|
||||
|
||||
Overseerr will perform a full scan of your Plex libraries once every 24 hours (recently added items are fetched more frequently). If this is your first time configuring Plex, a one-time full manual library scan is recommended!
|
||||
|
||||
## Services
|
||||
|
||||
{% hint style="info" %}
|
||||
If you keep separate copies of non-4K and 4K content in your media libraries, you will need to set up multiple Radarr/Sonarr instances and link each of them to Overseerr.
|
||||
|
||||
Overseerr checks these linked servers to determine whether or not media has already been requested or is available, so two servers of each type are required _if you keep separate non-4K and 4K copies of media_.
|
||||
|
||||
If you only maintain one copy of media, you can instead simply set up one server and set the "Quality Profile" setting on a per-request basis.
|
||||
{% endhint %}
|
||||
|
||||
### Radarr/Sonarr Settings
|
||||
|
||||
#### Default Server
|
||||
|
||||
At least one server needs to be marked as "Default" in order for requests to be sent successfully to Radarr/Sonarr.
|
||||
|
||||
If you have separate 4K Radarr/Sonarr servers, you need to designate default 4K servers _in addition to_ default non-4K servers.
|
||||
|
||||
#### 4K Server
|
||||
|
||||
Only select this option if you have separate non-4K and 4K servers. If you only have a single Radarr/Sonarr server, do **not** check this box!
|
||||
|
||||
#### Server Name
|
||||
|
||||
Enter a friendly name for the Radarr/Sonarr server.
|
||||
|
||||
#### Hostname or IP Address
|
||||
|
||||
If you have Overseerr installed on the same network as Radarr/Sonarr, you can set this to the local IP address of your Radarr/Sonarr server. Otherwise, this should be set to a valid hostname (e.g., `radarr.myawesomeserver.com`).
|
||||
|
||||
#### Port
|
||||
|
||||
This value should be set to the port that your Radarr/Sonarr server listens on. By default, Radarr uses port `7878` and Sonarr uses port `8989`, but you may need to set this to `443` or some other value if your Radarr/Sonarr server is hosted on a VPS or cloud provider.
|
||||
|
||||
#### SSL
|
||||
|
||||
Tick this box to connect to Radarr/Sonarr via HTTPS rather than HTTP. Note that self-signed certificates are **not** supported.
|
||||
|
||||
#### API Key
|
||||
|
||||
Enter your Radarr/Sonarr API key here. Do **not** share these key publicly, as they can be used to gain administrator access to your Radarr/Sonarr servers!
|
||||
|
||||
You can locate the required API keys in Radarr/Sonarr in **Settings → General → Security**.
|
||||
|
||||
#### Base URL
|
||||
|
||||
If you have configured a base URL for Radarr/Sonarr, you **must** enter it here in order for Overseerr to connect to those services!
|
||||
|
||||
You can verify whether or not you have a base URL configured in Radarr/Sonarr in **Settings → General → Host**. (Note that a restart of your Radarr/Sonarr servers is required if you modify this setting!)
|
||||
|
||||
#### Profiles, Root Folder, Minimum Availability
|
||||
|
||||
Select the default settings you would like to use for all new requests. Note that all of these options are required, and that requests will fail if any of these are not configured!
|
||||
|
||||
#### External URL
|
||||
|
||||
If the hostname or IP address you configured above is not accessible outside your network, you can set a different URL here. This "external" URL is used to add clickable links to your Radarr/Sonarr servers on media detail pages.
|
||||
|
||||
#### Enable Scan
|
||||
|
||||
Tick this box if you would like to scan your Radarr/Sonarr server for existing media/request status. It is recommended that you enable this setting, so that users cannot submit requests for media which has already been requested or is already available.
|
||||
|
||||
#### Disable Auto-Search
|
||||
|
||||
If you do not want Radarr/Sonarr to automatically search for media upon submission of a request, you can disable this setting.
|
||||
|
||||
## Notifications
|
||||
|
||||
Please see [Notifications](../notifications/README.md) for details on configuring and enabling notifications.
|
||||
|
||||
## Jobs & Cache
|
||||
|
||||
Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered on this page.
|
||||
|
||||
Overseerr also caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls. If necessary, the cache for any particular endpoint can be cleared by clicking the "Flush Cache" button.
|
||||
@@ -2,12 +2,16 @@ module.exports = {
|
||||
env: {
|
||||
commitTag: process.env.COMMIT_TAG || 'local',
|
||||
},
|
||||
images: {
|
||||
domains: ['image.tmdb.org'],
|
||||
},
|
||||
future: {
|
||||
webpack5: true,
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
issuer: {
|
||||
test: /\.(js|ts)x?$/,
|
||||
},
|
||||
issuer: /\.(js|ts)x?$/,
|
||||
use: ['@svgr/webpack'],
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const devConfig = {
|
||||
synchronize: true,
|
||||
migrationsRun: false,
|
||||
logging: false,
|
||||
enableWAL: true,
|
||||
entities: ['server/entity/**/*.ts'],
|
||||
migrations: ['server/migration/**/*.ts'],
|
||||
subscribers: ['server/subscriber/**/*.ts'],
|
||||
@@ -22,6 +23,7 @@ const prodConfig = {
|
||||
: 'config/db/db.sqlite3',
|
||||
synchronize: false,
|
||||
logging: false,
|
||||
enableWAL: true,
|
||||
entities: ['dist/entity/**/*.js'],
|
||||
migrations: ['dist/migration/**/*.js'],
|
||||
migrationsRun: false,
|
||||
|
||||
2253
overseerr-api.yml
144
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "overseerr",
|
||||
"version": "1.17.2",
|
||||
"version": "1.22.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,7 +9,7 @@
|
||||
"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 -o src/i18n/locale -d en --flat true --overwriteDefault false './src/**/!(*.test).{ts,tsx}'",
|
||||
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault true \"./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",
|
||||
@@ -17,110 +17,127 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^0.3.1",
|
||||
"@supercharge/request-ip": "^1.1.2",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"@tanem/react-nprogress": "^3.0.60",
|
||||
"ace-builds": "^1.4.12",
|
||||
"axios": "^0.21.1",
|
||||
"bcrypt": "^5.0.0",
|
||||
"bcrypt": "^5.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"bowser": "^2.11.0",
|
||||
"connect-typeorm": "^1.1.4",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"email-templates": "^8.0.3",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"country-flag-icons": "^1.2.9",
|
||||
"csurf": "^1.11.0",
|
||||
"email-templates": "^8.0.4",
|
||||
"express": "^4.17.1",
|
||||
"express-openapi-validator": "^4.10.8",
|
||||
"express-openapi-validator": "^4.12.6",
|
||||
"express-rate-limit": "^5.2.6",
|
||||
"express-session": "^1.17.1",
|
||||
"formik": "^2.2.6",
|
||||
"gravatar-url": "^3.1.0",
|
||||
"intl": "^1.2.5",
|
||||
"lodash": "^4.17.20",
|
||||
"next": "10.0.3",
|
||||
"node-schedule": "^1.3.2",
|
||||
"nodemailer": "^6.4.17",
|
||||
"nookies": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "10.1.2",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-schedule": "^2.0.0",
|
||||
"nodemailer": "^6.5.0",
|
||||
"nookies": "^2.5.2",
|
||||
"openpgp": "^5.0.0-1",
|
||||
"plex-api": "^5.3.1",
|
||||
"pug": "^3.0.0",
|
||||
"react": "17.0.1",
|
||||
"react-ace": "^9.2.1",
|
||||
"react-dom": "17.0.1",
|
||||
"pug": "^3.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-ace": "^9.3.0",
|
||||
"react-animate-height": "^2.0.23",
|
||||
"react-dom": "17.0.2",
|
||||
"react-intersection-observer": "^8.31.0",
|
||||
"react-intl": "^5.10.16",
|
||||
"react-intl": "5.13.5",
|
||||
"react-markdown": "^5.0.3",
|
||||
"react-select": "^4.3.0",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-toast-notifications": "^2.4.0",
|
||||
"react-toast-notifications": "^2.4.3",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-truncate-markup": "^5.1.0",
|
||||
"react-use-clipboard": "1.0.7",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"secure-random-password": "^0.2.2",
|
||||
"sqlite3": "^5.0.0",
|
||||
"sqlite3": "^5.0.2",
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
"swr": "^0.4.0",
|
||||
"typeorm": "^0.2.30",
|
||||
"swr": "^0.5.5",
|
||||
"typeorm": "^0.2.32",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.0",
|
||||
"winston-daily-rotate-file": "^4.5.1",
|
||||
"xml2js": "^0.4.23",
|
||||
"yamljs": "^0.3.0",
|
||||
"yup": "^0.32.8"
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@commitlint/cli": "^11.0.0",
|
||||
"@commitlint/config-conventional": "^11.0.0",
|
||||
"@babel/cli": "^7.13.14",
|
||||
"@commitlint/cli": "^12.0.1",
|
||||
"@commitlint/config-conventional": "^12.0.1",
|
||||
"@semantic-release/changelog": "^5.0.1",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/exec": "^5.0.0",
|
||||
"@semantic-release/git": "^9.0.0",
|
||||
"@tailwindcss/aspect-ratio": "^0.2.0",
|
||||
"@tailwindcss/forms": "^0.2.1",
|
||||
"@tailwindcss/forms": "^0.3.2",
|
||||
"@tailwindcss/typography": "^0.4.0",
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/email-templates": "^8.0.0",
|
||||
"@types/country-flag-icons": "^1.2.0",
|
||||
"@types/csurf": "^1.11.0",
|
||||
"@types/email-templates": "^8.0.2",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/express-session": "^1.17.0",
|
||||
"@types/lodash": "^4.14.167",
|
||||
"@types/node": "^14.14.21",
|
||||
"@types/express-rate-limit": "^5.1.1",
|
||||
"@types/express-session": "^1.17.3",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/node-schedule": "^1.3.1",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/nodemailer": "^6.4.1",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@types/react-select": "^4.0.13",
|
||||
"@types/react-toast-notifications": "^2.4.0",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/react-transition-group": "^4.4.1",
|
||||
"@types/secure-random-password": "^0.2.0",
|
||||
"@types/swagger-ui-express": "^4.1.2",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/xml2js": "^0.4.7",
|
||||
"@types/xml2js": "^0.4.8",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"@types/yup": "^0.29.11",
|
||||
"@typescript-eslint/eslint-plugin": "^4.13.0",
|
||||
"@typescript-eslint/parser": "^4.13.0",
|
||||
"autoprefixer": "^9",
|
||||
"@typescript-eslint/eslint-plugin": "^4.20.0",
|
||||
"@typescript-eslint/parser": "^4.20.0",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"babel-plugin-react-intl": "^8.2.25",
|
||||
"babel-plugin-react-intl-auto": "^3.3.0",
|
||||
"commitizen": "^4.2.3",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^7.18.0",
|
||||
"eslint-config-prettier": "^7.1.0",
|
||||
"eslint-plugin-formatjs": "^2.10.3",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-formatjs": "^2.14.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"eslint-plugin-react": "^7.23.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^10.5.3",
|
||||
"husky": "4.3.8",
|
||||
"lint-staged": "^10.5.4",
|
||||
"nodemon": "^2.0.7",
|
||||
"postcss": "^7",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"postcss": "^8.2.9",
|
||||
"prettier": "^2.2.1",
|
||||
"semantic-release": "^17.3.3",
|
||||
"semantic-release-docker": "^2.2.0",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
|
||||
"semantic-release": "^17.4.2",
|
||||
"semantic-release-docker-buildx": "^1.0.1",
|
||||
"tailwindcss": "^2.0.4",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"sqlite3/node-gyp": "^5.1.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
@@ -137,12 +154,10 @@
|
||||
"lint-staged": {
|
||||
"**/*.{ts,tsx,js}": [
|
||||
"prettier --write",
|
||||
"eslint",
|
||||
"git add"
|
||||
"eslint"
|
||||
],
|
||||
"**/*.{json,md}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"commitlint": {
|
||||
@@ -175,13 +190,7 @@
|
||||
"message": "chore(release): ${nextRelease.version}"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/exec",
|
||||
{
|
||||
"prepareCmd": "docker build --build-arg COMMIT_TAG=$GITHUB_SHA -t sctx/overseerr ."
|
||||
}
|
||||
],
|
||||
"semantic-release-docker",
|
||||
"semantic-release-docker-buildx",
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
@@ -195,8 +204,19 @@
|
||||
"npmPublish": false,
|
||||
"publish": [
|
||||
{
|
||||
"path": "semantic-release-docker",
|
||||
"name": "sctx/overseerr"
|
||||
"path": "semantic-release-docker-buildx",
|
||||
"buildArgs": {
|
||||
"COMMIT_TAG": "$GITHUB_SHA"
|
||||
},
|
||||
"imageNames": [
|
||||
"sctx/overseerr",
|
||||
"ghcr.io/sct/overseerr"
|
||||
],
|
||||
"platforms": [
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
"linux/arm/v7"
|
||||
]
|
||||
},
|
||||
"@semantic-release/github"
|
||||
]
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: ['tailwindcss', 'postcss-preset-env'],
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 70 KiB |
BIN
public/apple-splash-1125-2436.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/apple-splash-1136-640.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/apple-splash-1170-2532.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/apple-splash-1242-2208.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/apple-splash-1242-2688.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
public/apple-splash-1284-2778.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/apple-splash-1334-750.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/apple-splash-1536-2048.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/apple-splash-1620-2160.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/apple-splash-1668-2224.jpg
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/apple-splash-1668-2388.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/apple-splash-1792-828.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/apple-splash-2048-1536.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/apple-splash-2048-2732.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/apple-splash-2160-1620.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/apple-splash-2208-1242.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/apple-splash-2224-1668.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
public/apple-splash-2388-1668.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/apple-splash-2436-1125.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/apple-splash-2532-1170.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
public/apple-splash-2688-1242.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
public/apple-splash-2732-2048.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/apple-splash-2778-1284.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
public/apple-splash-640-1136.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/apple-splash-750-1334.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/apple-splash-828-1792.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/overseerr_poster_not_found.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/overseerr_poster_not_found_logo_center.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/images/overseerr_poster_not_found_logo_top.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB |
1
public/images/radarr_logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 1000 1115.2"><metadata/><defs><path id="a" d="m0 0h1024v1024h-1024z"/></defs><use width="100%" height="100%" fill="#fff" fill-opacity="0" transform="rotate(.704 1914.4 -5491.6)" xlink:href="#a"/><g><path fill="#fff" d="m105.76 154.15-1.263 714.59c-60.261 6.782-105.02-23.858-104.28-84.025l-0.216-594.25c2.312-188.02 175.85-231.02 280.22-154.52l530.2 314.93c74.563 53.572 88.403 151.53 49.965 218.76-6.873-52.739-29.067-83.101-73.823-113.74l-597.52-345.84c-44.756-30.639-82.453-23.58-83.286 44.109zm-54.377 751.54c44.941 15.597 90.16 8.63 128.04-13.47l621.16-353.43c36.958 53.109 28.79 105.66-16.706 135.19l-522.65 294.46c-75.673 36.68-172.98-2.127-209.85-62.757z" transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)"/><path fill="#ffc230" d="m240.52 702.59 365.02-216.68-364.35-197.07z" transform="matrix(1.1348 0 0 1.1348 -.0011348 -.013738)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 969 B |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 20 KiB |
1
public/images/sonarr_logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="216.7" height="216.9" viewBox="0 0 216.7 216.9"><path fill="#EEE" fill-rule="evenodd" d="M216.7 108.45c0 29.833-10.533 55.4-31.6 76.7-.7.833-1.483 1.6-2.35 2.3-3.466 3.4-7.133 6.484-11 9.25-18.267 13.467-39.367 20.2-63.3 20.2-23.967 0-45.033-6.733-63.2-20.2-4.8-3.4-9.3-7.25-13.5-11.55-16.367-16.266-26.417-35.167-30.15-56.7-.733-4.2-1.217-8.467-1.45-12.8-.1-2.4-.15-4.8-.15-7.2 0-2.533.05-4.95.15-7.25 0-.233.066-.467.2-.7 1.567-26.6 12.033-49.583 31.4-68.95C53.05 10.517 78.617 0 108.45 0c29.933 0 55.484 10.517 76.65 31.55 21.067 21.433 31.6 47.067 31.6 76.9z" clip-rule="evenodd"/><path fill="#3A3F51" fill-rule="evenodd" d="M194.65 42.5l-22.4 22.4C159.152 77.998 158 89.4 158 109.5c0 17.934 2.852 34.352 16.2 47.7 9.746 9.746 19 18.95 19 18.95-2.5 3.067-5.2 6.067-8.1 9-.7.833-1.483 1.6-2.35 2.3-2.533 2.5-5.167 4.817-7.9 6.95l-17.55-17.55c-15.598-15.6-27.996-17.1-48.6-17.1-19.77 0-33.223 1.822-47.7 16.3-8.647 8.647-18.55 18.6-18.55 18.6-3.767-2.867-7.333-6.034-10.7-9.5-2.8-2.8-5.417-5.667-7.85-8.6 0 0 9.798-9.848 19.15-19.2 13.852-13.853 16.1-29.916 16.1-47.85 0-17.5-2.874-33.823-15.6-46.55-8.835-8.836-21.05-21-21.05-21 2.833-3.6 5.917-7.067 9.25-10.4 2.934-2.867 5.934-5.55 9-8.05L61.1 43.85C74.102 56.852 90.767 60.2 108.7 60.2c18.467 0 35.077-3.577 48.6-17.1 8.32-8.32 19.3-19.25 19.3-19.25 2.9 2.367 5.733 4.933 8.5 7.7 3.467 3.533 6.65 7.183 9.55 10.95z" clip-rule="evenodd"/><g clip-rule="evenodd"><path fill="#0CF" fill-rule="evenodd" d="M78.7 114c-.2-1.167-.332-2.35-.4-3.55-.032-.667-.05-1.333-.05-2 0-.7.018-1.367.05-2 0-.067.018-.133.05-.2.435-7.367 3.334-13.733 8.7-19.1 5.9-5.833 12.984-8.75 21.25-8.75 8.3 0 15.384 2.917 21.25 8.75 5.834 5.934 8.75 13.033 8.75 21.3 0 8.267-2.916 15.35-8.75 21.25-.2.233-.416.45-.65.65-.966.933-1.982 1.783-3.05 2.55-5.065 3.733-10.916 5.6-17.55 5.6s-12.466-1.866-17.5-5.6c-1.332-.934-2.582-2-3.75-3.2-4.532-4.5-7.316-9.734-8.35-15.7z"/><path fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="2" d="M157.8 59.75l-15 14.65M30.785 32.526L71.65 73.25m84.6 84.25l27.808 28.78m1.855-153.894L157.8 59.75m-125.45 126l27.35-27.4"/><path fill="none" stroke="#0CF" stroke-miterlimit="1" stroke-width="7" d="M157.8 59.75l-16.95 17.2M58.97 60.604l17.2 17.15M59.623 158.43l16.75-17.4m61.928-1.396l18.028 17.945"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/logo.png
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
@@ -6,12 +6,14 @@
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#2d3748",
|
||||
|
||||
@@ -8,11 +8,13 @@ const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapp
|
||||
// originally at https://raw.githubusercontent.com/ScudLee/anime-lists/master/anime-list.xml
|
||||
const MAPPING_URL =
|
||||
'https://raw.githubusercontent.com/Anime-Lists/anime-lists/master/anime-list.xml';
|
||||
const LOCAL_PATH = path.join(__dirname, '../../config/anime-list.xml');
|
||||
const LOCAL_PATH = process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/anime-list.xml`
|
||||
: path.join(__dirname, '../../config/anime-list.xml');
|
||||
|
||||
const mappingRegexp = new RegExp(/;[0-9]+-([0-9]+)/g);
|
||||
|
||||
// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to tvdb/tmdb IDs
|
||||
// Anime-List xml files are community maintained mappings that Hama agent uses to map AniDB IDs to TVDB/TMDb IDs
|
||||
// https://github.com/Anime-Lists/anime-lists/
|
||||
|
||||
interface AnimeMapping {
|
||||
@@ -125,7 +127,7 @@ class AnimeListMapping {
|
||||
}
|
||||
} else {
|
||||
// some movies do not have mapping-list, so map episode 1,2,3,..to movies
|
||||
// movies must have imdbid or tmdbid
|
||||
// movies must have imdbId or tmdbId
|
||||
const hasImdb = imdbIds.length > 1 || imdbIds[0] !== undefined;
|
||||
if ((hasImdb || tmdbId) && anime.$.defaulttvdbseason === '0') {
|
||||
if (!this.specials[tvdbId]) {
|
||||
|
||||
102
server/api/externalapi.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import NodeCache from 'node-cache';
|
||||
|
||||
// 5 minute default TTL (in seconds)
|
||||
const DEFAULT_TTL = 300;
|
||||
|
||||
// 10 seconds default rolling buffer (in ms)
|
||||
const DEFAULT_ROLLING_BUFFER = 10000;
|
||||
|
||||
interface ExternalAPIOptions {
|
||||
nodeCache?: NodeCache;
|
||||
headers?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class ExternalAPI {
|
||||
protected axios: AxiosInstance;
|
||||
private baseUrl: string;
|
||||
private cache?: NodeCache;
|
||||
|
||||
constructor(
|
||||
baseUrl: string,
|
||||
params: Record<string, unknown>,
|
||||
options: ExternalAPIOptions = {}
|
||||
) {
|
||||
this.axios = axios.create({
|
||||
baseURL: baseUrl,
|
||||
params,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
this.baseUrl = baseUrl;
|
||||
this.cache = options.nodeCache;
|
||||
}
|
||||
|
||||
protected async get<T>(
|
||||
endpoint: string,
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const response = await this.axios.get<T>(endpoint, config);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async getRolling<T>(
|
||||
endpoint: string,
|
||||
config?: AxiosRequestConfig,
|
||||
ttl?: number
|
||||
): Promise<T> {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
|
||||
if (cachedItem) {
|
||||
const keyTtl = this.cache?.getTtl(cacheKey) ?? 0;
|
||||
|
||||
// If the item has passed our rolling check, fetch again in background
|
||||
if (
|
||||
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
|
||||
Date.now() - DEFAULT_ROLLING_BUFFER
|
||||
) {
|
||||
this.axios.get<T>(endpoint, config).then((response) => {
|
||||
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
});
|
||||
}
|
||||
return cachedItem;
|
||||
}
|
||||
|
||||
const response = await this.axios.get<T>(endpoint, config);
|
||||
|
||||
if (this.cache) {
|
||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
private serializeCacheKey(
|
||||
endpoint: string,
|
||||
params?: Record<string, unknown>
|
||||
) {
|
||||
if (!params) {
|
||||
return `${this.baseUrl}${endpoint}`;
|
||||
}
|
||||
|
||||
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ExternalAPI;
|
||||
@@ -1,5 +1,5 @@
|
||||
import NodePlexAPI from 'plex-api';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import { getSettings, PlexSettings } from '../lib/settings';
|
||||
|
||||
export interface PlexLibraryItem {
|
||||
ratingKey: string;
|
||||
@@ -80,13 +80,26 @@ interface PlexMetadataResponse {
|
||||
class PlexAPI {
|
||||
private plexClient: NodePlexAPI;
|
||||
|
||||
constructor({ plexToken }: { plexToken?: string }) {
|
||||
constructor({
|
||||
plexToken,
|
||||
plexSettings,
|
||||
timeout,
|
||||
}: {
|
||||
plexToken?: string;
|
||||
plexSettings?: PlexSettings;
|
||||
timeout?: number;
|
||||
}) {
|
||||
const settings = getSettings();
|
||||
let settingsPlex: PlexSettings | undefined;
|
||||
plexSettings
|
||||
? (settingsPlex = plexSettings)
|
||||
: (settingsPlex = getSettings().plex);
|
||||
|
||||
this.plexClient = new NodePlexAPI({
|
||||
hostname: settings.plex.ip,
|
||||
port: settings.plex.port,
|
||||
https: settings.plex.useSsl,
|
||||
hostname: settingsPlex.ip,
|
||||
port: settingsPlex.port,
|
||||
https: settingsPlex.useSsl,
|
||||
timeout: timeout,
|
||||
token: plexToken,
|
||||
authenticator: {
|
||||
authenticate: (
|
||||
@@ -111,6 +124,7 @@ class PlexAPI {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
public async getStatus() {
|
||||
return await this.plexClient.query('/');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import xml2js from 'xml2js';
|
||||
import { PlexDevice } from '../interfaces/api/plexInterfaces';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
|
||||
@@ -29,6 +30,45 @@ interface PlexUser {
|
||||
entitlements: string[];
|
||||
}
|
||||
|
||||
interface ConnectionResponse {
|
||||
$: {
|
||||
protocol: string;
|
||||
address: string;
|
||||
port: string;
|
||||
uri: string;
|
||||
local: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeviceResponse {
|
||||
$: {
|
||||
name: string;
|
||||
product: string;
|
||||
productVersion: string;
|
||||
platform: string;
|
||||
platformVersion: string;
|
||||
device: string;
|
||||
clientIdentifier: string;
|
||||
createdAt: string;
|
||||
lastSeenAt: string;
|
||||
provides: string;
|
||||
owned: string;
|
||||
accessToken?: string;
|
||||
publicAddress?: string;
|
||||
httpsRequired?: string;
|
||||
synced?: string;
|
||||
relay?: string;
|
||||
dnsRebindingProtection?: string;
|
||||
natLoopbackSupported?: string;
|
||||
publicAddressMatches?: string;
|
||||
presence?: string;
|
||||
ownerID?: string;
|
||||
home?: string;
|
||||
sourceTitle?: string;
|
||||
};
|
||||
Connection: ConnectionResponse[];
|
||||
}
|
||||
|
||||
interface ServerResponse {
|
||||
$: {
|
||||
id: string;
|
||||
@@ -87,6 +127,62 @@ class PlexTvAPI {
|
||||
});
|
||||
}
|
||||
|
||||
public async getDevices(): Promise<PlexDevice[]> {
|
||||
try {
|
||||
const devicesResp = await this.axios.get(
|
||||
'/api/resources?includeHttps=1',
|
||||
{
|
||||
transformResponse: [],
|
||||
responseType: 'text',
|
||||
}
|
||||
);
|
||||
const parsedXml = await xml2js.parseStringPromise(
|
||||
devicesResp.data as DeviceResponse
|
||||
);
|
||||
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
|
||||
name: pxml.$.name,
|
||||
product: pxml.$.product,
|
||||
productVersion: pxml.$.productVersion,
|
||||
platform: pxml.$?.platform,
|
||||
platformVersion: pxml.$?.platformVersion,
|
||||
device: pxml.$?.device,
|
||||
clientIdentifier: pxml.$.clientIdentifier,
|
||||
createdAt: new Date(parseInt(pxml.$?.createdAt, 10) * 1000),
|
||||
lastSeenAt: new Date(parseInt(pxml.$?.lastSeenAt, 10) * 1000),
|
||||
provides: pxml.$.provides.split(','),
|
||||
owned: pxml.$.owned == '1' ? true : false,
|
||||
accessToken: pxml.$?.accessToken,
|
||||
publicAddress: pxml.$?.publicAddress,
|
||||
publicAddressMatches:
|
||||
pxml.$?.publicAddressMatches == '1' ? true : false,
|
||||
httpsRequired: pxml.$?.httpsRequired == '1' ? true : false,
|
||||
synced: pxml.$?.synced == '1' ? true : false,
|
||||
relay: pxml.$?.relay == '1' ? true : false,
|
||||
dnsRebindingProtection:
|
||||
pxml.$?.dnsRebindingProtection == '1' ? true : false,
|
||||
natLoopbackSupported:
|
||||
pxml.$?.natLoopbackSupported == '1' ? true : false,
|
||||
presence: pxml.$?.presence == '1' ? true : false,
|
||||
ownerID: pxml.$?.ownerID,
|
||||
home: pxml.$?.home == '1' ? true : false,
|
||||
sourceTitle: pxml.$?.sourceTitle,
|
||||
connection: pxml?.Connection?.map((conn: ConnectionResponse) => ({
|
||||
protocol: conn.$.protocol,
|
||||
address: conn.$.address,
|
||||
port: parseInt(conn.$.port, 10),
|
||||
uri: conn.$.uri,
|
||||
local: conn.$.local == '1' ? true : false,
|
||||
})),
|
||||
}));
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong getting the devices from plex.tv', {
|
||||
label: 'Plex.tv API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
throw new Error('Invalid auth token');
|
||||
}
|
||||
}
|
||||
|
||||
public async getUser(): Promise<PlexUser> {
|
||||
try {
|
||||
const account = await this.axios.get<PlexAccountResponse>(
|
||||
@@ -96,7 +192,7 @@ class PlexTvAPI {
|
||||
return account.data.user;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Something went wrong getting the account from plex.tv: ${e.message}`,
|
||||
`Something went wrong while getting the account from plex.tv: ${e.message}`,
|
||||
{ label: 'Plex.tv API' }
|
||||
);
|
||||
throw new Error('Invalid auth token');
|
||||
@@ -116,7 +212,7 @@ class PlexTvAPI {
|
||||
return parsedXml;
|
||||
}
|
||||
|
||||
public async checkUserAccess(authUser: PlexUser): Promise<boolean> {
|
||||
public async checkUserAccess(userId: number): Promise<boolean> {
|
||||
const settings = getSettings();
|
||||
|
||||
try {
|
||||
@@ -128,11 +224,11 @@ class PlexTvAPI {
|
||||
|
||||
const users = friends.MediaContainer.User;
|
||||
|
||||
const user = users.find((u) => Number(u.$.id) === authUser.id);
|
||||
const user = users.find((u) => Number(u.$.id) === userId);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(
|
||||
'This user does not exist on the main plex accounts shared list'
|
||||
"This user does not exist on the main Plex account's shared list"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Axios, { AxiosInstance } from 'axios';
|
||||
import cacheManager from '../lib/cache';
|
||||
import { RadarrSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface RadarrMovieOptions {
|
||||
title: string;
|
||||
@@ -13,12 +15,13 @@ interface RadarrMovieOptions {
|
||||
searchNow?: boolean;
|
||||
}
|
||||
|
||||
interface RadarrMovie {
|
||||
export interface RadarrMovie {
|
||||
id: number;
|
||||
title: string;
|
||||
isAvailable: boolean;
|
||||
monitored: boolean;
|
||||
tmdbId: number;
|
||||
imdbId: string;
|
||||
titleSlug: string;
|
||||
folderName: string;
|
||||
path: string;
|
||||
@@ -45,15 +48,49 @@ export interface RadarrProfile {
|
||||
name: string;
|
||||
}
|
||||
|
||||
class RadarrAPI {
|
||||
private axios: AxiosInstance;
|
||||
interface QueueItem {
|
||||
movieId: number;
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: string;
|
||||
trackedDownloadState: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
indexer: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface QueueResponse {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey: string;
|
||||
sortDirection: string;
|
||||
totalRecords: number;
|
||||
records: QueueItem[];
|
||||
}
|
||||
|
||||
class RadarrAPI extends ExternalAPI {
|
||||
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
|
||||
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
radarrSettings.hostname
|
||||
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
|
||||
}
|
||||
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
this.axios = Axios.create({
|
||||
baseURL: url,
|
||||
params: {
|
||||
super(
|
||||
url,
|
||||
{
|
||||
apikey: apiKey,
|
||||
},
|
||||
});
|
||||
{
|
||||
nodeCache: cacheManager.getCache('radarr').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public getMovies = async (): Promise<RadarrMovie[]> => {
|
||||
@@ -76,8 +113,89 @@ class RadarrAPI {
|
||||
}
|
||||
};
|
||||
|
||||
public addMovie = async (options: RadarrMovieOptions): Promise<boolean> => {
|
||||
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
|
||||
try {
|
||||
const response = await this.axios.get<RadarrMovie[]>('/movie/lookup', {
|
||||
params: {
|
||||
term: `tmdb:${id}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.data[0]) {
|
||||
throw new Error('Movie not found');
|
||||
}
|
||||
|
||||
return response.data[0];
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving movie by TMDb ID', {
|
||||
label: 'Radarr API',
|
||||
message: e.message,
|
||||
});
|
||||
throw new Error('Movie not found');
|
||||
}
|
||||
}
|
||||
|
||||
public addMovie = async (
|
||||
options: RadarrMovieOptions
|
||||
): Promise<RadarrMovie> => {
|
||||
try {
|
||||
const movie = await this.getMovieByTmdbId(options.tmdbId);
|
||||
|
||||
if (movie.downloaded) {
|
||||
logger.info(
|
||||
'Title already exists and is available. Skipping add and returning success',
|
||||
{
|
||||
label: 'Radarr',
|
||||
}
|
||||
);
|
||||
return movie;
|
||||
}
|
||||
|
||||
// movie exists in radarr but is neither downloaded nor monitored
|
||||
if (movie.id && !movie.monitored) {
|
||||
const response = await this.axios.put<RadarrMovie>(`/movie`, {
|
||||
...movie,
|
||||
title: options.title,
|
||||
qualityProfileId: options.qualityProfileId,
|
||||
profileId: options.profileId,
|
||||
titleSlug: options.tmdbId.toString(),
|
||||
minimumAvailability: options.minimumAvailability,
|
||||
tmdbId: options.tmdbId,
|
||||
year: options.year,
|
||||
rootFolderPath: options.rootFolderPath,
|
||||
monitored: options.monitored,
|
||||
addOptions: {
|
||||
searchForMovie: options.searchNow,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data.monitored) {
|
||||
logger.info(
|
||||
'Found existing title in Radarr and set it to monitored. Returning success',
|
||||
{ label: 'Radarr' }
|
||||
);
|
||||
logger.debug('Radarr update details', {
|
||||
label: 'Radarr',
|
||||
movie: response.data,
|
||||
});
|
||||
return response.data;
|
||||
} else {
|
||||
logger.error('Failed to update existing movie in Radarr', {
|
||||
label: 'Radarr',
|
||||
options,
|
||||
});
|
||||
throw new Error('Failed to update existing movie in Radarr');
|
||||
}
|
||||
}
|
||||
|
||||
if (movie.id) {
|
||||
logger.info(
|
||||
'Movie is already monitored in Radarr. Skipping add and returning success',
|
||||
{ label: 'Radarr' }
|
||||
);
|
||||
return movie;
|
||||
}
|
||||
|
||||
const response = await this.axios.post<RadarrMovie>(`/movie`, {
|
||||
title: options.title,
|
||||
qualityProfileId: options.qualityProfileId,
|
||||
@@ -104,9 +222,9 @@ class RadarrAPI {
|
||||
label: 'Radarr',
|
||||
options,
|
||||
});
|
||||
return false;
|
||||
throw new Error('Failed to add movie to Radarr');
|
||||
}
|
||||
return true;
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
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.',
|
||||
@@ -117,18 +235,19 @@ class RadarrAPI {
|
||||
response: e?.response?.data,
|
||||
}
|
||||
);
|
||||
if (e?.response?.data?.[0]?.errorCode === 'MovieExistsValidator') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
throw new Error('Failed to add movie to Radarr');
|
||||
}
|
||||
};
|
||||
|
||||
public getProfiles = async (): Promise<RadarrProfile[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<RadarrProfile[]>(`/profile`);
|
||||
const data = await this.getRolling<RadarrProfile[]>(
|
||||
`/qualityProfile`,
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve profiles: ${e.message}`);
|
||||
}
|
||||
@@ -136,13 +255,27 @@ class RadarrAPI {
|
||||
|
||||
public getRootFolders = async (): Promise<RadarrRootFolder[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<RadarrRootFolder[]>(`/rootfolder`);
|
||||
const data = await this.getRolling<RadarrRootFolder[]>(
|
||||
`/rootfolder`,
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve root folders: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getQueue = async (): Promise<QueueItem[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<QueueResponse>(`/queue`);
|
||||
|
||||
return response.data.records;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve queue: ${e.message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default RadarrAPI;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import cacheManager from '../lib/cache';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface RTMovieOldSearchResult {
|
||||
id: number;
|
||||
@@ -55,17 +56,19 @@ export interface RTRating {
|
||||
* Unfortunately, we need to do it by searching for the movie name, so it's
|
||||
* not always accurate.
|
||||
*/
|
||||
class RottenTomatoes {
|
||||
private axios: AxiosInstance;
|
||||
|
||||
class RottenTomatoes extends ExternalAPI {
|
||||
constructor() {
|
||||
this.axios = axios.create({
|
||||
baseURL: 'https://www.rottentomatoes.com/api/private',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
super(
|
||||
'https://www.rottentomatoes.com/api/private',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
nodeCache: cacheManager.getCache('rt').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,33 +88,30 @@ class RottenTomatoes {
|
||||
year: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const response = await this.axios.get<RTMovieSearchResponse>(
|
||||
'/v1.0/movies',
|
||||
{
|
||||
params: { q: name },
|
||||
}
|
||||
);
|
||||
const data = await this.get<RTMovieSearchResponse>('/v1.0/movies', {
|
||||
params: { q: name },
|
||||
});
|
||||
|
||||
// First, attempt to match exact name and year
|
||||
let movie = response.data.movies.find(
|
||||
let movie = data.movies.find(
|
||||
(movie) => movie.year === year && movie.title === name
|
||||
);
|
||||
|
||||
// If we don't find a movie, try to match partial name and year
|
||||
if (!movie) {
|
||||
movie = response.data.movies.find(
|
||||
movie = data.movies.find(
|
||||
(movie) => movie.year === year && movie.title.includes(name)
|
||||
);
|
||||
}
|
||||
|
||||
// If we still dont find a movie, try to match just on year
|
||||
if (!movie) {
|
||||
movie = response.data.movies.find((movie) => movie.year === year);
|
||||
movie = data.movies.find((movie) => movie.year === year);
|
||||
}
|
||||
|
||||
// One last try, try exact name match only
|
||||
if (!movie) {
|
||||
movie = response.data.movies.find((movie) => movie.title === name);
|
||||
movie = data.movies.find((movie) => movie.title === name);
|
||||
}
|
||||
|
||||
if (!movie) {
|
||||
@@ -139,19 +139,14 @@ class RottenTomatoes {
|
||||
year?: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const response = await this.axios.get<RTMultiSearchResponse>(
|
||||
'/v2.0/search/',
|
||||
{
|
||||
params: { q: name, limit: 10 },
|
||||
}
|
||||
);
|
||||
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
||||
params: { q: name, limit: 10 },
|
||||
});
|
||||
|
||||
let tvshow: RTTvSearchResult | undefined = response.data.tvSeries[0];
|
||||
let tvshow: RTTvSearchResult | undefined = data.tvSeries[0];
|
||||
|
||||
if (year) {
|
||||
tvshow = response.data.tvSeries.find(
|
||||
(series) => series.startYear === year
|
||||
);
|
||||
tvshow = data.tvSeries.find((series) => series.startYear === year);
|
||||
}
|
||||
|
||||
if (!tvshow) {
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import Axios, { AxiosInstance } from 'axios';
|
||||
import cacheManager from '../lib/cache';
|
||||
import { SonarrSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import ExternalAPI from './externalapi';
|
||||
|
||||
interface SonarrSeason {
|
||||
seasonNumber: number;
|
||||
monitored: boolean;
|
||||
statistics?: {
|
||||
previousAiring?: string;
|
||||
episodeFileCount: number;
|
||||
episodeCount: number;
|
||||
totalEpisodeCount: number;
|
||||
sizeOnDisk: number;
|
||||
percentOfEpisodes: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SonarrSeries {
|
||||
@@ -55,6 +65,33 @@ export interface SonarrSeries {
|
||||
};
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
seriesId: number;
|
||||
episodeId: number;
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: string;
|
||||
trackedDownloadState: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
indexer: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface QueueResponse {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey: string;
|
||||
sortDirection: string;
|
||||
totalRecords: number;
|
||||
records: QueueItem[];
|
||||
}
|
||||
|
||||
interface SonarrProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -75,6 +112,7 @@ interface AddSeriesOptions {
|
||||
tvdbid: number;
|
||||
title: string;
|
||||
profileId: number;
|
||||
languageProfileId?: number;
|
||||
seasons: number[];
|
||||
seasonFolder: boolean;
|
||||
rootFolderPath: string;
|
||||
@@ -83,15 +121,60 @@ interface AddSeriesOptions {
|
||||
searchNow?: boolean;
|
||||
}
|
||||
|
||||
class SonarrAPI {
|
||||
private axios: AxiosInstance;
|
||||
export interface LanguageProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
class SonarrAPI extends ExternalAPI {
|
||||
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
|
||||
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
sonarrSettings.hostname
|
||||
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
|
||||
}
|
||||
|
||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||
this.axios = Axios.create({
|
||||
baseURL: url,
|
||||
params: {
|
||||
super(
|
||||
url,
|
||||
{
|
||||
apikey: apiKey,
|
||||
},
|
||||
});
|
||||
{
|
||||
nodeCache: cacheManager.getCache('sonarr').data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async getSeries(): Promise<SonarrSeries[]> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrSeries[]>('/series');
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve series: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
||||
params: {
|
||||
term: title,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.data[0]) {
|
||||
throw new Error('No series found');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
logger.error('Error retrieving series by series title', {
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
});
|
||||
throw new Error('No series found');
|
||||
}
|
||||
}
|
||||
|
||||
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
|
||||
@@ -116,7 +199,7 @@ class SonarrAPI {
|
||||
}
|
||||
}
|
||||
|
||||
public async addSeries(options: AddSeriesOptions): Promise<boolean> {
|
||||
public async addSeries(options: AddSeriesOptions): Promise<SonarrSeries> {
|
||||
try {
|
||||
const series = await this.getSeriesByTvdbId(options.tvdbid);
|
||||
|
||||
@@ -124,11 +207,6 @@ class SonarrAPI {
|
||||
if (series.id) {
|
||||
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
||||
|
||||
series.addOptions = {
|
||||
ignoreEpisodesWithFiles: true,
|
||||
searchForMissingEpisodes: options.searchNow,
|
||||
};
|
||||
|
||||
const newSeriesResponse = await this.axios.put<SonarrSeries>(
|
||||
'/series',
|
||||
series
|
||||
@@ -138,19 +216,22 @@ class SonarrAPI {
|
||||
logger.info('Sonarr accepted request. Updated existing series', {
|
||||
label: 'Sonarr',
|
||||
});
|
||||
logger.debug('Sonarr add details', {
|
||||
logger.debug('Sonarr update details', {
|
||||
label: 'Sonarr',
|
||||
movie: newSeriesResponse.data,
|
||||
});
|
||||
if (options.searchNow) {
|
||||
this.searchSeries(newSeriesResponse.data.id);
|
||||
}
|
||||
} else {
|
||||
logger.error('Failed to add movie to Sonarr', {
|
||||
logger.error('Failed to update series in Sonarr', {
|
||||
label: 'Sonarr',
|
||||
options,
|
||||
});
|
||||
return false;
|
||||
throw new Error('Failed to update series in Sonarr');
|
||||
}
|
||||
|
||||
return true;
|
||||
return newSeriesResponse.data;
|
||||
}
|
||||
|
||||
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
|
||||
@@ -158,7 +239,8 @@ class SonarrAPI {
|
||||
{
|
||||
tvdbId: options.tvdbid,
|
||||
title: options.title,
|
||||
profileId: options.profileId,
|
||||
qualityProfileId: options.profileId,
|
||||
languageProfileId: options.languageProfileId,
|
||||
seasons: this.buildSeasonList(
|
||||
options.seasons,
|
||||
series.seasons.map((season) => ({
|
||||
@@ -189,28 +271,32 @@ class SonarrAPI {
|
||||
label: 'Sonarr',
|
||||
options,
|
||||
});
|
||||
return false;
|
||||
throw new Error('Failed to add series to Sonarr');
|
||||
}
|
||||
|
||||
return true;
|
||||
return createdSeriesResponse.data;
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong adding a series to Sonarr', {
|
||||
logger.error('Something went wrong while adding a series to Sonarr.', {
|
||||
label: 'Sonarr API',
|
||||
errorMessage: e.message,
|
||||
error: e,
|
||||
response: e?.response?.data,
|
||||
});
|
||||
return false;
|
||||
throw new Error('Failed to add series');
|
||||
}
|
||||
}
|
||||
|
||||
public async getProfiles(): Promise<SonarrProfile[]> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrProfile[]>('/profile');
|
||||
const data = await this.getRolling<SonarrProfile[]>(
|
||||
'/qualityProfile',
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong retrieving Sonarr profiles', {
|
||||
logger.error('Something went wrong while retrieving Sonarr profiles.', {
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
});
|
||||
@@ -220,15 +306,72 @@ class SonarrAPI {
|
||||
|
||||
public async getRootFolders(): Promise<SonarrRootFolder[]> {
|
||||
try {
|
||||
const response = await this.axios.get<SonarrRootFolder[]>('/rootfolder');
|
||||
const data = await this.getRolling<SonarrRootFolder[]>(
|
||||
'/rootfolder',
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return response.data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong retrieving Sonarr root folders', {
|
||||
logger.error(
|
||||
'Something went wrong while retrieving Sonarr root folders.',
|
||||
{
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
}
|
||||
);
|
||||
|
||||
throw new Error('Failed to get root folders');
|
||||
}
|
||||
}
|
||||
|
||||
public async getLanguageProfiles(): Promise<LanguageProfile[]> {
|
||||
try {
|
||||
const data = await this.getRolling<LanguageProfile[]>(
|
||||
'/languageprofile',
|
||||
undefined,
|
||||
3600
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'Something went wrong while retrieving Sonarr language profiles.',
|
||||
{
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
}
|
||||
);
|
||||
|
||||
throw new Error('Failed to get language profiles');
|
||||
}
|
||||
}
|
||||
|
||||
public async searchSeries(seriesId: number): Promise<void> {
|
||||
logger.info('Executing series search command', {
|
||||
label: 'Sonarr API',
|
||||
seriesId,
|
||||
});
|
||||
await this.runCommand('SeriesSearch', { seriesId });
|
||||
}
|
||||
|
||||
private async runCommand(
|
||||
commandName: string,
|
||||
options: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.axios.post(`/command`, {
|
||||
name: commandName,
|
||||
...options,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong attempting to run a Sonarr command.', {
|
||||
label: 'Sonarr API',
|
||||
message: e.message,
|
||||
});
|
||||
throw new Error('Failed to get root folders');
|
||||
|
||||
throw new Error('Failed to run Sonarr command.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +399,16 @@ class SonarrAPI {
|
||||
|
||||
return newSeasons;
|
||||
}
|
||||
|
||||
public getQueue = async (): Promise<QueueItem[]> => {
|
||||
try {
|
||||
const response = await this.axios.get<QueueResponse>(`/queue`);
|
||||
|
||||
return response.data.records;
|
||||
} catch (e) {
|
||||
throw new Error(`[Radarr] Failed to retrieve queue: ${e.message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default SonarrAPI;
|
||||
|
||||
@@ -1,934 +0,0 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
export const ANIME_KEYWORD_ID = 210024;
|
||||
|
||||
interface SearchOptions {
|
||||
query: string;
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
interface DiscoverMovieOptions {
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
sortBy?:
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
| 'release_date.asc'
|
||||
| 'release_date.desc'
|
||||
| 'revenue.asc'
|
||||
| 'revenue.desc'
|
||||
| 'primary_release_date.asc'
|
||||
| 'primary_release_date.desc'
|
||||
| 'original_title.asc'
|
||||
| 'original_title.desc'
|
||||
| 'vote_average.asc'
|
||||
| 'vote_average.desc'
|
||||
| 'vote_count.asc'
|
||||
| 'vote_count.desc';
|
||||
}
|
||||
|
||||
interface DiscoverTvOptions {
|
||||
page?: number;
|
||||
language?: string;
|
||||
sortBy?:
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
| 'vote_average.asc'
|
||||
| 'vote_average.desc'
|
||||
| 'vote_count.asc'
|
||||
| 'vote_count.desc'
|
||||
| 'first_air_date.asc'
|
||||
| 'first_air_date.desc';
|
||||
}
|
||||
|
||||
interface TmdbMediaResult {
|
||||
id: number;
|
||||
media_type: string;
|
||||
popularity: number;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
vote_count: number;
|
||||
vote_average: number;
|
||||
genre_ids: number[];
|
||||
overview: string;
|
||||
original_language: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieResult extends TmdbMediaResult {
|
||||
media_type: 'movie';
|
||||
title: string;
|
||||
original_title: string;
|
||||
release_date: string;
|
||||
adult: boolean;
|
||||
video: boolean;
|
||||
}
|
||||
|
||||
export interface TmdbTvResult extends TmdbMediaResult {
|
||||
media_type: 'tv';
|
||||
name: string;
|
||||
original_name: string;
|
||||
origin_country: string[];
|
||||
first_air_date: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonResult {
|
||||
id: number;
|
||||
name: string;
|
||||
popularity: number;
|
||||
profile_path?: string;
|
||||
adult: boolean;
|
||||
media_type: 'person';
|
||||
known_for: (TmdbMovieResult | TmdbTvResult)[];
|
||||
}
|
||||
|
||||
interface TmdbPaginatedResponse {
|
||||
page: number;
|
||||
total_results: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
|
||||
}
|
||||
|
||||
interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
interface TmdbUpcomingMoviesResponse extends TmdbPaginatedResponse {
|
||||
dates: {
|
||||
maximum: string;
|
||||
minimum: string;
|
||||
};
|
||||
results: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
interface TmdbExternalIdResponse {
|
||||
movie_results: TmdbMovieResult[];
|
||||
tv_results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
export interface TmdbCreditCast {
|
||||
cast_id: number;
|
||||
character: string;
|
||||
credit_id: string;
|
||||
gender?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
order: number;
|
||||
profile_path?: string;
|
||||
}
|
||||
|
||||
export interface TmdbCreditCrew {
|
||||
credit_id: string;
|
||||
gender?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
profile_path?: string;
|
||||
job: string;
|
||||
department: string;
|
||||
}
|
||||
|
||||
export interface TmdbExternalIds {
|
||||
imdb_id?: string;
|
||||
freebase_mid?: string;
|
||||
freebase_id?: string;
|
||||
tvdb_id?: number;
|
||||
tvrage_id?: string;
|
||||
facebook_id?: string;
|
||||
instagram_id?: string;
|
||||
twitter_id?: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieDetails {
|
||||
id: number;
|
||||
imdb_id?: string;
|
||||
adult: boolean;
|
||||
backdrop_path?: string;
|
||||
poster_path?: string;
|
||||
budget: number;
|
||||
genres: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
homepage?: string;
|
||||
original_language: string;
|
||||
original_title: string;
|
||||
overview?: string;
|
||||
popularity: number;
|
||||
production_companies: {
|
||||
id: number;
|
||||
name: string;
|
||||
logo_path?: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
production_countries: {
|
||||
iso_3166_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
release_date: string;
|
||||
revenue: number;
|
||||
runtime?: number;
|
||||
spoken_languages: {
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
status: string;
|
||||
tagline?: string;
|
||||
title: string;
|
||||
video: boolean;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
credits: {
|
||||
cast: TmdbCreditCast[];
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
belongs_to_collection?: {
|
||||
id: number;
|
||||
name: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
videos: TmdbVideoResult;
|
||||
}
|
||||
|
||||
export interface TmdbVideo {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
site: 'YouTube';
|
||||
size: number;
|
||||
type:
|
||||
| 'Clip'
|
||||
| 'Teaser'
|
||||
| 'Trailer'
|
||||
| 'Featurette'
|
||||
| 'Opening Credits'
|
||||
| 'Behind the Scenes'
|
||||
| 'Bloopers';
|
||||
}
|
||||
|
||||
export interface TmdbTvEpisodeResult {
|
||||
id: number;
|
||||
air_date: string;
|
||||
episode_number: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
production_code: string;
|
||||
season_number: number;
|
||||
show_id: number;
|
||||
still_path: string;
|
||||
vote_average: number;
|
||||
vote_cuont: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvSeasonResult {
|
||||
id: number;
|
||||
air_date: string;
|
||||
episode_count: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
poster_path?: string;
|
||||
season_number: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvDetails {
|
||||
id: number;
|
||||
backdrop_path?: string;
|
||||
created_by: {
|
||||
id: number;
|
||||
credit_id: string;
|
||||
name: string;
|
||||
gender: number;
|
||||
profile_path?: string;
|
||||
}[];
|
||||
episode_run_time: number[];
|
||||
first_air_date: string;
|
||||
genres: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
homepage: string;
|
||||
in_production: boolean;
|
||||
languages: string[];
|
||||
last_air_date: string;
|
||||
last_episode_to_air?: TmdbTvEpisodeResult;
|
||||
name: string;
|
||||
next_episode_to_air?: TmdbTvEpisodeResult;
|
||||
networks: {
|
||||
id: number;
|
||||
name: string;
|
||||
logo_path: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
number_of_episodes: number;
|
||||
number_of_seasons: number;
|
||||
origin_country: string[];
|
||||
original_language: string;
|
||||
original_name: string;
|
||||
overview: string;
|
||||
popularity: number;
|
||||
poster_path?: string;
|
||||
production_companies: {
|
||||
id: number;
|
||||
logo_path?: string;
|
||||
name: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
spoken_languages: {
|
||||
english_name: string;
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
seasons: TmdbTvSeasonResult[];
|
||||
status: string;
|
||||
type: string;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
credits: {
|
||||
cast: TmdbCreditCast[];
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
keywords: {
|
||||
results: TmdbKeyword[];
|
||||
};
|
||||
videos: TmdbVideoResult;
|
||||
}
|
||||
|
||||
export interface TmdbVideoResult {
|
||||
results: TmdbVideo[];
|
||||
}
|
||||
|
||||
export interface TmdbKeyword {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
deathday: string;
|
||||
known_for_department: string;
|
||||
also_known_as?: string[];
|
||||
gender: number;
|
||||
biography: string;
|
||||
popularity: string;
|
||||
place_of_birth?: string;
|
||||
profile_path?: string;
|
||||
adult: boolean;
|
||||
imdb_id?: string;
|
||||
homepage?: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCredit {
|
||||
id: number;
|
||||
original_language: string;
|
||||
episode_count: number;
|
||||
overview: string;
|
||||
origin_country: string[];
|
||||
original_name: string;
|
||||
vote_count: number;
|
||||
name: string;
|
||||
media_type?: string;
|
||||
popularity: number;
|
||||
credit_id: string;
|
||||
backdrop_path?: string;
|
||||
first_air_date: string;
|
||||
vote_average: number;
|
||||
genre_ids?: number[];
|
||||
poster_path?: string;
|
||||
original_title: string;
|
||||
video?: boolean;
|
||||
title: string;
|
||||
adult: boolean;
|
||||
release_date: string;
|
||||
}
|
||||
export interface TmdbPersonCreditCast extends TmdbPersonCredit {
|
||||
character: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCreditCrew extends TmdbPersonCredit {
|
||||
department: string;
|
||||
job: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCombinedCredits {
|
||||
id: number;
|
||||
cast: TmdbPersonCreditCast[];
|
||||
crew: TmdbPersonCreditCrew[];
|
||||
}
|
||||
|
||||
export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
|
||||
episodes: TmdbTvEpisodeResult[];
|
||||
external_ids: TmdbExternalIds;
|
||||
}
|
||||
|
||||
export interface TmdbCollection {
|
||||
id: number;
|
||||
name: string;
|
||||
overview?: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
parts: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
class TheMovieDb {
|
||||
private apiKey = 'db55323b8d3e4154498498a75642b381';
|
||||
private axios: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.axios = axios.create({
|
||||
baseURL: 'https://api.themoviedb.org/3',
|
||||
params: {
|
||||
api_key: this.apiKey,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public searchMulti = async ({
|
||||
query,
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en-US',
|
||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get('/search/multi', {
|
||||
params: { query, page, include_adult: includeAdult, language },
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
return {
|
||||
page: 1,
|
||||
results: [],
|
||||
total_pages: 1,
|
||||
total_results: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public getPerson = async ({
|
||||
personId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
personId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbPersonDetail> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbPersonDetail>(
|
||||
`/person/${personId}`,
|
||||
{
|
||||
params: { language },
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getPersonCombinedCredits = async ({
|
||||
personId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
personId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbPersonCombinedCredits> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbPersonCombinedCredits>(
|
||||
`/person/${personId}/combined_credits`,
|
||||
{
|
||||
params: { language },
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to fetch person combined credits: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public getMovie = async ({
|
||||
movieId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
movieId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbMovieDetails> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbMovieDetails>(
|
||||
`/movie/${movieId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'credits,external_ids,videos',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvShow = async ({
|
||||
tvId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
tvId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbTvDetails>(`/tv/${tvId}`, {
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'credits,external_ids,keywords,videos',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvSeason = async ({
|
||||
tvId,
|
||||
seasonNumber,
|
||||
language,
|
||||
}: {
|
||||
tvId: number;
|
||||
seasonNumber: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSeasonWithEpisodes> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSeasonWithEpisodes>(
|
||||
`/tv/${tvId}/season/${seasonNumber}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'external_ids',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch tv show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getMovieRecommendations({
|
||||
movieId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
movieId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/recommendations`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieSimilar({
|
||||
movieId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
movieId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/similar`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
tvId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchTvResponse>(
|
||||
`/tv/${tvId}/recommendations`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to fetch tv recommendations: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTvSimilar({
|
||||
tvId,
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
tvId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchTvResponse>(
|
||||
`/tv/${tvId}/similar`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch tv similar: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getDiscoverMovies = async ({
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en-US',
|
||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
'/discover/movie',
|
||||
{
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getDiscoverTv = async ({
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchTvResponse>(
|
||||
'/discover/tv',
|
||||
{
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch discover tv: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getUpcomingMovies = async ({
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
page: number;
|
||||
language: string;
|
||||
}): Promise<TmdbUpcomingMoviesResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbUpcomingMoviesResponse>(
|
||||
'/movie/upcoming',
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getAllTrending = async ({
|
||||
page = 1,
|
||||
timeWindow = 'day',
|
||||
language = 'en-US',
|
||||
}: {
|
||||
page?: number;
|
||||
timeWindow?: 'day' | 'week';
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMultiResponse>(
|
||||
`/trending/all/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getMovieTrending = async ({
|
||||
page = 1,
|
||||
timeWindow = 'day',
|
||||
}: {
|
||||
page?: number;
|
||||
timeWindow?: 'day' | 'week';
|
||||
} = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchMovieResponse>(
|
||||
`/trending/movie/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvTrending = async ({
|
||||
page = 1,
|
||||
timeWindow = 'day',
|
||||
}: {
|
||||
page?: number;
|
||||
timeWindow?: 'day' | 'week';
|
||||
} = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbSearchTvResponse>(
|
||||
`/trending/tv/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getByExternalId({
|
||||
externalId,
|
||||
type,
|
||||
language = 'en-US',
|
||||
}:
|
||||
| {
|
||||
externalId: string;
|
||||
type: 'imdb';
|
||||
language?: string;
|
||||
}
|
||||
| {
|
||||
externalId: number;
|
||||
type: 'tvdb';
|
||||
language?: string;
|
||||
}): Promise<TmdbExternalIdResponse> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbExternalIdResponse>(
|
||||
`/find/${externalId}`,
|
||||
{
|
||||
params: {
|
||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieByImdbId({
|
||||
imdbId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
imdbId: string;
|
||||
language?: string;
|
||||
}): Promise<TmdbMovieDetails> {
|
||||
try {
|
||||
const extResponse = await this.getByExternalId({
|
||||
externalId: imdbId,
|
||||
type: 'imdb',
|
||||
});
|
||||
|
||||
if (extResponse.movie_results[0]) {
|
||||
const movie = await this.getMovie({
|
||||
movieId: extResponse.movie_results[0].id,
|
||||
language,
|
||||
});
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'[TMDB] Failed to find a title with the provided IMDB id'
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to get movie by external imdb ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getShowByTvdbId({
|
||||
tvdbId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
tvdbId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> {
|
||||
try {
|
||||
const extResponse = await this.getByExternalId({
|
||||
externalId: tvdbId,
|
||||
type: 'tvdb',
|
||||
});
|
||||
|
||||
if (extResponse.tv_results[0]) {
|
||||
const tvshow = await this.getTvShow({
|
||||
tvId: extResponse.tv_results[0].id,
|
||||
language,
|
||||
});
|
||||
|
||||
return tvshow;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`[TMDB] Failed to find a tv show with the provided TVDB id: ${tvdbId}`
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDB] Failed to get tv show by external tvdb ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getCollection({
|
||||
collectionId,
|
||||
language = 'en-US',
|
||||
}: {
|
||||
collectionId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbCollection> {
|
||||
try {
|
||||
const response = await this.axios.get<TmdbCollection>(
|
||||
`/collection/${collectionId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TheMovieDb;
|
||||
1
server/api/themoviedb/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ANIME_KEYWORD_ID = 210024;
|
||||
751
server/api/themoviedb/index.ts
Normal file
@@ -0,0 +1,751 @@
|
||||
import { sortBy } from 'lodash';
|
||||
import cacheManager from '../../lib/cache';
|
||||
import ExternalAPI from '../externalapi';
|
||||
import {
|
||||
TmdbCollection,
|
||||
TmdbExternalIdResponse,
|
||||
TmdbGenre,
|
||||
TmdbGenresResult,
|
||||
TmdbLanguage,
|
||||
TmdbMovieDetails,
|
||||
TmdbNetwork,
|
||||
TmdbPersonCombinedCredits,
|
||||
TmdbPersonDetail,
|
||||
TmdbProductionCompany,
|
||||
TmdbRegion,
|
||||
TmdbSearchMovieResponse,
|
||||
TmdbSearchMultiResponse,
|
||||
TmdbSearchTvResponse,
|
||||
TmdbSeasonWithEpisodes,
|
||||
TmdbTvDetails,
|
||||
TmdbUpcomingMoviesResponse,
|
||||
} from './interfaces';
|
||||
|
||||
interface SearchOptions {
|
||||
query: string;
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
interface DiscoverMovieOptions {
|
||||
page?: number;
|
||||
includeAdult?: boolean;
|
||||
language?: string;
|
||||
primaryReleaseDateGte?: string;
|
||||
primaryReleaseDateLte?: string;
|
||||
originalLanguage?: string;
|
||||
genre?: number;
|
||||
studio?: number;
|
||||
sortBy?:
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
| 'release_date.asc'
|
||||
| 'release_date.desc'
|
||||
| 'revenue.asc'
|
||||
| 'revenue.desc'
|
||||
| 'primary_release_date.asc'
|
||||
| 'primary_release_date.desc'
|
||||
| 'original_title.asc'
|
||||
| 'original_title.desc'
|
||||
| 'vote_average.asc'
|
||||
| 'vote_average.desc'
|
||||
| 'vote_count.asc'
|
||||
| 'vote_count.desc';
|
||||
}
|
||||
|
||||
interface DiscoverTvOptions {
|
||||
page?: number;
|
||||
language?: string;
|
||||
firstAirDateGte?: string;
|
||||
firstAirDateLte?: string;
|
||||
includeEmptyReleaseDate?: boolean;
|
||||
originalLanguage?: string;
|
||||
genre?: number;
|
||||
network?: number;
|
||||
sortBy?:
|
||||
| 'popularity.asc'
|
||||
| 'popularity.desc'
|
||||
| 'vote_average.asc'
|
||||
| 'vote_average.desc'
|
||||
| 'vote_count.asc'
|
||||
| 'vote_count.desc'
|
||||
| 'first_air_date.asc'
|
||||
| 'first_air_date.desc';
|
||||
}
|
||||
|
||||
class TheMovieDb extends ExternalAPI {
|
||||
private region?: string;
|
||||
private originalLanguage?: string;
|
||||
constructor({
|
||||
region,
|
||||
originalLanguage,
|
||||
}: { region?: string; originalLanguage?: string } = {}) {
|
||||
super(
|
||||
'https://api.themoviedb.org/3',
|
||||
{
|
||||
api_key: 'db55323b8d3e4154498498a75642b381',
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tmdb').data,
|
||||
}
|
||||
);
|
||||
this.region = region;
|
||||
this.originalLanguage = originalLanguage;
|
||||
}
|
||||
|
||||
public searchMulti = async ({
|
||||
query,
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en',
|
||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
||||
params: { query, page, include_adult: includeAdult, language },
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
return {
|
||||
page: 1,
|
||||
results: [],
|
||||
total_pages: 1,
|
||||
total_results: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public getPerson = async ({
|
||||
personId,
|
||||
language = 'en',
|
||||
}: {
|
||||
personId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbPersonDetail> => {
|
||||
try {
|
||||
const data = await this.get<TmdbPersonDetail>(`/person/${personId}`, {
|
||||
params: { language },
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch person details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getPersonCombinedCredits = async ({
|
||||
personId,
|
||||
language = 'en',
|
||||
}: {
|
||||
personId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbPersonCombinedCredits> => {
|
||||
try {
|
||||
const data = await this.get<TmdbPersonCombinedCredits>(
|
||||
`/person/${personId}/combined_credits`,
|
||||
{
|
||||
params: { language },
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDb] Failed to fetch person combined credits: ${e.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public getMovie = async ({
|
||||
movieId,
|
||||
language = 'en',
|
||||
}: {
|
||||
movieId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbMovieDetails> => {
|
||||
try {
|
||||
const data = await this.get<TmdbMovieDetails>(
|
||||
`/movie/${movieId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'credits,external_ids,videos,release_dates',
|
||||
},
|
||||
},
|
||||
43200
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch movie details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvShow = async ({
|
||||
tvId,
|
||||
language = 'en',
|
||||
}: {
|
||||
tvId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> => {
|
||||
try {
|
||||
const data = await this.get<TmdbTvDetails>(
|
||||
`/tv/${tvId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response:
|
||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings',
|
||||
},
|
||||
},
|
||||
43200
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvSeason = async ({
|
||||
tvId,
|
||||
seasonNumber,
|
||||
language,
|
||||
}: {
|
||||
tvId: number;
|
||||
seasonNumber: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSeasonWithEpisodes> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSeasonWithEpisodes>(
|
||||
`/tv/${tvId}/season/${seasonNumber}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
append_to_response: 'external_ids',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getMovieRecommendations({
|
||||
movieId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
}: {
|
||||
movieId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/recommendations`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieSimilar({
|
||||
movieId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
}: {
|
||||
movieId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/movie/${movieId}/similar`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMoviesByKeyword({
|
||||
keywordId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
}: {
|
||||
keywordId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchMovieResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/keyword/${keywordId}/movies`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch movies by keyword: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTvRecommendations({
|
||||
tvId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
}: {
|
||||
tvId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>(
|
||||
`/tv/${tvId}/recommendations`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDb] Failed to fetch TV recommendations: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTvSimilar({
|
||||
tvId,
|
||||
page = 1,
|
||||
language = 'en',
|
||||
}: {
|
||||
tvId: number;
|
||||
page?: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbSearchTvResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch TV similar: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public getDiscoverMovies = async ({
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
includeAdult = false,
|
||||
language = 'en',
|
||||
primaryReleaseDateGte,
|
||||
primaryReleaseDateLte,
|
||||
originalLanguage,
|
||||
genre,
|
||||
studio,
|
||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
include_adult: includeAdult,
|
||||
language,
|
||||
region: this.region,
|
||||
with_original_language: originalLanguage ?? this.originalLanguage,
|
||||
'primary_release_date.gte': primaryReleaseDateGte,
|
||||
'primary_release_date.lte': primaryReleaseDateLte,
|
||||
with_genres: genre,
|
||||
with_companies: studio,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getDiscoverTv = async ({
|
||||
sortBy = 'popularity.desc',
|
||||
page = 1,
|
||||
language = 'en-US',
|
||||
firstAirDateGte,
|
||||
firstAirDateLte,
|
||||
includeEmptyReleaseDate = false,
|
||||
originalLanguage,
|
||||
genre,
|
||||
network,
|
||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||
params: {
|
||||
sort_by: sortBy,
|
||||
page,
|
||||
language,
|
||||
region: this.region,
|
||||
'first_air_date.gte': firstAirDateGte,
|
||||
'first_air_date.lte': firstAirDateLte,
|
||||
with_original_language: originalLanguage ?? this.originalLanguage,
|
||||
include_null_first_air_dates: includeEmptyReleaseDate,
|
||||
with_genres: genre,
|
||||
with_networks: network,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch discover TV: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getUpcomingMovies = async ({
|
||||
page = 1,
|
||||
language = 'en',
|
||||
}: {
|
||||
page: number;
|
||||
language: string;
|
||||
}): Promise<TmdbUpcomingMoviesResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbUpcomingMoviesResponse>(
|
||||
'/movie/upcoming',
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
region: this.region,
|
||||
originalLanguage: this.originalLanguage,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch upcoming movies: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getAllTrending = async ({
|
||||
page = 1,
|
||||
timeWindow = 'day',
|
||||
language = 'en',
|
||||
}: {
|
||||
page?: number;
|
||||
timeWindow?: 'day' | 'week';
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbSearchMultiResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMultiResponse>(
|
||||
`/trending/all/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
language,
|
||||
region: this.region,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getMovieTrending = async ({
|
||||
page = 1,
|
||||
timeWindow = 'day',
|
||||
}: {
|
||||
page?: number;
|
||||
timeWindow?: 'day' | 'week';
|
||||
} = {}): Promise<TmdbSearchMovieResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchMovieResponse>(
|
||||
`/trending/movie/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public getTvTrending = async ({
|
||||
page = 1,
|
||||
timeWindow = 'day',
|
||||
}: {
|
||||
page?: number;
|
||||
timeWindow?: 'day' | 'week';
|
||||
} = {}): Promise<TmdbSearchTvResponse> => {
|
||||
try {
|
||||
const data = await this.get<TmdbSearchTvResponse>(
|
||||
`/trending/tv/${timeWindow}`,
|
||||
{
|
||||
params: {
|
||||
page,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public async getByExternalId({
|
||||
externalId,
|
||||
type,
|
||||
language = 'en',
|
||||
}:
|
||||
| {
|
||||
externalId: string;
|
||||
type: 'imdb';
|
||||
language?: string;
|
||||
}
|
||||
| {
|
||||
externalId: number;
|
||||
type: 'tvdb';
|
||||
language?: string;
|
||||
}): Promise<TmdbExternalIdResponse> {
|
||||
try {
|
||||
const data = await this.get<TmdbExternalIdResponse>(
|
||||
`/find/${externalId}`,
|
||||
{
|
||||
params: {
|
||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to find by external ID: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieByImdbId({
|
||||
imdbId,
|
||||
language = 'en',
|
||||
}: {
|
||||
imdbId: string;
|
||||
language?: string;
|
||||
}): Promise<TmdbMovieDetails> {
|
||||
try {
|
||||
const extResponse = await this.getByExternalId({
|
||||
externalId: imdbId,
|
||||
type: 'imdb',
|
||||
});
|
||||
|
||||
if (extResponse.movie_results[0]) {
|
||||
const movie = await this.getMovie({
|
||||
movieId: extResponse.movie_results[0].id,
|
||||
language,
|
||||
});
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'[TMDb] Failed to find a title with the provided IMDB id'
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDb] Failed to get movie by external imdb ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getShowByTvdbId({
|
||||
tvdbId,
|
||||
language = 'en',
|
||||
}: {
|
||||
tvdbId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbTvDetails> {
|
||||
try {
|
||||
const extResponse = await this.getByExternalId({
|
||||
externalId: tvdbId,
|
||||
type: 'tvdb',
|
||||
});
|
||||
|
||||
if (extResponse.tv_results[0]) {
|
||||
const tvshow = await this.getTvShow({
|
||||
tvId: extResponse.tv_results[0].id,
|
||||
language,
|
||||
});
|
||||
|
||||
return tvshow;
|
||||
}
|
||||
|
||||
throw new Error(`No show returned from API for ID ${tvdbId}`);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[TMDb] Failed to get TV show using the external TVDB ID: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async getCollection({
|
||||
collectionId,
|
||||
language = 'en',
|
||||
}: {
|
||||
collectionId: number;
|
||||
language?: string;
|
||||
}): Promise<TmdbCollection> {
|
||||
try {
|
||||
const data = await this.get<TmdbCollection>(
|
||||
`/collection/${collectionId}`,
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch collection: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getRegions(): Promise<TmdbRegion[]> {
|
||||
try {
|
||||
const data = await this.get<TmdbRegion[]>(
|
||||
'/configuration/countries',
|
||||
{},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
const regions = sortBy(data, 'english_name');
|
||||
|
||||
return regions;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch countries: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getLanguages(): Promise<TmdbLanguage[]> {
|
||||
try {
|
||||
const data = await this.get<TmdbLanguage[]>(
|
||||
'/configuration/languages',
|
||||
{},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
const languages = sortBy(data, 'english_name');
|
||||
|
||||
return languages;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch langauges: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getStudio(studioId: number): Promise<TmdbProductionCompany> {
|
||||
try {
|
||||
const data = await this.get<TmdbProductionCompany>(
|
||||
`/company/${studioId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch movie studio: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getNetwork(networkId: number): Promise<TmdbNetwork> {
|
||||
try {
|
||||
const data = await this.get<TmdbNetwork>(`/network/${networkId}`);
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch TV network: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMovieGenres({
|
||||
language = 'en',
|
||||
}: {
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbGenre[]> {
|
||||
try {
|
||||
const data = await this.get<TmdbGenresResult>(
|
||||
'/genre/movie/list',
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
const movieGenres = sortBy(data.genres, 'name');
|
||||
|
||||
return movieGenres;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch movie genres: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async getTvGenres({
|
||||
language = 'en',
|
||||
}: {
|
||||
language?: string;
|
||||
} = {}): Promise<TmdbGenre[]> {
|
||||
try {
|
||||
const data = await this.get<TmdbGenresResult>(
|
||||
'/genre/tv/list',
|
||||
{
|
||||
params: {
|
||||
language,
|
||||
},
|
||||
},
|
||||
86400 // 24 hours
|
||||
);
|
||||
|
||||
const tvGenres = sortBy(data.genres, 'name');
|
||||
|
||||
return tvGenres;
|
||||
} catch (e) {
|
||||
throw new Error(`[TMDb] Failed to fetch TV genres: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TheMovieDb;
|
||||
403
server/api/themoviedb/interfaces.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
interface TmdbMediaResult {
|
||||
id: number;
|
||||
media_type: string;
|
||||
popularity: number;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
vote_count: number;
|
||||
vote_average: number;
|
||||
genre_ids: number[];
|
||||
overview: string;
|
||||
original_language: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieResult extends TmdbMediaResult {
|
||||
media_type: 'movie';
|
||||
title: string;
|
||||
original_title: string;
|
||||
release_date: string;
|
||||
adult: boolean;
|
||||
video: boolean;
|
||||
}
|
||||
|
||||
export interface TmdbTvResult extends TmdbMediaResult {
|
||||
media_type: 'tv';
|
||||
name: string;
|
||||
original_name: string;
|
||||
origin_country: string[];
|
||||
first_air_date: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonResult {
|
||||
id: number;
|
||||
name: string;
|
||||
popularity: number;
|
||||
profile_path?: string;
|
||||
adult: boolean;
|
||||
media_type: 'person';
|
||||
known_for: (TmdbMovieResult | TmdbTvResult)[];
|
||||
}
|
||||
|
||||
interface TmdbPaginatedResponse {
|
||||
page: number;
|
||||
total_results: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
|
||||
}
|
||||
|
||||
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
export interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
|
||||
results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
export interface TmdbUpcomingMoviesResponse extends TmdbPaginatedResponse {
|
||||
dates: {
|
||||
maximum: string;
|
||||
minimum: string;
|
||||
};
|
||||
results: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
export interface TmdbExternalIdResponse {
|
||||
movie_results: TmdbMovieResult[];
|
||||
tv_results: TmdbTvResult[];
|
||||
}
|
||||
|
||||
export interface TmdbCreditCast {
|
||||
cast_id: number;
|
||||
character: string;
|
||||
credit_id: string;
|
||||
gender?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
order: number;
|
||||
profile_path?: string;
|
||||
}
|
||||
|
||||
export interface TmdbAggregateCreditCast extends TmdbCreditCast {
|
||||
roles: {
|
||||
credit_id: string;
|
||||
character: string;
|
||||
episode_count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface TmdbCreditCrew {
|
||||
credit_id: string;
|
||||
gender?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
profile_path?: string;
|
||||
job: string;
|
||||
department: string;
|
||||
}
|
||||
|
||||
export interface TmdbExternalIds {
|
||||
imdb_id?: string;
|
||||
freebase_mid?: string;
|
||||
freebase_id?: string;
|
||||
tvdb_id?: number;
|
||||
tvrage_id?: string;
|
||||
facebook_id?: string;
|
||||
instagram_id?: string;
|
||||
twitter_id?: string;
|
||||
}
|
||||
|
||||
export interface TmdbProductionCompany {
|
||||
id: number;
|
||||
logo_path?: string;
|
||||
name: string;
|
||||
origin_country: string;
|
||||
homepage?: string;
|
||||
headquarters?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieDetails {
|
||||
id: number;
|
||||
imdb_id?: string;
|
||||
adult: boolean;
|
||||
backdrop_path?: string;
|
||||
poster_path?: string;
|
||||
budget: number;
|
||||
genres: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
homepage?: string;
|
||||
original_language: string;
|
||||
original_title: string;
|
||||
overview?: string;
|
||||
popularity: number;
|
||||
production_companies: TmdbProductionCompany[];
|
||||
production_countries: {
|
||||
iso_3166_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
release_date: string;
|
||||
release_dates: TmdbMovieReleaseResult;
|
||||
revenue: number;
|
||||
runtime?: number;
|
||||
spoken_languages: {
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
status: string;
|
||||
tagline?: string;
|
||||
title: string;
|
||||
video: boolean;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
credits: {
|
||||
cast: TmdbCreditCast[];
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
belongs_to_collection?: {
|
||||
id: number;
|
||||
name: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
videos: TmdbVideoResult;
|
||||
}
|
||||
|
||||
export interface TmdbVideo {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
site: 'YouTube';
|
||||
size: number;
|
||||
type:
|
||||
| 'Clip'
|
||||
| 'Teaser'
|
||||
| 'Trailer'
|
||||
| 'Featurette'
|
||||
| 'Opening Credits'
|
||||
| 'Behind the Scenes'
|
||||
| 'Bloopers';
|
||||
}
|
||||
|
||||
export interface TmdbTvEpisodeResult {
|
||||
id: number;
|
||||
air_date: string;
|
||||
episode_number: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
production_code: string;
|
||||
season_number: number;
|
||||
show_id: number;
|
||||
still_path: string;
|
||||
vote_average: number;
|
||||
vote_cuont: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvSeasonResult {
|
||||
id: number;
|
||||
air_date: string;
|
||||
episode_count: number;
|
||||
name: string;
|
||||
overview: string;
|
||||
poster_path?: string;
|
||||
season_number: number;
|
||||
}
|
||||
|
||||
export interface TmdbTvDetails {
|
||||
id: number;
|
||||
backdrop_path?: string;
|
||||
content_ratings: TmdbTvRatingResult;
|
||||
created_by: {
|
||||
id: number;
|
||||
credit_id: string;
|
||||
name: string;
|
||||
gender: number;
|
||||
profile_path?: string;
|
||||
}[];
|
||||
episode_run_time: number[];
|
||||
first_air_date: string;
|
||||
genres: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
homepage: string;
|
||||
in_production: boolean;
|
||||
languages: string[];
|
||||
last_air_date: string;
|
||||
last_episode_to_air?: TmdbTvEpisodeResult;
|
||||
name: string;
|
||||
next_episode_to_air?: TmdbTvEpisodeResult;
|
||||
networks: TmdbNetwork[];
|
||||
number_of_episodes: number;
|
||||
number_of_seasons: number;
|
||||
origin_country: string[];
|
||||
original_language: string;
|
||||
original_name: string;
|
||||
overview: string;
|
||||
popularity: number;
|
||||
poster_path?: string;
|
||||
production_companies: {
|
||||
id: number;
|
||||
logo_path?: string;
|
||||
name: string;
|
||||
origin_country: string;
|
||||
}[];
|
||||
spoken_languages: {
|
||||
english_name: string;
|
||||
iso_639_1: string;
|
||||
name: string;
|
||||
}[];
|
||||
seasons: TmdbTvSeasonResult[];
|
||||
status: string;
|
||||
tagline?: string;
|
||||
type: string;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
aggregate_credits: {
|
||||
cast: TmdbAggregateCreditCast[];
|
||||
};
|
||||
credits: {
|
||||
crew: TmdbCreditCrew[];
|
||||
};
|
||||
external_ids: TmdbExternalIds;
|
||||
keywords: {
|
||||
results: TmdbKeyword[];
|
||||
};
|
||||
videos: TmdbVideoResult;
|
||||
}
|
||||
|
||||
export interface TmdbVideoResult {
|
||||
results: TmdbVideo[];
|
||||
}
|
||||
|
||||
export interface TmdbTvRatingResult {
|
||||
results: TmdbRating[];
|
||||
}
|
||||
|
||||
export interface TmdbRating {
|
||||
iso_3166_1: string;
|
||||
rating: string;
|
||||
}
|
||||
|
||||
export interface TmdbMovieReleaseResult {
|
||||
results: TmdbRelease[];
|
||||
}
|
||||
|
||||
export interface TmdbRelease extends TmdbRating {
|
||||
release_dates: {
|
||||
certification: string;
|
||||
iso_639_1?: string;
|
||||
note?: string;
|
||||
release_date: string;
|
||||
type: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface TmdbKeyword {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
birthday: string;
|
||||
deathday: string;
|
||||
known_for_department: string;
|
||||
also_known_as?: string[];
|
||||
gender: number;
|
||||
biography: string;
|
||||
popularity: string;
|
||||
place_of_birth?: string;
|
||||
profile_path?: string;
|
||||
adult: boolean;
|
||||
imdb_id?: string;
|
||||
homepage?: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCredit {
|
||||
id: number;
|
||||
original_language: string;
|
||||
episode_count: number;
|
||||
overview: string;
|
||||
origin_country: string[];
|
||||
original_name: string;
|
||||
vote_count: number;
|
||||
name: string;
|
||||
media_type?: string;
|
||||
popularity: number;
|
||||
credit_id: string;
|
||||
backdrop_path?: string;
|
||||
first_air_date: string;
|
||||
vote_average: number;
|
||||
genre_ids?: number[];
|
||||
poster_path?: string;
|
||||
original_title: string;
|
||||
video?: boolean;
|
||||
title: string;
|
||||
adult: boolean;
|
||||
release_date: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCreditCast extends TmdbPersonCredit {
|
||||
character: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCreditCrew extends TmdbPersonCredit {
|
||||
department: string;
|
||||
job: string;
|
||||
}
|
||||
|
||||
export interface TmdbPersonCombinedCredits {
|
||||
id: number;
|
||||
cast: TmdbPersonCreditCast[];
|
||||
crew: TmdbPersonCreditCrew[];
|
||||
}
|
||||
|
||||
export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
|
||||
episodes: TmdbTvEpisodeResult[];
|
||||
external_ids: TmdbExternalIds;
|
||||
}
|
||||
|
||||
export interface TmdbCollection {
|
||||
id: number;
|
||||
name: string;
|
||||
overview?: string;
|
||||
poster_path?: string;
|
||||
backdrop_path?: string;
|
||||
parts: TmdbMovieResult[];
|
||||
}
|
||||
|
||||
export interface TmdbRegion {
|
||||
iso_3166_1: string;
|
||||
english_name: string;
|
||||
}
|
||||
|
||||
export interface TmdbLanguage {
|
||||
iso_639_1: string;
|
||||
english_name: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbGenresResult {
|
||||
genres: TmdbGenre[];
|
||||
}
|
||||
|
||||
export interface TmdbGenre {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TmdbNetwork {
|
||||
id: number;
|
||||
name: string;
|
||||
headquarters?: string;
|
||||
homepage?: string;
|
||||
logo_path?: string;
|
||||
origin_country?: string;
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export enum MediaRequestStatus {
|
||||
PENDING = 1,
|
||||
APPROVED,
|
||||
DECLINED,
|
||||
AVAILABLE,
|
||||
}
|
||||
|
||||
export enum MediaType {
|
||||
|
||||
@@ -8,11 +8,16 @@ import {
|
||||
UpdateDateColumn,
|
||||
getRepository,
|
||||
In,
|
||||
AfterLoad,
|
||||
} from 'typeorm';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import { MediaStatus, MediaType } from '../constants/media';
|
||||
import logger from '../logger';
|
||||
import Season from './Season';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import RadarrAPI from '../api/radarr';
|
||||
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
|
||||
import SonarrAPI from '../api/sonarr';
|
||||
|
||||
@Entity()
|
||||
class Media {
|
||||
@@ -104,9 +109,170 @@ class Media {
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
public mediaAddedAt: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public serviceId?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public serviceId4k?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public externalServiceId?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public externalServiceId4k?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public externalServiceSlug?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public externalServiceSlug4k?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public ratingKey?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public ratingKey4k?: string;
|
||||
|
||||
public serviceUrl?: string;
|
||||
public serviceUrl4k?: string;
|
||||
public downloadStatus?: DownloadingItem[] = [];
|
||||
public downloadStatus4k?: DownloadingItem[] = [];
|
||||
|
||||
public plexUrl?: string;
|
||||
public plexUrl4k?: string;
|
||||
|
||||
constructor(init?: Partial<Media>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
public setPlexUrls(): void {
|
||||
const machineId = getSettings().plex.machineId;
|
||||
if (this.ratingKey) {
|
||||
this.plexUrl = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey}`;
|
||||
}
|
||||
if (this.ratingKey4k) {
|
||||
this.plexUrl4k = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`;
|
||||
}
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
public setServiceUrl(): void {
|
||||
if (this.mediaType === MediaType.MOVIE) {
|
||||
if (this.serviceId !== null && this.externalServiceSlug !== null) {
|
||||
const settings = getSettings();
|
||||
const server = settings.radarr.find(
|
||||
(radarr) => radarr.id === this.serviceId
|
||||
);
|
||||
|
||||
if (server) {
|
||||
this.serviceUrl = server.externalUrl
|
||||
? `${server.externalUrl}/movie/${this.externalServiceSlug}`
|
||||
: RadarrAPI.buildRadarrUrl(
|
||||
server,
|
||||
`/movie/${this.externalServiceSlug}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.serviceId4k !== null && this.externalServiceSlug4k !== null) {
|
||||
const settings = getSettings();
|
||||
const server = settings.radarr.find(
|
||||
(radarr) => radarr.id === this.serviceId4k
|
||||
);
|
||||
|
||||
if (server) {
|
||||
this.serviceUrl4k = server.externalUrl
|
||||
? `${server.externalUrl}/movie/${this.externalServiceSlug4k}`
|
||||
: RadarrAPI.buildRadarrUrl(
|
||||
server,
|
||||
`/movie/${this.externalServiceSlug4k}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mediaType === MediaType.TV) {
|
||||
if (this.serviceId !== null && this.externalServiceSlug !== null) {
|
||||
const settings = getSettings();
|
||||
const server = settings.sonarr.find(
|
||||
(sonarr) => sonarr.id === this.serviceId
|
||||
);
|
||||
|
||||
if (server) {
|
||||
this.serviceUrl = server.externalUrl
|
||||
? `${server.externalUrl}/series/${this.externalServiceSlug}`
|
||||
: SonarrAPI.buildSonarrUrl(
|
||||
server,
|
||||
`/series/${this.externalServiceSlug}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.serviceId4k !== null && this.externalServiceSlug4k !== null) {
|
||||
const settings = getSettings();
|
||||
const server = settings.sonarr.find(
|
||||
(sonarr) => sonarr.id === this.serviceId4k
|
||||
);
|
||||
|
||||
if (server) {
|
||||
this.serviceUrl4k = server.externalUrl
|
||||
? `${server.externalUrl}/series/${this.externalServiceSlug4k}`
|
||||
: SonarrAPI.buildSonarrUrl(
|
||||
server,
|
||||
`/series/${this.externalServiceSlug4k}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
public getDownloadingItem(): void {
|
||||
if (this.mediaType === MediaType.MOVIE) {
|
||||
if (
|
||||
this.externalServiceId !== undefined &&
|
||||
this.serviceId !== undefined
|
||||
) {
|
||||
this.downloadStatus = downloadTracker.getMovieProgress(
|
||||
this.serviceId,
|
||||
this.externalServiceId
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.externalServiceId4k !== undefined &&
|
||||
this.serviceId4k !== undefined
|
||||
) {
|
||||
this.downloadStatus4k = downloadTracker.getMovieProgress(
|
||||
this.serviceId4k,
|
||||
this.externalServiceId4k
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mediaType === MediaType.TV) {
|
||||
if (
|
||||
this.externalServiceId !== undefined &&
|
||||
this.serviceId !== undefined
|
||||
) {
|
||||
this.downloadStatus = downloadTracker.getSeriesProgress(
|
||||
this.serviceId,
|
||||
this.externalServiceId
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.externalServiceId4k !== undefined &&
|
||||
this.serviceId4k !== undefined
|
||||
) {
|
||||
this.downloadStatus4k = downloadTracker.getSeriesProgress(
|
||||
this.serviceId4k,
|
||||
this.externalServiceId4k
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Media;
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
getRepository,
|
||||
OneToMany,
|
||||
AfterRemove,
|
||||
RelationCount,
|
||||
} from 'typeorm';
|
||||
import { User } from './User';
|
||||
import Media from './Media';
|
||||
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import TheMovieDb, { ANIME_KEYWORD_ID } from '../api/themoviedb';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants';
|
||||
import RadarrAPI from '../api/radarr';
|
||||
import logger from '../logger';
|
||||
import SeasonRequest from './SeasonRequest';
|
||||
@@ -59,6 +61,9 @@ export class MediaRequest {
|
||||
@Column({ type: 'varchar' })
|
||||
public type: MediaType;
|
||||
|
||||
@RelationCount((request: MediaRequest) => request.seasons)
|
||||
public seasonCount: number;
|
||||
|
||||
@OneToMany(() => SeasonRequest, (season) => season.request, {
|
||||
eager: true,
|
||||
cascade: true,
|
||||
@@ -77,6 +82,9 @@ export class MediaRequest {
|
||||
@Column({ nullable: true })
|
||||
public rootFolder: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public languageProfileId: number;
|
||||
|
||||
constructor(init?: Partial<MediaRequest>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
@@ -107,6 +115,7 @@ export class MediaRequest {
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
notifyUser: this.requestedBy,
|
||||
media,
|
||||
request: this,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,6 +135,7 @@ export class MediaRequest {
|
||||
.join(', '),
|
||||
},
|
||||
],
|
||||
request: this,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -138,7 +148,7 @@ export class MediaRequest {
|
||||
* auto approved content
|
||||
*/
|
||||
@AfterUpdate()
|
||||
public async notifyApprovedOrDeclined(): Promise<void> {
|
||||
public async notifyApprovedOrDeclined(autoApproved = false): Promise<void> {
|
||||
if (
|
||||
this.status === MediaRequestStatus.APPROVED ||
|
||||
this.status === MediaRequestStatus.DECLINED
|
||||
@@ -165,21 +175,26 @@ export class MediaRequest {
|
||||
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
|
||||
notificationManager.sendNotification(
|
||||
this.status === MediaRequestStatus.APPROVED
|
||||
? Notification.MEDIA_APPROVED
|
||||
? autoApproved
|
||||
? Notification.MEDIA_AUTO_APPROVED
|
||||
: Notification.MEDIA_APPROVED
|
||||
: Notification.MEDIA_DECLINED,
|
||||
{
|
||||
subject: movie.title,
|
||||
message: movie.overview,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
notifyUser: this.requestedBy,
|
||||
notifyUser: autoApproved ? undefined : this.requestedBy,
|
||||
media,
|
||||
request: this,
|
||||
}
|
||||
);
|
||||
} else if (this.media.mediaType === MediaType.TV) {
|
||||
const tv = await tmdb.getTvShow({ tvId: this.media.tmdbId });
|
||||
notificationManager.sendNotification(
|
||||
this.status === MediaRequestStatus.APPROVED
|
||||
? Notification.MEDIA_APPROVED
|
||||
? autoApproved
|
||||
? Notification.MEDIA_AUTO_APPROVED
|
||||
: Notification.MEDIA_APPROVED
|
||||
: Notification.MEDIA_DECLINED,
|
||||
{
|
||||
subject: tv.name,
|
||||
@@ -195,12 +210,20 @@ export class MediaRequest {
|
||||
.join(', '),
|
||||
},
|
||||
],
|
||||
request: this,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AfterInsert()
|
||||
public async autoapprovalNotification(): Promise<void> {
|
||||
if (this.status === MediaRequestStatus.APPROVED) {
|
||||
this.notifyApprovedOrDeclined(true);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterUpdate()
|
||||
@AfterInsert()
|
||||
public async updateParentStatus(): Promise<void> {
|
||||
@@ -367,9 +390,7 @@ export class MediaRequest {
|
||||
const tmdb = new TheMovieDb();
|
||||
const radarr = new RadarrAPI({
|
||||
apiKey: radarrSettings.apiKey,
|
||||
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
radarrSettings.hostname
|
||||
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}/api`,
|
||||
url: RadarrAPI.buildRadarrUrl(radarrSettings, '/api/v3'),
|
||||
});
|
||||
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
|
||||
|
||||
@@ -399,37 +420,51 @@ export class MediaRequest {
|
||||
tmdbId: movie.id,
|
||||
year: Number(movie.release_date.slice(0, 4)),
|
||||
monitored: true,
|
||||
searchNow: true,
|
||||
searchNow: !radarrSettings.preventSearch,
|
||||
})
|
||||
.then(async (success) => {
|
||||
if (!success) {
|
||||
media.status = MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
logger.warn(
|
||||
'Newly added movie request failed to add to Radarr, marking as unknown',
|
||||
{
|
||||
label: 'Media Request',
|
||||
}
|
||||
);
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
select: ['id', 'plexToken'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
|
||||
subject: movie.title,
|
||||
message: 'Movie failed to add to Radarr',
|
||||
notifyUser: admin,
|
||||
media,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
});
|
||||
.then(async (radarrMovie) => {
|
||||
// We grab media again here to make sure we have the latest version of it
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: this.media.id },
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
throw new Error('Media data is missing');
|
||||
}
|
||||
|
||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
radarrMovie.id;
|
||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
radarrMovie.titleSlug;
|
||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = radarrSettings?.id;
|
||||
await mediaRepository.save(media);
|
||||
})
|
||||
.catch(async () => {
|
||||
media.status = MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
logger.warn(
|
||||
'Newly added movie request failed to add to Radarr, marking as unknown',
|
||||
{
|
||||
label: 'Media Request',
|
||||
}
|
||||
);
|
||||
|
||||
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
|
||||
subject: movie.title,
|
||||
message: movie.overview,
|
||||
media,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||
request: this,
|
||||
});
|
||||
});
|
||||
logger.info('Sent request to Radarr', { label: 'Media Request' });
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[MediaRequest] Request failed to send to radarr: ${e.message}`
|
||||
);
|
||||
const errorMessage = `Request failed to send to radarr: ${e.message}`;
|
||||
logger.error('Request failed to send to Radarr', {
|
||||
label: 'Media Request',
|
||||
errorMessage,
|
||||
});
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,13 +531,15 @@ export class MediaRequest {
|
||||
const tmdb = new TheMovieDb();
|
||||
const sonarr = new SonarrAPI({
|
||||
apiKey: sonarrSettings.apiKey,
|
||||
url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${
|
||||
sonarrSettings.hostname
|
||||
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}/api`,
|
||||
url: SonarrAPI.buildSonarrUrl(sonarrSettings, '/api/v3'),
|
||||
});
|
||||
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
|
||||
const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
|
||||
|
||||
if (!series.external_ids.tvdb_id) {
|
||||
if (!tvdbId) {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
await mediaRepository.remove(media);
|
||||
await requestRepository.remove(this);
|
||||
throw new Error('Series was missing tvdb id');
|
||||
}
|
||||
|
||||
@@ -526,6 +563,11 @@ export class MediaRequest {
|
||||
? sonarrSettings.activeAnimeProfileId
|
||||
: sonarrSettings.activeProfileId;
|
||||
|
||||
let languageProfile =
|
||||
seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
|
||||
? sonarrSettings.activeAnimeLanguageProfileId
|
||||
: sonarrSettings.activeLanguageProfileId;
|
||||
|
||||
if (
|
||||
this.rootFolder &&
|
||||
this.rootFolder !== '' &&
|
||||
@@ -539,60 +581,90 @@ export class MediaRequest {
|
||||
|
||||
if (this.profileId && this.profileId !== qualityProfile) {
|
||||
qualityProfile = this.profileId;
|
||||
logger.info(`Request has an override profile id: ${qualityProfile}`, {
|
||||
logger.info(`Request has an override profile ID: ${qualityProfile}`, {
|
||||
label: 'Media Request',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
this.languageProfileId &&
|
||||
this.languageProfileId !== languageProfile
|
||||
) {
|
||||
languageProfile = this.languageProfileId;
|
||||
logger.info(
|
||||
`Request has an override Language Profile: ${languageProfile}`,
|
||||
{
|
||||
label: 'Media Request',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Run this asynchronously so we don't wait for it on the UI side
|
||||
sonarr
|
||||
.addSeries({
|
||||
profileId: qualityProfile,
|
||||
languageProfileId: languageProfile,
|
||||
rootFolderPath: rootFolder,
|
||||
title: series.name,
|
||||
tvdbid: series.external_ids.tvdb_id,
|
||||
tvdbid: tvdbId,
|
||||
seasons: this.seasons.map((season) => season.seasonNumber),
|
||||
seasonFolder: sonarrSettings.enableSeasonFolders,
|
||||
seriesType,
|
||||
monitored: true,
|
||||
searchNow: true,
|
||||
searchNow: !sonarrSettings.preventSearch,
|
||||
})
|
||||
.then(async (success) => {
|
||||
if (!success) {
|
||||
media.status = MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
logger.warn(
|
||||
'Newly added series request failed to add to Sonarr, marking as unknown',
|
||||
{
|
||||
label: 'Media Request',
|
||||
}
|
||||
);
|
||||
const userRepository = getRepository(User);
|
||||
const admin = await userRepository.findOneOrFail({
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
|
||||
subject: series.name,
|
||||
message: 'Series failed to add to Sonarr',
|
||||
notifyUser: admin,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
|
||||
media,
|
||||
extra: [
|
||||
{
|
||||
name: 'Seasons',
|
||||
value: this.seasons
|
||||
.map((season) => season.seasonNumber)
|
||||
.join(', '),
|
||||
},
|
||||
],
|
||||
});
|
||||
.then(async (sonarrSeries) => {
|
||||
// We grab media again here to make sure we have the latest version of it
|
||||
const media = await mediaRepository.findOne({
|
||||
where: { id: this.media.id },
|
||||
relations: ['requests'],
|
||||
});
|
||||
|
||||
if (!media) {
|
||||
throw new Error('Media data is missing');
|
||||
}
|
||||
|
||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
sonarrSeries.id;
|
||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
sonarrSeries.titleSlug;
|
||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
|
||||
await mediaRepository.save(media);
|
||||
})
|
||||
.catch(async () => {
|
||||
media.status = MediaStatus.UNKNOWN;
|
||||
await mediaRepository.save(media);
|
||||
logger.warn(
|
||||
'Newly added series request failed to add to Sonarr, marking as unknown',
|
||||
{
|
||||
label: 'Media Request',
|
||||
}
|
||||
);
|
||||
|
||||
notificationManager.sendNotification(Notification.MEDIA_FAILED, {
|
||||
subject: series.name,
|
||||
message: series.overview,
|
||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${series.poster_path}`,
|
||||
media,
|
||||
extra: [
|
||||
{
|
||||
name: 'Seasons',
|
||||
value: this.seasons
|
||||
.map((season) => season.seasonNumber)
|
||||
.join(', '),
|
||||
},
|
||||
],
|
||||
request: this,
|
||||
});
|
||||
});
|
||||
logger.info('Sent request to Sonarr', { label: 'Media Request' });
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[MediaRequest] Request failed to send to sonarr: ${e.message}`
|
||||
);
|
||||
const errorMessage = `Request failed to send to sonarr: ${e.message}`;
|
||||
logger.error('Request failed to send to Sonarr', {
|
||||
label: 'Media Request',
|
||||
errorMessage,
|
||||
});
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,48 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
RelationCount,
|
||||
} from 'typeorm';
|
||||
import { Permission, hasPermission } from '../lib/permissions';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import bcrypt from 'bcrypt';
|
||||
import path from 'path';
|
||||
import PreparedEmail from '../lib/email';
|
||||
import logger from '../logger';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import { default as generatePassword } from 'secure-random-password';
|
||||
import {
|
||||
AfterLoad,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
getRepository,
|
||||
MoreThan,
|
||||
Not,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
RelationCount,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { MediaRequestStatus, MediaType } from '../constants/media';
|
||||
import { UserType } from '../constants/user';
|
||||
import { QuotaResponse } from '../interfaces/api/userInterfaces';
|
||||
import PreparedEmail from '../lib/email';
|
||||
import {
|
||||
hasPermission,
|
||||
Permission,
|
||||
PermissionCheckOptions,
|
||||
} from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import SeasonRequest from './SeasonRequest';
|
||||
import { UserSettings } from './UserSettings';
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
public static filterMany(users: User[]): Partial<User>[] {
|
||||
return users.map((u) => u.filter());
|
||||
public static filterMany(
|
||||
users: User[],
|
||||
showFiltered?: boolean
|
||||
): Partial<User>[] {
|
||||
return users.map((u) => u.filter(showFiltered));
|
||||
}
|
||||
|
||||
static readonly filteredFields: string[] = ['plexToken', 'password'];
|
||||
static readonly filteredFields: string[] = ['email'];
|
||||
|
||||
public displayName: string;
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
@@ -31,12 +50,21 @@ export class User {
|
||||
@Column({ unique: true })
|
||||
public email: string;
|
||||
|
||||
@Column()
|
||||
public username: string;
|
||||
@Column({ nullable: true })
|
||||
public plexUsername: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public username?: string;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
public password?: string;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
public resetPasswordGuid?: string;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
public recoveryLinkExpirationDate?: Date | null;
|
||||
|
||||
@Column({ type: 'integer', default: UserType.PLEX })
|
||||
public userType: UserType;
|
||||
|
||||
@@ -58,6 +86,25 @@ export class User {
|
||||
@OneToMany(() => MediaRequest, (request) => request.requestedBy)
|
||||
public requests: MediaRequest[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
public movieQuotaLimit?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public movieQuotaDays?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public tvQuotaLimit?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public tvQuotaDays?: number;
|
||||
|
||||
@OneToOne(() => UserSettings, (settings) => settings.user, {
|
||||
cascade: true,
|
||||
eager: true,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
public settings?: UserSettings;
|
||||
|
||||
@CreateDateColumn()
|
||||
public createdAt: Date;
|
||||
|
||||
@@ -68,27 +115,30 @@ export class User {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
public filter(): Partial<User> {
|
||||
public filter(showFiltered?: boolean): Partial<User> {
|
||||
const filtered: Partial<User> = Object.assign(
|
||||
{},
|
||||
...(Object.keys(this) as (keyof User)[])
|
||||
.filter((k) => !User.filteredFields.includes(k))
|
||||
.filter((k) => showFiltered || !User.filteredFields.includes(k))
|
||||
.map((k) => ({ [k]: this[k] }))
|
||||
);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public hasPermission(permissions: Permission | Permission[]): boolean {
|
||||
return !!hasPermission(permissions, this.permissions);
|
||||
public hasPermission(
|
||||
permissions: Permission | Permission[],
|
||||
options?: PermissionCheckOptions
|
||||
): boolean {
|
||||
return !!hasPermission(permissions, this.permissions, options);
|
||||
}
|
||||
|
||||
public passwordMatch(password: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
if (this.password) {
|
||||
resolve(bcrypt.compare(password, this.password));
|
||||
} else {
|
||||
return reject(false);
|
||||
return resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -98,31 +148,174 @@ export class User {
|
||||
this.password = hashedPassword;
|
||||
}
|
||||
|
||||
public async resetPassword(): Promise<void> {
|
||||
public async generatePassword(): Promise<void> {
|
||||
const password = generatePassword.randomPassword({ length: 16 });
|
||||
this.setPassword(password);
|
||||
|
||||
const applicationUrl = getSettings().main.applicationUrl;
|
||||
const { applicationTitle, applicationUrl } = getSettings().main;
|
||||
try {
|
||||
logger.info(`Sending password email for ${this.email}`, {
|
||||
label: 'User creation',
|
||||
logger.info(`Sending generated password email for ${this.email}`, {
|
||||
label: 'User Management',
|
||||
});
|
||||
const email = new PreparedEmail();
|
||||
await email.send({
|
||||
template: path.join(__dirname, '../templates/email/password'),
|
||||
template: path.join(__dirname, '../templates/email/generatedpassword'),
|
||||
message: {
|
||||
to: this.email,
|
||||
},
|
||||
locals: {
|
||||
password: password,
|
||||
applicationUrl,
|
||||
applicationTitle,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Failed to send out password email', {
|
||||
label: 'User creation',
|
||||
logger.error('Failed to send out generated password email', {
|
||||
label: 'User Management',
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async resetPassword(): Promise<void> {
|
||||
const guid = uuid();
|
||||
this.resetPasswordGuid = guid;
|
||||
|
||||
// 24 hours into the future
|
||||
const targetDate = new Date();
|
||||
targetDate.setDate(targetDate.getDate() + 1);
|
||||
this.recoveryLinkExpirationDate = targetDate;
|
||||
|
||||
const { applicationTitle, applicationUrl } = getSettings().main;
|
||||
const resetPasswordLink = `${applicationUrl}/resetpassword/${guid}`;
|
||||
|
||||
try {
|
||||
logger.info(`Sending reset password email for ${this.email}`, {
|
||||
label: 'User Management',
|
||||
});
|
||||
const email = new PreparedEmail();
|
||||
await email.send({
|
||||
template: path.join(__dirname, '../templates/email/resetpassword'),
|
||||
message: {
|
||||
to: this.email,
|
||||
},
|
||||
locals: {
|
||||
resetPasswordLink,
|
||||
applicationUrl: resetPasswordLink,
|
||||
applicationTitle,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Failed to send out reset password email', {
|
||||
label: 'User Management',
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
public setDisplayName(): void {
|
||||
this.displayName = this.username || this.plexUsername;
|
||||
}
|
||||
|
||||
public async getQuota(): Promise<QuotaResponse> {
|
||||
const {
|
||||
main: { defaultQuotas },
|
||||
} = getSettings();
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const canBypass = this.hasPermission([Permission.MANAGE_USERS], {
|
||||
type: 'or',
|
||||
});
|
||||
|
||||
const movieQuotaLimit = !canBypass
|
||||
? this.movieQuotaLimit ?? defaultQuotas.movie.quotaLimit
|
||||
: 0;
|
||||
const movieQuotaDays = this.movieQuotaDays ?? defaultQuotas.movie.quotaDays;
|
||||
|
||||
// Count movie requests made during quota period
|
||||
const movieDate = new Date();
|
||||
if (movieQuotaDays) {
|
||||
movieDate.setDate(movieDate.getDate() - movieQuotaDays);
|
||||
} else {
|
||||
movieDate.setDate(0);
|
||||
}
|
||||
// YYYY-MM-DD format
|
||||
const movieQuotaStartDate = movieDate.toJSON().split('T')[0];
|
||||
const movieQuotaUsed = movieQuotaLimit
|
||||
? await requestRepository.count({
|
||||
where: {
|
||||
requestedBy: this,
|
||||
createdAt: MoreThan(movieQuotaStartDate),
|
||||
type: MediaType.MOVIE,
|
||||
status: Not(MediaRequestStatus.DECLINED),
|
||||
},
|
||||
})
|
||||
: 0;
|
||||
|
||||
const tvQuotaLimit = !canBypass
|
||||
? this.tvQuotaLimit ?? defaultQuotas.tv.quotaLimit
|
||||
: 0;
|
||||
const tvQuotaDays = this.tvQuotaDays ?? defaultQuotas.tv.quotaDays;
|
||||
|
||||
// Count tv season requests made during quota period
|
||||
const tvDate = new Date();
|
||||
if (tvQuotaDays) {
|
||||
tvDate.setDate(tvDate.getDate() - tvQuotaDays);
|
||||
} else {
|
||||
tvDate.setDate(0);
|
||||
}
|
||||
// YYYY-MM-DD format
|
||||
const tvQuotaStartDate = tvDate.toJSON().split('T')[0];
|
||||
const tvQuotaUsed = tvQuotaLimit
|
||||
? (
|
||||
await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoin('request.seasons', 'seasons')
|
||||
.leftJoin('request.requestedBy', 'requestedBy')
|
||||
.where('request.type = :requestType', {
|
||||
requestType: MediaType.TV,
|
||||
})
|
||||
.andWhere('requestedBy.id = :userId', {
|
||||
userId: this.id,
|
||||
})
|
||||
.andWhere('request.createdAt > :date', {
|
||||
date: tvQuotaStartDate,
|
||||
})
|
||||
.andWhere('request.status != :declinedStatus', {
|
||||
declinedStatus: MediaRequestStatus.DECLINED,
|
||||
})
|
||||
.addSelect((subQuery) => {
|
||||
return subQuery
|
||||
.select('COUNT(season.id)', 'seasonCount')
|
||||
.from(SeasonRequest, 'season')
|
||||
.leftJoin('season.request', 'parentRequest')
|
||||
.where('parentRequest.id = request.id');
|
||||
}, 'seasonCount')
|
||||
.getMany()
|
||||
).reduce((sum: number, req: MediaRequest) => sum + req.seasonCount, 0)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
movie: {
|
||||
days: movieQuotaDays,
|
||||
limit: movieQuotaLimit,
|
||||
used: movieQuotaUsed,
|
||||
remaining: movieQuotaLimit
|
||||
? movieQuotaLimit - movieQuotaUsed
|
||||
: undefined,
|
||||
restricted:
|
||||
movieQuotaLimit && movieQuotaLimit - movieQuotaUsed <= 0
|
||||
? true
|
||||
: false,
|
||||
},
|
||||
tv: {
|
||||
days: tvQuotaDays,
|
||||
limit: tvQuotaLimit,
|
||||
used: tvQuotaUsed,
|
||||
remaining: tvQuotaLimit ? tvQuotaLimit - tvQuotaUsed : undefined,
|
||||
restricted:
|
||||
tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0 ? true : false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
43
server/entity/UserSettings.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from './User';
|
||||
|
||||
@Entity()
|
||||
export class UserSettings {
|
||||
constructor(init?: Partial<UserSettings>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@OneToOne(() => User, (user) => user.settings, { onDelete: 'CASCADE' })
|
||||
@JoinColumn()
|
||||
public user: User;
|
||||
|
||||
@Column({ default: true })
|
||||
public enableNotifications: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public discordId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public telegramChatId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public telegramSendSilently?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public region?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public originalLanguage?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public pgpKey?: string;
|
||||
}
|
||||