Compare commits
563 Commits
v1.1.1
...
feat-media
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c40d59573a | ||
|
|
19efb06faa | ||
|
|
770d788fd7 | ||
|
|
c58261c841 | ||
|
|
ccfcdea1f6 | ||
|
|
8ec8f2ac57 | ||
|
|
91f97f96ab | ||
|
|
f4051a1e5d | ||
|
|
f564cddff4 | ||
|
|
cfcce6acf0 | ||
|
|
b85d7f37b9 | ||
|
|
97396c2f57 | ||
|
|
a0ec992028 | ||
|
|
724b2f93b3 | ||
|
|
4005397f3d | ||
|
|
a67e4dbb80 | ||
|
|
cf5cf3f9ca | ||
|
|
8ae4391f37 | ||
|
|
bfd77e271a | ||
|
|
d90fc2de1c | ||
|
|
3b67d6b0e8 | ||
|
|
38348accb0 | ||
|
|
be335c39be | ||
|
|
c25c5cae38 | ||
|
|
2e059cefc0 | ||
|
|
e540b58f73 | ||
|
|
22b548bad2 | ||
|
|
c4adbdb3a8 | ||
|
|
e5d565b435 | ||
|
|
5c531011be | ||
|
|
f2b1fd24c2 | ||
|
|
4be95fade4 | ||
|
|
d02f5b0090 | ||
|
|
d5f2034e69 | ||
|
|
9059f15291 | ||
|
|
b168d04fe6 | ||
|
|
9a51c5b47b | ||
|
|
ab8efc91d5 | ||
|
|
c115f813e5 | ||
|
|
8967bb9f90 | ||
|
|
b316b9984d | ||
|
|
605a1de98f | ||
|
|
74d84a1cad | ||
|
|
8a7f39994f | ||
|
|
6e47834de0 | ||
|
|
14aafbe1d6 | ||
|
|
445604a615 | ||
|
|
fa28f05263 | ||
|
|
fd5338167a | ||
|
|
81b5e8afbd | ||
|
|
4fe4e377a6 | ||
|
|
87a59651b2 | ||
|
|
3a680c47b6 | ||
|
|
44444402a9 | ||
|
|
9140b8d98c | ||
|
|
2e20fbae1b | ||
|
|
6c0d75759f | ||
|
|
f483062d0e | ||
|
|
a7cf87f266 | ||
|
|
8ef7ec40ae | ||
|
|
3b74002f25 | ||
|
|
2b1427108c | ||
|
|
68b2388205 | ||
|
|
b20c334941 | ||
|
|
9f2ee0beeb | ||
|
|
24a3ee1e77 | ||
|
|
510a564a57 | ||
|
|
6540ba7226 | ||
|
|
3291cd08dd | ||
|
|
a08512ff71 | ||
|
|
345c67c750 | ||
|
|
bff97d2a70 | ||
|
|
62c289bd65 | ||
|
|
21cc64eee4 | ||
|
|
4a759e64fd | ||
|
|
f5122ec652 | ||
|
|
e9fafeaef8 | ||
|
|
8e2c6edd42 | ||
|
|
532f2882da | ||
|
|
9e73eaa5a3 | ||
|
|
8ef2815b44 | ||
|
|
63d4ab958a | ||
|
|
b031b58598 | ||
|
|
bdd45231e1 | ||
|
|
a38db77c8e | ||
|
|
21fa447da6 | ||
|
|
87bd130420 | ||
|
|
9a9ec41d92 | ||
|
|
e81a305f4d | ||
|
|
144980136e | ||
|
|
f6e90de708 | ||
|
|
95636c4825 | ||
|
|
aa05235392 | ||
|
|
84bfc5c363 | ||
|
|
2f2427f125 | ||
|
|
1ac2a2a909 | ||
|
|
44e368cb1b | ||
|
|
9bf20b76fa | ||
|
|
2a7128c390 | ||
|
|
8e93d351fd | ||
|
|
4acec9aeb9 | ||
|
|
51b655e364 | ||
|
|
f658e5ee66 | ||
|
|
9021e60d11 | ||
|
|
df510820fa | ||
|
|
26f90b0d7f | ||
|
|
d7ba80d502 | ||
|
|
96e90c1e7e | ||
|
|
559b7ff018 | ||
|
|
dd08f5e7cf | ||
|
|
0730e17932 | ||
|
|
a32307e6cf | ||
|
|
f9bd02553c | ||
|
|
d039e87da4 | ||
|
|
4347728a1b | ||
|
|
68f7f397d3 | ||
|
|
8c82a61450 | ||
|
|
67bde68596 | ||
|
|
3cb9494e62 | ||
|
|
f92231850c | ||
|
|
8f9d3f7fbd | ||
|
|
2b7dab2765 | ||
|
|
9ac56a4057 | ||
|
|
e8ee6f9e32 | ||
|
|
9348cdfd01 | ||
|
|
40c739c5a4 | ||
|
|
364fb46805 | ||
|
|
405f6bbb7f | ||
|
|
9a7a98b75e | ||
|
|
dc67aaaf53 | ||
|
|
31bc6ca612 | ||
|
|
b5acc09ba9 | ||
|
|
506ea92826 | ||
|
|
200d47bb43 | ||
|
|
be047427df | ||
|
|
e297d25603 | ||
|
|
89287af096 | ||
|
|
3a593d9d76 | ||
|
|
10737dd4ec | ||
|
|
7c03b831f5 | ||
|
|
cdf1e1ecc7 | ||
|
|
b9c0d5f46e | ||
|
|
4676d4f0bb | ||
|
|
a45fc86032 | ||
|
|
59dabed380 | ||
|
|
b40ba07a4d | ||
|
|
246887efa1 | ||
|
|
28a2c50495 | ||
|
|
c84ca43074 | ||
|
|
e2771a3011 | ||
|
|
3ea5076053 | ||
|
|
bd65f940e3 | ||
|
|
7bdd25e5a4 | ||
|
|
f6286359cf | ||
|
|
ac7fe1baf0 | ||
|
|
9c895f26e3 | ||
|
|
591533f850 | ||
|
|
127897b9d7 | ||
|
|
92507359b4 | ||
|
|
ca4c4440ae | ||
|
|
eb4306a2b8 | ||
|
|
baa847330d | ||
|
|
39372d2182 | ||
|
|
c484810f96 | ||
|
|
0c39057ca5 | ||
|
|
28d6e5f5ce | ||
|
|
e62a078298 | ||
|
|
3fd016808b | ||
|
|
b7282ce990 | ||
|
|
8685f5796a | ||
|
|
acc230fd20 | ||
|
|
30361f2ab7 | ||
|
|
6a8406b5e3 | ||
|
|
7980212bee | ||
|
|
317110855e | ||
|
|
048fa967f2 | ||
|
|
f7b4dfcac4 | ||
|
|
a686d31e4d | ||
|
|
cb63bf217b | ||
|
|
7eed23637d | ||
|
|
46e21c4e3e | ||
|
|
b4191f9c65 | ||
|
|
83b008c839 | ||
|
|
68c7b3650e | ||
|
|
2816c66300 | ||
|
|
01de972a8f | ||
|
|
da2d8fe35b | ||
|
|
a761b7dd35 | ||
|
|
4f89286fa8 | ||
|
|
d0836ce0ef | ||
|
|
4740476c9a | ||
|
|
c167d3ac38 | ||
|
|
2c3f533076 | ||
|
|
55baca57c1 | ||
|
|
0b797964a8 | ||
|
|
c1a47bd9de | ||
|
|
030cbc535a | ||
|
|
b0fd0f59c4 | ||
|
|
47287c3688 | ||
|
|
cc041b5e0a | ||
|
|
b4c74de7b3 | ||
|
|
9daceb7017 | ||
|
|
ff7f9725f8 | ||
|
|
cd7930eef9 | ||
|
|
24d94ef6fd | ||
|
|
04fbd00d4a | ||
|
|
33ec4436fb | ||
|
|
e848386d10 | ||
|
|
235cee1d28 | ||
|
|
8d4943997e | ||
|
|
2ab814574c | ||
|
|
c6b2dd3728 | ||
|
|
825fa75ee2 | ||
|
|
21231186d1 | ||
|
|
48f76662d5 | ||
|
|
4920670495 | ||
|
|
0a30cd356d | ||
|
|
1fe4bb8a04 | ||
|
|
21c1bbec90 | ||
|
|
ad69d6715e | ||
|
|
46cd4d01d9 | ||
|
|
672061cd64 | ||
|
|
df332cec84 | ||
|
|
d7fa35e066 | ||
|
|
f33eb862fd | ||
|
|
0a007ca805 | ||
|
|
24f268b6cb | ||
|
|
03316c642d | ||
|
|
b8e3c07c47 | ||
|
|
aa84977680 | ||
|
|
e051b1dfea | ||
|
|
c27f96096a | ||
|
|
4bd87647d0 | ||
|
|
c1e10338c1 | ||
|
|
cd1cacad55 | ||
|
|
ac77b037d5 | ||
|
|
10eb69a7dc | ||
|
|
70b1540ae2 | ||
|
|
7522aa3174 | ||
|
|
77a33cb74d | ||
|
|
c08897bdc1 | ||
|
|
469f64d484 | ||
|
|
b7e3d285ed | ||
|
|
5f1c10d50a | ||
|
|
9637c3f4ab | ||
|
|
a15c85cbd1 | ||
|
|
53f6a890b9 | ||
|
|
7dbe6f61d0 | ||
|
|
fd460df243 | ||
|
|
2e5cf22626 | ||
|
|
092d639dd9 | ||
|
|
fc1f3202e8 | ||
|
|
3bf04f2abd | ||
|
|
38fb66d31e | ||
|
|
8b3801539e | ||
|
|
101ffae641 | ||
|
|
bc9017f54d | ||
|
|
b90dedfafc | ||
|
|
a4d07f5afa | ||
|
|
f5191aded6 | ||
|
|
2520d8f739 | ||
|
|
ee23de6d2f | ||
|
|
04980f93ab | ||
|
|
2a3213d706 | ||
|
|
c36a4ba2b8 | ||
|
|
ae3818304b | ||
|
|
b3882de893 | ||
|
|
af880a6c83 | ||
|
|
eb5502a16f | ||
|
|
50f06dabbf | ||
|
|
ddbc377d79 | ||
|
|
1e2c6f46ab | ||
|
|
dd1378cef5 | ||
|
|
e684456bba | ||
|
|
6bd3f015d6 | ||
|
|
7bd4c4d1d4 | ||
|
|
3005e577d7 | ||
|
|
2d97be0d6c | ||
|
|
966639df43 | ||
|
|
33e7691b94 | ||
|
|
d7b83d22ce | ||
|
|
b6eac0f364 | ||
|
|
572a7db4aa | ||
|
|
862cd2d6ac | ||
|
|
6f23abaa6d | ||
|
|
81518df89a | ||
|
|
604335a16d | ||
|
|
78ccea94bd | ||
|
|
a487ab4506 | ||
|
|
c93467b3ac | ||
|
|
c709e8596a | ||
|
|
26e49e73a5 | ||
|
|
d954328911 | ||
|
|
3e43586acc | ||
|
|
7040da1334 | ||
|
|
9d10e6a88c | ||
|
|
4c22a71cdf | ||
|
|
b9e7933e09 | ||
|
|
2508b4340f | ||
|
|
e1f67ad8ba | ||
|
|
91abdb2ba5 | ||
|
|
8942eb8b7c | ||
|
|
ad3d922440 | ||
|
|
51b05cd8fb | ||
|
|
374c78c989 | ||
|
|
507693881b | ||
|
|
f4a22dc437 | ||
|
|
5d1c6f7065 | ||
|
|
3db010b9ea | ||
|
|
fd219717c0 | ||
|
|
d328485161 | ||
|
|
da00d454e1 | ||
|
|
d7bfc73727 | ||
|
|
5940ff7f5f | ||
|
|
fcbca1722f | ||
|
|
19370f856c | ||
|
|
154f3e72ef | ||
|
|
6fd11cf425 | ||
|
|
1154156459 | ||
|
|
cb650745f6 | ||
|
|
3aefddd488 | ||
|
|
1bf0103422 | ||
|
|
a672b324ec | ||
|
|
a343f8ad91 | ||
|
|
7a69cb35f8 | ||
|
|
c2a1a20a3b | ||
|
|
dd00e48f59 | ||
|
|
b5157010c4 | ||
|
|
812fb2f087 | ||
|
|
7b6db50ae5 | ||
|
|
3ba6df1a41 | ||
|
|
2eebb7fd39 | ||
|
|
c60667ba63 | ||
|
|
7d6831483a | ||
|
|
58c5c27929 | ||
|
|
f5c7a4fa97 | ||
|
|
50e85c975a | ||
|
|
62e2de70bf | ||
|
|
0683f4f000 | ||
|
|
b1bd569335 | ||
|
|
a49ea92692 | ||
|
|
d23b2132de | ||
|
|
8bd10b5bf3 | ||
|
|
08c9085f0d | ||
|
|
0d8b390b67 | ||
|
|
19b4dc424f | ||
|
|
7fef48df63 | ||
|
|
8220ea55ae | ||
|
|
4de4a1a52c | ||
|
|
042a1a950f | ||
|
|
421029ebab | ||
|
|
4e9be7a3f7 | ||
|
|
af4a3b4279 | ||
|
|
5ce59cc2ee | ||
|
|
ff2fa66002 | ||
|
|
9388a1e61c | ||
|
|
b44a1b4a99 | ||
|
|
6f73dbc36a | ||
|
|
9d3446d370 | ||
|
|
2f680b4cec | ||
|
|
2179637d43 | ||
|
|
e084649878 | ||
|
|
edf5010659 | ||
|
|
93afead92e | ||
|
|
dd48d59b20 | ||
|
|
c4b16abc62 | ||
|
|
1a95d423f2 | ||
|
|
cd3574851a | ||
|
|
299f65c597 | ||
|
|
6021d1e336 | ||
|
|
3ce1ef350e | ||
|
|
06c91744f3 | ||
|
|
f14d9407d8 | ||
|
|
68223f4b1e | ||
|
|
76335ec8d3 | ||
|
|
2714cbcefd | ||
|
|
3309f77aa4 | ||
|
|
6face8cc45 | ||
|
|
27feeea691 | ||
|
|
03853a1b91 | ||
|
|
357cab87ac | ||
|
|
d18e3d185f | ||
|
|
e222463a63 | ||
|
|
03b9bda287 | ||
|
|
7e20c7cb78 | ||
|
|
d0cdce9e90 | ||
|
|
113b09bf2b | ||
|
|
b16f192b92 | ||
|
|
d9ca3c6e52 | ||
|
|
ba82ecec5c | ||
|
|
c052a2455c | ||
|
|
2d99a8b03c | ||
|
|
7434c0cf2f | ||
|
|
afcb096f49 | ||
|
|
9dc11cedbf | ||
|
|
22aab783d4 | ||
|
|
a2babb83ad | ||
|
|
76a7ceb758 | ||
|
|
9688acaa87 | ||
|
|
64339e5f03 | ||
|
|
1ceea3dcca | ||
|
|
e3c3283603 | ||
|
|
4ac02d3aac | ||
|
|
8eacfe045f | ||
|
|
15e246929b | ||
|
|
c1424634fb | ||
|
|
07ec3efbca | ||
|
|
9b07b10901 | ||
|
|
b1e9cdbea2 | ||
|
|
9aee630392 | ||
|
|
6b50f77624 | ||
|
|
16f1c286c4 | ||
|
|
64aab6dd82 | ||
|
|
144bb84bdc | ||
|
|
76260f9b22 | ||
|
|
500cd1f872 | ||
|
|
9252817b58 | ||
|
|
a66925067d | ||
|
|
d037d178aa | ||
|
|
ab09664d41 | ||
|
|
bfe56c3470 | ||
|
|
1dfa9431a9 | ||
|
|
0faae20bac | ||
|
|
5b10da4073 | ||
|
|
6049edffca | ||
|
|
f27200c8c1 | ||
|
|
613ebb95d2 | ||
|
|
15c79e03a5 | ||
|
|
ed95b0af25 | ||
|
|
f5c2fc1c20 | ||
|
|
3ba69f9a74 | ||
|
|
bcd2bb7c96 | ||
|
|
66357019f0 | ||
|
|
21d20fdfd6 | ||
|
|
cf96db90ad | ||
|
|
430b1ab871 | ||
|
|
7404d68143 | ||
|
|
16cb53f703 | ||
|
|
407af32d32 | ||
|
|
5c01313cc4 | ||
|
|
d8da5cbe9d | ||
|
|
5a72f5f86e | ||
|
|
3d458dd2fd | ||
|
|
e486623310 | ||
|
|
e0f9a6e12f | ||
|
|
05139717d1 | ||
|
|
f20ba3fc2e | ||
|
|
30141f76e0 | ||
|
|
87825a0e05 | ||
|
|
99fc9a2da0 | ||
|
|
6dbb99e0b6 | ||
|
|
3b0c0915fb | ||
|
|
5f7e7eef11 | ||
|
|
2dd3925e92 | ||
|
|
611ceeb5f4 | ||
|
|
0636ff83a2 | ||
|
|
aa005149be | ||
|
|
13130188fc | ||
|
|
8724058aa5 | ||
|
|
94513425be | ||
|
|
323086db09 | ||
|
|
9518cb3635 | ||
|
|
b66f12a0e1 | ||
|
|
e9eba96f5a | ||
|
|
14280c5437 | ||
|
|
867286996b | ||
|
|
03d5e56678 | ||
|
|
410ad0d4b4 | ||
|
|
23f93e311d | ||
|
|
2950cf4438 | ||
|
|
dbdecb1e0a | ||
|
|
833f52de56 | ||
|
|
889caaa733 | ||
|
|
4d56320870 | ||
|
|
1a0053221b | ||
|
|
b925857dfa | ||
|
|
c4aa08f5f0 | ||
|
|
5d73bc2238 | ||
|
|
095048d94a | ||
|
|
98028bf2f4 | ||
|
|
baf1ea95a3 | ||
|
|
23409e6f2f | ||
|
|
dd28200040 | ||
|
|
22360f3b87 | ||
|
|
815d709bcf | ||
|
|
8a2acb7f2b | ||
|
|
67f3a3829e | ||
|
|
f5e5016ca5 | ||
|
|
6e60a275c7 | ||
|
|
3b2633812b | ||
|
|
507227aa49 | ||
|
|
29ab178fb0 | ||
|
|
f5e6b620c1 | ||
|
|
0839718806 | ||
|
|
950b1712b7 | ||
|
|
43a9067976 | ||
|
|
c6a133d4e5 | ||
|
|
4b855b8114 | ||
|
|
6c0fd40877 | ||
|
|
301f2bf7ab | ||
|
|
7943e0c339 | ||
|
|
6ce0aa5b10 | ||
|
|
a0301e2d83 | ||
|
|
9021696cf0 | ||
|
|
9bc1f89777 | ||
|
|
a12697b061 | ||
|
|
5247f14968 | ||
|
|
fd0ff4bd5f | ||
|
|
16545eec22 | ||
|
|
36d17fed6e | ||
|
|
ac34328074 | ||
|
|
91e0928aa0 | ||
|
|
f836cadd23 | ||
|
|
f4910a1483 | ||
|
|
103c4ca49c | ||
|
|
c143c0b8d2 | ||
|
|
e5d8c93ab8 | ||
|
|
72d7a3477f | ||
|
|
808fabba9a | ||
|
|
7a5fab35ff | ||
|
|
17ac5069e5 | ||
|
|
cfab63c0ca | ||
|
|
0fa84eae8d | ||
|
|
821bb79d83 | ||
|
|
233035dbd7 | ||
|
|
114943ae2c | ||
|
|
a6f7b19693 | ||
|
|
3db3044210 | ||
|
|
1fcfe93b58 | ||
|
|
6cb456cb69 | ||
|
|
e939dc678e | ||
|
|
f3e56da3b7 | ||
|
|
70dc4c4b3b | ||
|
|
6428b8d419 | ||
|
|
004e1bb17e | ||
|
|
ebd22ffcea | ||
|
|
22ec058431 | ||
|
|
7d4455ba6b | ||
|
|
db898db9f2 | ||
|
|
b33956e6b8 | ||
|
|
f5864b49de | ||
|
|
25eb765f9b | ||
|
|
9da8461225 | ||
|
|
aed1409f29 | ||
|
|
575da306b0 | ||
|
|
f4c38fa81f | ||
|
|
a3b620efb3 | ||
|
|
054da8e456 | ||
|
|
6cd0c9b2c8 | ||
|
|
2e7458457e | ||
|
|
b67844a0ee | ||
|
|
b08025195e | ||
|
|
a5cc36c88f | ||
|
|
c744e2a9b6 | ||
|
|
4a34574a23 | ||
|
|
38fc150892 | ||
|
|
6e2cf2f80e | ||
|
|
4ccc956c35 | ||
|
|
5af3a7e71b | ||
|
|
8619724c65 | ||
|
|
304b82b594 | ||
|
|
a6c1f3f7ce | ||
|
|
eb5248d8d1 |
@@ -4,674 +4,279 @@
|
|||||||
],
|
],
|
||||||
"imageSize": 100,
|
"imageSize": 100,
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"login": "sct",
|
|
||||||
"name": "sct",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/234213?v=4",
|
|
||||||
"profile": "https://sct.dev",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"design",
|
|
||||||
"ideas"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "azoitos",
|
|
||||||
"name": "Alex Zoitos",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/26529049?v=4",
|
|
||||||
"profile": "https://github.com/azoitos",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "OwsleyJr",
|
|
||||||
"name": "Brandon Cohen",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/8635678?v=4",
|
|
||||||
"profile": "https://github.com/OwsleyJr",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"doc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Ahreluth",
|
|
||||||
"name": "Ahreluth",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/75682440?v=4",
|
|
||||||
"profile": "https://github.com/Ahreluth",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "KovalevArtem",
|
|
||||||
"name": "KovalevArtem",
|
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/36500228?v=4",
|
|
||||||
"profile": "https://github.com/KovalevArtem",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "GiyomuWeb",
|
|
||||||
"name": "GiyomuWeb",
|
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/62489209?v=4",
|
|
||||||
"profile": "https://github.com/GiyomuWeb",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "angrycuban13",
|
|
||||||
"name": "Angry Cuban",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/39564898?v=4",
|
|
||||||
"profile": "https://github.com/angrycuban13",
|
|
||||||
"contributions": [
|
|
||||||
"doc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "jvennik",
|
|
||||||
"name": "jvennik",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/6672637?v=4",
|
|
||||||
"profile": "https://github.com/jvennik",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "darknessgp",
|
|
||||||
"name": "darknessgp",
|
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1521243?v=4",
|
|
||||||
"profile": "https://github.com/darknessgp",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "saltydk",
|
|
||||||
"name": "salty",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/6587950?v=4",
|
|
||||||
"profile": "https://github.com/saltydk",
|
|
||||||
"contributions": [
|
|
||||||
"infra"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Shutruk",
|
|
||||||
"name": "Shutruk",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/9198633?v=4",
|
|
||||||
"profile": "https://github.com/Shutruk",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "krystiancharubin",
|
|
||||||
"name": "Krystian Charubin",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/17775600?v=4",
|
|
||||||
"profile": "https://github.com/krystiancharubin",
|
|
||||||
"contributions": [
|
|
||||||
"design"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "kieron",
|
|
||||||
"name": "Kieron Boswell",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/8655212?v=4",
|
|
||||||
"profile": "https://github.com/kieron",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "samwiseg0",
|
|
||||||
"name": "samwiseg0",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/2241731?v=4",
|
|
||||||
"profile": "https://github.com/samwiseg0",
|
|
||||||
"contributions": [
|
|
||||||
"question",
|
|
||||||
"infra"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "ecelebi29",
|
|
||||||
"name": "ecelebi29",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/8337120?v=4",
|
|
||||||
"profile": "https://github.com/ecelebi29",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"doc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mmozeiko",
|
|
||||||
"name": "Mārtiņš Možeiko",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/1665010?v=4",
|
|
||||||
"profile": "https://github.com/mmozeiko",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mazzetta86",
|
|
||||||
"name": "mazzetta86",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/45591560?v=4",
|
|
||||||
"profile": "https://github.com/mazzetta86",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Panzer1119",
|
|
||||||
"name": "Paul Hagedorn",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/23016343?v=4",
|
|
||||||
"profile": "https://github.com/Panzer1119",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Shagon94",
|
|
||||||
"name": "Shagon94",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/9140783?v=4",
|
|
||||||
"profile": "https://github.com/Shagon94",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "sebstrgg",
|
|
||||||
"name": "sebstrgg",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/27026694?v=4",
|
|
||||||
"profile": "https://github.com/sebstrgg",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "danshilm",
|
|
||||||
"name": "Danshil Mungur",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/20923978?v=4",
|
|
||||||
"profile": "https://github.com/danshilm",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"doc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "doob187",
|
|
||||||
"name": "doob187",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/60312740?v=4",
|
|
||||||
"profile": "https://github.com/doob187",
|
|
||||||
"contributions": [
|
|
||||||
"infra"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "johnpyp",
|
|
||||||
"name": "johnpyp",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/20625636?v=4",
|
|
||||||
"profile": "https://github.com/johnpyp",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "ankarhem",
|
|
||||||
"name": "Jakob Ankarhem",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/14110063?v=4",
|
|
||||||
"profile": "https://github.com/ankarhem",
|
|
||||||
"contributions": [
|
|
||||||
"doc",
|
|
||||||
"code",
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "jayesh100",
|
|
||||||
"name": "Jayesh",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/8022175?v=4",
|
|
||||||
"profile": "https://github.com/jayesh100",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "flying-sausages",
|
|
||||||
"name": "flying-sausages",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/23618693?v=4",
|
|
||||||
"profile": "https://github.com/flying-sausages",
|
|
||||||
"contributions": [
|
|
||||||
"doc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "hirenshah",
|
|
||||||
"name": "hirenshah",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/418112?v=4",
|
|
||||||
"profile": "https://github.com/hirenshah",
|
|
||||||
"contributions": [
|
|
||||||
"doc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "TheCatLady",
|
|
||||||
"name": "TheCatLady",
|
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/52870424?v=4",
|
|
||||||
"profile": "https://github.com/TheCatLady",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"translation",
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "acortelyou",
|
|
||||||
"name": "Alex Cortelyou",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1689668?v=4",
|
|
||||||
"profile": "https://github.com/acortelyou",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "jonocairns",
|
|
||||||
"name": "Jono Cairns",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/182836?v=4",
|
|
||||||
"profile": "https://nz.linkedin.com/in/jonocairns",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "DJScias",
|
|
||||||
"name": "DJScias",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/439655?v=4",
|
|
||||||
"profile": "https://scias.net/",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Dabu-dot",
|
|
||||||
"name": "Dabu-dot",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/52525576?v=4",
|
|
||||||
"profile": "https://github.com/Dabu-dot",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Jabster28",
|
|
||||||
"name": "Jabster28",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/29015942?v=4",
|
|
||||||
"profile": "https://github.com/Jabster28",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "littlerooster",
|
|
||||||
"name": "littlerooster",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/83890654?v=4",
|
|
||||||
"profile": "https://github.com/littlerooster",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "dphildebrandt",
|
|
||||||
"name": "Dustin Hildebrandt",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/154459?v=4",
|
|
||||||
"profile": "https://github.com/dphildebrandt",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Generator",
|
|
||||||
"name": "Bruno Guerreiro",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/44146?v=4",
|
|
||||||
"profile": "https://github.com/Generator",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "iceHtwoO",
|
|
||||||
"name": "Alexander Neuhäuser",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/27020492?v=4",
|
|
||||||
"profile": "https://github.com/iceHtwoO",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "liviokanone",
|
|
||||||
"name": "Livio",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/37431541?v=4",
|
|
||||||
"profile": "http://www.unext.co.jp",
|
|
||||||
"contributions": [
|
|
||||||
"design"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "tangentThought",
|
|
||||||
"name": "tangentThought",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/25516090?v=4",
|
|
||||||
"profile": "https://github.com/tangentThought",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "nicospz",
|
|
||||||
"name": "Nicolás Espinoza",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/31373060?v=4",
|
|
||||||
"profile": "https://github.com/nicospz",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "sootylunatic",
|
|
||||||
"name": "sootylunatic",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/36486087?v=4",
|
|
||||||
"profile": "https://github.com/sootylunatic",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "JoKerIsCraZy",
|
|
||||||
"name": "JoKerIsCraZy",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/47474211?v=4",
|
|
||||||
"profile": "https://github.com/JoKerIsCraZy",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "GoByeBye",
|
|
||||||
"name": "Daddie0",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/33762262?v=4",
|
|
||||||
"profile": "https://daddie.dev",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Simoneu01",
|
|
||||||
"name": "Simone",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/43807696?v=4",
|
|
||||||
"profile": "http://ungaro.me",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "adan89lion",
|
|
||||||
"name": "Seohyun Joo",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/6585644?v=4",
|
|
||||||
"profile": "https://github.com/adan89lion",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "ty4ko",
|
|
||||||
"name": "Sergey",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/21213535?v=4",
|
|
||||||
"profile": "https://github.com/ty4ko",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "skafte1990",
|
|
||||||
"name": "Shaaft",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/31465453?v=4",
|
|
||||||
"profile": "https://github.com/skafte1990",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "sr093906",
|
|
||||||
"name": "sr093906",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8369201?v=4",
|
|
||||||
"profile": "https://github.com/sr093906",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Nackophilz",
|
|
||||||
"name": "Nackophilz",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/61667226?v=4",
|
|
||||||
"profile": "https://github.com/Nackophilz",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "schambers",
|
|
||||||
"name": "Sean Chambers",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/31563?v=4",
|
|
||||||
"profile": "https://github.com/schambers",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "deniscerri",
|
|
||||||
"name": "deniscerri",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/64997243?v=4",
|
|
||||||
"profile": "https://github.com/deniscerri",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "tomgacz",
|
|
||||||
"name": "tomgacz",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/14138209?v=4",
|
|
||||||
"profile": "https://github.com/tomgacz",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Andersborrits",
|
|
||||||
"name": "Andersborrits",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/29452218?v=4",
|
|
||||||
"profile": "https://github.com/Andersborrits",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Maxentr",
|
|
||||||
"name": "Maxent",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/67283154?v=4",
|
|
||||||
"profile": "http://maxentrouault.fr",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
"projectName": "overseerr",
|
"projectName": "jellyseerr",
|
||||||
"projectOwner": "sct",
|
"projectOwner": "Fallenbagel",
|
||||||
"repoType": "github",
|
"repoType": "github",
|
||||||
"repoHost": "https://github.com",
|
"repoHost": "https://github.com",
|
||||||
"skipCi": true
|
"skipCi": true,
|
||||||
|
"commitConvention": "angular",
|
||||||
|
"commitType": "docs",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"login": "Fallenbagel",
|
||||||
|
"name": "Fallenbagel",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/98979876?v=4",
|
||||||
|
"profile": "https://github.com/Fallenbagel",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"maintenance"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "seanzhang98",
|
||||||
|
"name": "Sean",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/34902361?v=4",
|
||||||
|
"profile": "https://github.com/seanzhang98",
|
||||||
|
"contributions": [
|
||||||
|
"translation",
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "notfakie",
|
||||||
|
"name": "notfakie",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/103784113?v=4",
|
||||||
|
"profile": "https://github.com/notfakie",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Jumail",
|
||||||
|
"name": "Mohamed Jumail",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/7672055?v=4",
|
||||||
|
"profile": "https://github.com/Jumail",
|
||||||
|
"contributions": [
|
||||||
|
"review"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "jsl9208",
|
||||||
|
"name": "Shilong Jiang",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/4048787?v=4",
|
||||||
|
"profile": "https://www.heywhale.com",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "boring-dragon",
|
||||||
|
"name": "Boring Dragon",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/28459081?v=4",
|
||||||
|
"profile": "https://jinas.me",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "sambartik",
|
||||||
|
"name": "Samuel Bartík",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/63553146?v=4",
|
||||||
|
"profile": "https://github.com/sambartik",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "CyferShepard",
|
||||||
|
"name": "Thegan Govender",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/24864904?v=4",
|
||||||
|
"profile": "https://github.com/CyferShepard",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "jab416171",
|
||||||
|
"name": "jab416171",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/345752?v=4",
|
||||||
|
"profile": "https://github.com/jab416171",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "NicolaiVdS",
|
||||||
|
"name": "Nicolai Van der Storm",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5257222?v=4",
|
||||||
|
"profile": "https://nvds.be",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Smexhy",
|
||||||
|
"name": "Smexhy",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/4880625?v=4",
|
||||||
|
"profile": "https://github.com/Smexhy",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "dd060606",
|
||||||
|
"name": "dd060606",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/58089504?v=4",
|
||||||
|
"profile": "https://dd06-dev.fr",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "darmiel",
|
||||||
|
"name": "Daniel",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/71837281?v=4",
|
||||||
|
"profile": "https://qwer.tz",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "undone37",
|
||||||
|
"name": "undone37",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/10513808?v=4",
|
||||||
|
"profile": "https://github.com/undone37",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "CheChu10",
|
||||||
|
"name": "Chechu García",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/32913133?v=4",
|
||||||
|
"profile": "https://github.com/CheChu10",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "DimitriDR",
|
||||||
|
"name": "Dimitri",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/56969769?v=4",
|
||||||
|
"profile": "https://github.com/DimitriDR",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "andrey4korop",
|
||||||
|
"name": "andrey4korop",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/24610708?v=4",
|
||||||
|
"profile": "https://github.com/andrey4korop",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "GeoffreyCoulaud",
|
||||||
|
"name": "Geoffrey Coulaud",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/20744730?v=4",
|
||||||
|
"profile": "https://geoffrey-coulaud.fr",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Pikachu920",
|
||||||
|
"name": "Pikachu920",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/28607612?v=4",
|
||||||
|
"profile": "https://github.com/Pikachu920",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "yalagin",
|
||||||
|
"name": "Maxim Yalagin",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/12879142?v=4",
|
||||||
|
"profile": "https://github.com/yalagin",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "jeaboswell",
|
||||||
|
"name": "Jesse Boswell",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/11653068?v=4",
|
||||||
|
"profile": "https://github.com/jeaboswell",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "d-fendrich",
|
||||||
|
"name": "d-fendrich",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/27904138?v=4",
|
||||||
|
"profile": "https://github.com/d-fendrich",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "davidfdezalcoba",
|
||||||
|
"name": "David Fernández Alcoba",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/15996018?v=4",
|
||||||
|
"profile": "https://github.com/davidfdezalcoba",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Gauvino",
|
||||||
|
"name": "Gauvino",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/68083474?v=4",
|
||||||
|
"profile": "https://github.com/Gauvino",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "EthanArmbrust",
|
||||||
|
"name": "EthanArmbrust",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/22754714?v=4",
|
||||||
|
"profile": "https://github.com/EthanArmbrust",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "SirMartin",
|
||||||
|
"name": "Eduardo",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/854646?v=4",
|
||||||
|
"profile": "http://www.piribisoft.com",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "RickLuiken",
|
||||||
|
"name": "RickLuiken",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/34110371?v=4",
|
||||||
|
"profile": "https://github.com/RickLuiken",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Br33ce",
|
||||||
|
"name": "Br33ce",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/124933490?v=4",
|
||||||
|
"profile": "https://github.com/Br33ce",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "athphane",
|
||||||
|
"name": "Athfan Khaleel",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/13810742?v=4",
|
||||||
|
"profile": "https://athfan.com",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ public/os_logo_filled.png
|
|||||||
public/preview.jpg
|
public/preview.jpg
|
||||||
snap
|
snap
|
||||||
stylelint.config.js
|
stylelint.config.js
|
||||||
|
cypress
|
||||||
|
|||||||
15
.eslintrc.js
15
.eslintrc.js
@@ -7,6 +7,7 @@ module.exports = {
|
|||||||
'plugin:jsx-a11y/recommended',
|
'plugin:jsx-a11y/recommended',
|
||||||
'plugin:react/recommended',
|
'plugin:react/recommended',
|
||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
'prettier',
|
'prettier',
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
@@ -26,11 +27,21 @@ module.exports = {
|
|||||||
'react-hooks/rules-of-hooks': 'error',
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
|
||||||
'formatjs/no-offset': 'error',
|
'formatjs/no-offset': 'error',
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': ['error'],
|
'@typescript-eslint/no-unused-vars': ['error'],
|
||||||
|
'@typescript-eslint/array-type': ['error', { default: 'array' }],
|
||||||
'jsx-a11y/no-onchange': 'off',
|
'jsx-a11y/no-onchange': 'off',
|
||||||
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
prefer: 'type-imports',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'no-relative-import-paths/no-relative-import-paths': [
|
||||||
|
'error',
|
||||||
|
{ allowSameFolder: true },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
@@ -40,7 +51,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: ['jsx-a11y', 'prettier', 'react-hooks', 'formatjs'],
|
plugins: ['jsx-a11y', 'react-hooks', 'formatjs', 'no-relative-import-paths'],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
pragma: 'React',
|
pragma: 'React',
|
||||||
|
|||||||
21
.gitattributes
vendored
21
.gitattributes
vendored
@@ -24,3 +24,24 @@
|
|||||||
*.woff binary
|
*.woff binary
|
||||||
*.pyc binary
|
*.pyc binary
|
||||||
*.pdf binary
|
*.pdf binary
|
||||||
|
|
||||||
|
#
|
||||||
|
## Theses files/directories should be excluded from git archives
|
||||||
|
#
|
||||||
|
|
||||||
|
.husky export-ignore
|
||||||
|
.vscode export-ignore
|
||||||
|
docs export-ignore
|
||||||
|
|
||||||
|
.git* export-ignore
|
||||||
|
*ignore export-ignore
|
||||||
|
*.md export-ignore
|
||||||
|
|
||||||
|
.all-contributorsrc export-ignore
|
||||||
|
.editorconfig export-ignore
|
||||||
|
Dockerfile.local export-ignore
|
||||||
|
docker-compose.yml export-ignore
|
||||||
|
stylelint.config.js export-ignore
|
||||||
|
|
||||||
|
public/os_logo_filled.png export-ignore
|
||||||
|
public/preview.jpg export-ignore
|
||||||
|
|||||||
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -1,7 +1,2 @@
|
|||||||
# Global code ownership
|
# Global code ownership
|
||||||
|
* @Fallenbagel
|
||||||
- @Fallenbagel
|
|
||||||
|
|
||||||
# i18n locale files
|
|
||||||
|
|
||||||
src/i18n/locale/ @Fallenbagel
|
|
||||||
|
|||||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
|||||||
github: [sct]
|
github: [Fallenbagel]
|
||||||
patreon: overseerr
|
|
||||||
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -19,7 +19,7 @@ body:
|
|||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version of Overseerr are you running? (You can find this in Settings → About → Version.)
|
description: What version of Jellyseerr are you running? (You can find this in Settings → About → Version.)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -87,5 +87,5 @@ body:
|
|||||||
label: Code of Conduct
|
label: Code of Conduct
|
||||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/CODE_OF_CONDUCT.md)
|
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/CODE_OF_CONDUCT.md)
|
||||||
options:
|
options:
|
||||||
- label: I agree to follow Overseerr's Code of Conduct
|
- label: I agree to follow Jellyseerr's Code of Conduct
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,7 +2,7 @@ blank_issues_enabled: false
|
|||||||
contact_links:
|
contact_links:
|
||||||
- name: 💬 Support via Discord
|
- name: 💬 Support via Discord
|
||||||
url: https://discord.gg/ckbvBtDJgC
|
url: https://discord.gg/ckbvBtDJgC
|
||||||
about: Chat with other users and the Overseerr dev team
|
about: Chat with other users and the Jellyseerr dev team
|
||||||
- name: 💬 Support via GitHub Discussions
|
- name: 💬 Support via GitHub Discussions
|
||||||
url: https://github.com/fallenbagel/jellyseerr/discussions
|
url: https://github.com/fallenbagel/jellyseerr/discussions
|
||||||
about: Ask questions and discuss with other community members
|
about: Ask questions and discuss with other community members
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
2
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
@@ -33,5 +33,5 @@ body:
|
|||||||
label: Code of Conduct
|
label: Code of Conduct
|
||||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/CODE_OF_CONDUCT.md)
|
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/CODE_OF_CONDUCT.md)
|
||||||
options:
|
options:
|
||||||
- label: I agree to follow Overseerr's Code of Conduct
|
- label: I agree to follow Jellyseerr's Code of Conduct
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
5
.github/holopin.yml
vendored
Normal file
5
.github/holopin.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
organization: overseerr
|
||||||
|
defaultSticker: clcyagj1j329008l468ya8pu2
|
||||||
|
stickers:
|
||||||
|
- id: clcyagj1j329008l468ya8pu2
|
||||||
|
alias: overseerr-contributor
|
||||||
56
.github/workflows/ci.yml
vendored
56
.github/workflows/ci.yml
vendored
@@ -3,7 +3,7 @@ name: Jellyseerr CI
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "*"
|
- '*'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
@@ -12,45 +12,51 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Lint & Test Build
|
name: Lint & Test Build
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
container: node:16.14-alpine
|
container: node:18.18-alpine
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
HUSKY_SKIP_INSTALL: 1
|
HUSKY: 0
|
||||||
run: yarn
|
run: yarn
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
- name: Formatting
|
||||||
|
run: yarn format:check
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build
|
run: yarn build
|
||||||
|
|
||||||
build_and_push:
|
build_and_push:
|
||||||
name: Build & Publish Docker Images
|
name: Build & Publish Docker Images
|
||||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set lower case owner name
|
||||||
|
run: |
|
||||||
|
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||||
|
env:
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -60,32 +66,24 @@ jobs:
|
|||||||
COMMIT_TAG=${{ github.sha }}
|
COMMIT_TAG=${{ github.sha }}
|
||||||
tags: |
|
tags: |
|
||||||
fallenbagel/jellyseerr:develop
|
fallenbagel/jellyseerr:develop
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
|
||||||
- # 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:
|
discord:
|
||||||
name: Send Discord Notification
|
name: Send Discord Notification
|
||||||
needs: build_and_push
|
needs: build_and_push
|
||||||
if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]')
|
if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Get Build Job Status
|
- name: Get Build Job Status
|
||||||
uses: technote-space/workflow-conclusion-action@v2
|
uses: technote-space/workflow-conclusion-action@v3
|
||||||
- name: Combine Job Status
|
- name: Combine Job Status
|
||||||
id: status
|
id: status
|
||||||
run: |
|
run: |
|
||||||
failures=(neutral, skipped, timed_out, action_required)
|
failures=(neutral, skipped, timed_out, action_required)
|
||||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||||
echo ::set-output name=status::failure
|
echo "status=failure" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
- name: Post Status to Discord
|
- name: Post Status to Discord
|
||||||
uses: sarisia/actions-status-discord@v1
|
uses: sarisia/actions-status-discord@v1
|
||||||
|
|||||||
41
.github/workflows/codeql.yml
vendored
Normal file
41
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: 'CodeQL'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['develop']
|
||||||
|
pull_request:
|
||||||
|
branches: ['develop']
|
||||||
|
schedule:
|
||||||
|
- cron: '50 7 * * 5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [javascript]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
queries: +security-and-quality
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: '/language:${{ matrix.language }}'
|
||||||
30
.github/workflows/cypress.yml
vendored
Normal file
30
.github/workflows/cypress.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Cypress Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cypress-run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Cypress run
|
||||||
|
uses: cypress-io/github-action@v4
|
||||||
|
with:
|
||||||
|
build: yarn cypress:build
|
||||||
|
start: yarn start
|
||||||
|
wait-on: 'http://localhost:5055'
|
||||||
|
record: true
|
||||||
|
env:
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
WITH_MIGRATIONS: true
|
||||||
|
# Fix test titles in cypress dashboard
|
||||||
|
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
|
||||||
|
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
|
||||||
14
.github/workflows/preview.yml
vendored
14
.github/workflows/preview.yml
vendored
@@ -3,29 +3,29 @@ name: Jellyseerr Preview
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "preview-*"
|
- 'preview-*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_push:
|
build_and_push:
|
||||||
name: Build & Publish Docker Preview Images
|
name: Build & Publish Docker Preview Images
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Get the version
|
- name: Get the version
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
39
.github/workflows/private_registery_push.yml
vendored
39
.github/workflows/private_registery_push.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: 'create docker image on pull request and push to private registery'
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-image:
|
|
||||||
runs-on: self-hosted
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Login to private registery
|
|
||||||
uses: docker/login-action@v2.0.0
|
|
||||||
with:
|
|
||||||
registry: ${{ secrets.REGISTRY_URL }}
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
-
|
|
||||||
name: Build and push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: ./
|
|
||||||
file: ./Dockerfile
|
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
|
||||||
push: true
|
|
||||||
tags: '${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:${{ github.sha }}'
|
|
||||||
cache-from: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache'
|
|
||||||
cache-to: 'type=registry,ref=${{ secrets.REGISTRY_URL }}/fallenbagel/jellyseerr:buildcache,mode=max'
|
|
||||||
73
.github/workflows/release.yml
vendored
73
.github/workflows/release.yml
vendored
@@ -5,7 +5,7 @@ on: workflow_dispatch
|
|||||||
jobs:
|
jobs:
|
||||||
semantic-release:
|
semantic-release:
|
||||||
name: Tag and release latest version
|
name: Tag and release latest version
|
||||||
runs-on: self-hosted
|
runs-on: ubuntu-22.04
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
steps:
|
steps:
|
||||||
@@ -18,16 +18,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Install Yarn
|
|
||||||
run: npm install -g yarn
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
- name: Release
|
- name: Release
|
||||||
@@ -37,22 +35,77 @@ jobs:
|
|||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
run: npx semantic-release
|
run: npx semantic-release
|
||||||
|
|
||||||
|
build-snap:
|
||||||
|
name: Build Snap Package (${{ matrix.architecture }})
|
||||||
|
needs: semantic-release
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
architecture:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- armhf
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Switch to main branch
|
||||||
|
run: git checkout main
|
||||||
|
- name: Pull latest changes
|
||||||
|
run: git pull
|
||||||
|
- name: Prepare
|
||||||
|
id: prepare
|
||||||
|
run: |
|
||||||
|
git fetch --prune --tags
|
||||||
|
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||||
|
echo "RELEASE=stable" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "RELEASE=edge" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
- 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
|
||||||
|
id: build
|
||||||
|
with:
|
||||||
|
architecture: ${{ matrix.architecture }}
|
||||||
|
- name: Upload Snap Package
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||||
|
path: ${{ steps.build.outputs.snap }}
|
||||||
|
- name: Review Snap Package
|
||||||
|
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||||
|
with:
|
||||||
|
snap: ${{ steps.build.outputs.snap }}
|
||||||
|
- name: Publish Snap Package
|
||||||
|
uses: snapcore/action-publish@v1
|
||||||
|
env:
|
||||||
|
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }}
|
||||||
|
with:
|
||||||
|
snap: ${{ steps.build.outputs.snap }}
|
||||||
|
release: ${{ steps.prepare.outputs.RELEASE }}
|
||||||
|
|
||||||
discord:
|
discord:
|
||||||
name: Send Discord Notification
|
name: Send Discord Notification
|
||||||
needs: semantic-release
|
needs: semantic-release
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: self-hosted
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Get Build Job Status
|
- name: Get Build Job Status
|
||||||
uses: technote-space/workflow-conclusion-action@v2
|
uses: technote-space/workflow-conclusion-action@v3
|
||||||
- name: Combine Job Status
|
- name: Combine Job Status
|
||||||
id: status
|
id: status
|
||||||
run: |
|
run: |
|
||||||
failures=(neutral, skipped, timed_out, action_required)
|
failures=(neutral, skipped, timed_out, action_required)
|
||||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||||
echo ::set-output name=status::failure
|
echo "status=failure" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo ::set-output name=status::$WORKFLOW_CONCLUSION
|
echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
- name: Post Status to Discord
|
- name: Post Status to Discord
|
||||||
uses: sarisia/actions-status-discord@v1
|
uses: sarisia/actions-status-discord@v1
|
||||||
|
|||||||
91
.github/workflows/snap.yaml
vendored
Normal file
91
.github/workflows/snap.yaml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
name: Publish Snap
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
jobs:
|
||||||
|
name: Job Check
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||||
|
steps:
|
||||||
|
- name: Cancel Previous Runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.10.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
build-snap:
|
||||||
|
name: Build Snap Package (${{ matrix.architecture }})
|
||||||
|
needs: jobs
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
architecture:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- armhf
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Prepare
|
||||||
|
id: prepare
|
||||||
|
run: |
|
||||||
|
git fetch --prune --unshallow --tags
|
||||||
|
if [[ $GITHUB_REF == refs/tags/* || $GITHUB_REF == refs/heads/master ]]; then
|
||||||
|
echo "RELEASE=stable" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "RELEASE=edge" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
- name: Set Up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Configure Git
|
||||||
|
run: git config --add safe.directory /data/parts/jellyseerr/src
|
||||||
|
- name: Build Snap Package
|
||||||
|
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||||
|
id: build
|
||||||
|
with:
|
||||||
|
architecture: ${{ matrix.architecture }}
|
||||||
|
- name: Upload Snap Package
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: jellyseerr-snap-package-${{ matrix.architecture }}
|
||||||
|
path: ${{ steps.build.outputs.snap }}
|
||||||
|
- name: Review Snap Package
|
||||||
|
uses: diddlesnaps/snapcraft-review-tools-action@v1
|
||||||
|
with:
|
||||||
|
snap: ${{ steps.build.outputs.snap }}
|
||||||
|
- name: Publish Snap Package
|
||||||
|
uses: snapcore/action-publish@v1
|
||||||
|
env:
|
||||||
|
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_LOGIN }}
|
||||||
|
with:
|
||||||
|
snap: ${{ steps.build.outputs.snap }}
|
||||||
|
release: ${{ steps.prepare.outputs.RELEASE }}
|
||||||
|
|
||||||
|
discord:
|
||||||
|
name: Send Discord Notification
|
||||||
|
needs: build-snap
|
||||||
|
if: always() && !contains(github.event.head_commit.message, '[skip ci]')
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Get Build Job Status
|
||||||
|
uses: technote-space/workflow-conclusion-action@v3
|
||||||
|
- name: Combine Job Status
|
||||||
|
id: status
|
||||||
|
run: |
|
||||||
|
failures=(neutral, skipped, timed_out, action_required)
|
||||||
|
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||||
|
echo "status=failure" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
- name: Post Status to Discord
|
||||||
|
uses: sarisia/actions-status-discord@v1
|
||||||
|
with:
|
||||||
|
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
status: ${{ steps.status.outputs.status }}
|
||||||
|
title: ${{ github.workflow }}
|
||||||
|
nofail: true
|
||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -54,5 +54,19 @@ config/db/db.sqlite3-journal
|
|||||||
# VS Code
|
# VS Code
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
cypress.env.json
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
||||||
|
|
||||||
|
# ESLint
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# TS Build Info
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
# Webstorm
|
# Webstorm
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# Config Cache Directory
|
||||||
|
config/cache
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.next/
|
.next/
|
||||||
dist/
|
dist/
|
||||||
config/
|
config/
|
||||||
|
CHANGELOG.md
|
||||||
|
|
||||||
# assets
|
# assets
|
||||||
src/assets/
|
src/assets/
|
||||||
|
|||||||
5
.prettierrc.js
Normal file
5
.prettierrc.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [require('./merged-prettier-plugin.js')],
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
};
|
||||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -11,9 +11,6 @@
|
|||||||
// https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
|
// https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
|
|
||||||
// https://marketplace.visualstudio.com/items?itemName=eg2.vscode-npm-script
|
|
||||||
"eg2.vscode-npm-script",
|
|
||||||
|
|
||||||
// https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest
|
// https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest
|
||||||
"Orta.vscode-jest",
|
"Orta.vscode-jest",
|
||||||
|
|
||||||
|
|||||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -15,8 +15,9 @@
|
|||||||
"database": "./config/db/db.sqlite3"
|
"database": "./config/db/db.sqlite3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor.codeActionsOnSave": {
|
"editor.formatOnSave": true,
|
||||||
"source.organizeImports": true
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
},
|
"files.associations": {
|
||||||
"editor.formatOnSave": true
|
"globals.css": "tailwindcss"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
210
CHANGELOG.md
210
CHANGELOG.md
@@ -1,207 +1,21 @@
|
|||||||
# [1.1.1](https://github.com/fallenbagel/jellyseerr/compare/v1.1.0...v1.1.1) (2022-06-20)
|
# [1.3.0](https://github.com/fallenbagel/jellyseerr/compare/v1.2.1...v1.3.0) (2023-01-02)
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* conditional media server name for 4k url to add emby to tvdetails ([ddd773c](https://github.com/fallenbagel/jellyseerr/commit/ddd773c03ff61654490644dec21f406d03374b3d))
|
- added deep links to issues and status badges ([#3065](https://github.com/fallenbagel/jellyseerr/issues/3065)) ([bfe56c3](https://github.com/fallenbagel/jellyseerr/commit/bfe56c347073001795b1c3e917eb7a5afcc4462c))
|
||||||
* don't show 0 playcount in slideover ([dec4062](https://github.com/fallenbagel/jellyseerr/commit/dec4062cdcecbe297f72364ede6a000b863117f4))
|
- **api:** handle auth for accounts where the plex id may have been set to null ([#3125](https://github.com/fallenbagel/jellyseerr/issues/3125)) ([15e2469](https://github.com/fallenbagel/jellyseerr/commit/15e246929bdbc2b7b5bdab7a84bd7882b79d5cb1))
|
||||||
* fix mediaServerType not set for Plex which leads to Plex users seeing Jellyfin settings ([94ade93](https://github.com/fallenbagel/jellyseerr/commit/94ade93e16f02b372dafd2765bea475117431975))
|
- **api:** ignore Music,Books,Photos,MusicVideo libraries ([d9ca3c6](https://github.com/fallenbagel/jellyseerr/commit/d9ca3c6e52c118698ca71021217f6ca409e71974))
|
||||||
* fixes jellyfin forgot password and adds emby support to the forgot password link ([0259975](https://github.com/fallenbagel/jellyseerr/commit/02599754026e6a66662f753bb6b6117dfabb5f9a)), closes [#99](https://github.com/fallenbagel/jellyseerr/issues/99)
|
- count combined episodes ([64339e5](https://github.com/fallenbagel/jellyseerr/commit/64339e5f0374f8490e685e5c086e088bb7fd737e))
|
||||||
* hide plex guid cache settings from ui when running in jellyfin/emby mode ([7450138](https://github.com/fallenbagel/jellyseerr/commit/7450138ac12640797952c1a2d5e1e111d17a11e1))
|
- improved PTR scrolling performance ([#3095](https://github.com/fallenbagel/jellyseerr/issues/3095)) ([07ec3ef](https://github.com/fallenbagel/jellyseerr/commit/07ec3efbcaf669de7ccde4421c1112bfd23675d6))
|
||||||
* **import all:** fis for import all ([29478fc](https://github.com/fallenbagel/jellyseerr/commit/29478fc19534589db37499f1cdcc21ea4d389a74))
|
- **locale:** fix the duplicated wording in the Clear Media Warning message ([7e20c7c](https://github.com/fallenbagel/jellyseerr/commit/7e20c7cb78a44c32ab8a5f21203e285f23f402ab))
|
||||||
* **jellyfin:** ignore additional items with virtual location type ([c811548](https://github.com/fallenbagel/jellyseerr/commit/c81154800fd7dc48fe890f4dd57ff33cbab973bb))
|
- **ui:** adds mediaServerName to statusBadge and manageSlideOver ([d0cdce9](https://github.com/fallenbagel/jellyseerr/commit/d0cdce9e90fba642d2bf934a4266e1421424bc73)), closes [#254](https://github.com/fallenbagel/jellyseerr/issues/254)
|
||||||
* **jellyfinimportmodal:** fix for importing all jellyfin users ([a483ca9](https://github.com/fallenbagel/jellyseerr/commit/a483ca9837e12e2385d0e2407e52d6c64ae435e2))
|
- update API docs to allow 'all' seasons value ([#3073](https://github.com/fallenbagel/jellyseerr/issues/3073)) ([1dfa943](https://github.com/fallenbagel/jellyseerr/commit/1dfa9431a95e7e2a1843746c2473d8a06f03e184))
|
||||||
* **jellyfin:** sync errors ([d1dbd6e](https://github.com/fallenbagel/jellyseerr/commit/d1dbd6e3b9b1134e06150fc5eb21f729f64c0955))
|
|
||||||
* manual browser refresh would redirect to home on search page ([9ded45f](https://github.com/fallenbagel/jellyseerr/commit/9ded45fef80b4a7e0be237fbe0301629f862fff9))
|
|
||||||
* manual browser refresh would redirect to home on search page ([#2692](https://github.com/fallenbagel/jellyseerr/issues/2692)) ([b287839](https://github.com/fallenbagel/jellyseerr/commit/b2878390b486e338151f26a2354711147012f88e)), closes [#2683](https://github.com/fallenbagel/jellyseerr/issues/2683)
|
|
||||||
* only show mediaserver settings for current active mediaserver ([739f5f9](https://github.com/fallenbagel/jellyseerr/commit/739f5f9c9ade8a1680bcb374f6c9e919a9e1426c))
|
|
||||||
* **recommendations:** fixed recommendations page causing infinite network requests to tmdb api ([4f972be](https://github.com/fallenbagel/jellyseerr/commit/4f972be8584e48f544268aef9d1d05769ba2e38e))
|
|
||||||
* **recommendations:** only load more titles if there can be more than 40 ([#2749](https://github.com/fallenbagel/jellyseerr/issues/2749)) ([14519ef](https://github.com/fallenbagel/jellyseerr/commit/14519ef5559038b0d9d037a2bdc5d98e63c9db6f)), closes [#2710](https://github.com/fallenbagel/jellyseerr/issues/2710)
|
|
||||||
* remove internal Overseerr sponsor link, this is remaining on the main github page instead ([4b7bdd3](https://github.com/fallenbagel/jellyseerr/commit/4b7bdd3d7d608fe0bf52f494766fd7c40bede859))
|
|
||||||
* **scan:** ignore virtual seasons ([6574e18](https://github.com/fallenbagel/jellyseerr/commit/6574e18516201bc11b5f0c422bf6b432c722e067)), closes [#119](https://github.com/fallenbagel/jellyseerr/issues/119)
|
|
||||||
* **search:** use correct param to filter movies by year ([b07f703](https://github.com/fallenbagel/jellyseerr/commit/b07f7032ad89ccb359f3a6a4f4508de6b59ec393))
|
|
||||||
* **search:** use correct param to filter movies by year ([#2727](https://github.com/fallenbagel/jellyseerr/issues/2727)) ([1054b4e](https://github.com/fallenbagel/jellyseerr/commit/1054b4e2d7262d841fa83cde624f1138ad7bd23a))
|
|
||||||
* **setup&login:** fix a description error in the manual scan in setup and add emby to login page ([8810c20](https://github.com/fallenbagel/jellyseerr/commit/8810c20fc18a55c2f6768ddc40830a8494946072))
|
|
||||||
* **ui:** don't show 0 playcount in slideover ([#2714](https://github.com/fallenbagel/jellyseerr/issues/2714)) ([29be659](https://github.com/fallenbagel/jellyseerr/commit/29be6595125017700eccb34d33a0e852f23c97ba))
|
|
||||||
* **ui:** fix Avatar being broken when setup using internal ip ([01e81a7](https://github.com/fallenbagel/jellyseerr/commit/01e81a73a3ae3c4692d0b9b68dc27fe1a54b1a1d)), closes [#110](https://github.com/fallenbagel/jellyseerr/issues/110)
|
|
||||||
* **ui:** fix translation errors for all locales in the import plex user button ([0fb5803](https://github.com/fallenbagel/jellyseerr/commit/0fb5803eb9a7589141a63e13df9a8aa8ea4cebf2))
|
|
||||||
* **ui:** fix ui elements not reflecting the env variable ([722dda5](https://github.com/fallenbagel/jellyseerr/commit/722dda585631be365a2fb400b62dbc201f2b80de))
|
|
||||||
* **ui:** fixed translation issue where it showed as import {mediaServerName} user ([819190c](https://github.com/fallenbagel/jellyseerr/commit/819190ce98720d8d66a07c98a4f12e3c8cdcac94))
|
|
||||||
* **ui:** rectangular avatars getting stretched ([#2782](https://github.com/fallenbagel/jellyseerr/issues/2782)) ([db05172](https://github.com/fallenbagel/jellyseerr/commit/db05172d8b924a591ece4fae72d076eb59ee5f82))
|
|
||||||
* **ui:** replaced {mediaServerName} in the plex variable in NL locale ([d417fca](https://github.com/fallenbagel/jellyseerr/commit/d417fcafa1e38c6d56ed8360ae451e8b8ff82a8d))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* add Paramount+ to network slider ([d22bc09](https://github.com/fallenbagel/jellyseerr/commit/d22bc09652e5d4e703fca6838d06e4908432fe06))
|
- **api:** adds support for Mixed Libraries ([ba82ece](https://github.com/fallenbagel/jellyseerr/commit/ba82ecec5c994e79d7c9b658372041522b58a120)), closes [#95](https://github.com/fallenbagel/jellyseerr/issues/95)
|
||||||
* **api:** add issue counts endpoint ([af23a25](https://github.com/fallenbagel/jellyseerr/commit/af23a257d5795b5c3930cd3884a84a2e2eeeb1dc))
|
- custom image proxy ([#3056](https://github.com/fallenbagel/jellyseerr/issues/3056)) ([500cd1f](https://github.com/fallenbagel/jellyseerr/commit/500cd1f872942923d2b9c3b835e6329e335d4a3f))
|
||||||
* **api:** add issue counts endpoint ([#2713](https://github.com/fallenbagel/jellyseerr/issues/2713)) ([e4039d0](https://github.com/fallenbagel/jellyseerr/commit/e4039d09c0380d80f03c7a00b51a150f88c02cca))
|
- **lang:** add Croatian display language ([#3041](https://github.com/fallenbagel/jellyseerr/issues/3041)) ([64aab6d](https://github.com/fallenbagel/jellyseerr/commit/64aab6dd8240e191026512733b34cc046b6e508a))
|
||||||
* conditional media server name ([2bfdf02](https://github.com/fallenbagel/jellyseerr/commit/2bfdf02c7942762bd9f5201459b1a9ad6003b9a6))
|
|
||||||
* conditional media server name to add emby to tvdetails ([e75b71b](https://github.com/fallenbagel/jellyseerr/commit/e75b71b8168b4a661971b809c88f9910c4206545))
|
|
||||||
* conditional media server name to add emby to tvdetails ([ff3e3ce](https://github.com/fallenbagel/jellyseerr/commit/ff3e3ce841f0676713242d0c8e3a977ef65530d8))
|
|
||||||
* **discover:** add Paramount+ to network slider ([#2608](https://github.com/fallenbagel/jellyseerr/issues/2608)) ([1d00229](https://github.com/fallenbagel/jellyseerr/commit/1d00229a485bb2b376e9f63b52c70c7719f5f023))
|
|
||||||
* email ([a8bc0c0](https://github.com/fallenbagel/jellyseerr/commit/a8bc0c068b305710a224fa56a3725cc7e0758eb7)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
|
|
||||||
* **email validation:** email requirement and validation + better importer ([d835336](https://github.com/fallenbagel/jellyseerr/commit/d835336d330abfef5b15bc9febcb748a8154c7df))
|
|
||||||
* **manage slideover:** show more request override details ([#2772](https://github.com/fallenbagel/jellyseerr/issues/2772)) ([90095bb](https://github.com/fallenbagel/jellyseerr/commit/90095bb18548dfd663a78df1908c40dbf2f99faf))
|
|
||||||
* **uesrprofile:** email requirement and validation ([543859e](https://github.com/fallenbagel/jellyseerr/commit/543859e6f3b3a8cd4c61499a74bda610d3217626))
|
|
||||||
* **ui:** add emby as a mediaServerType to the import user button ([6a6bfe0](https://github.com/fallenbagel/jellyseerr/commit/6a6bfe0c6875a1d8ccb1a6fdc409f595202ef38e))
|
|
||||||
* **ui:** add emby user badge to the user list and fix local user badge ([410b536](https://github.com/fallenbagel/jellyseerr/commit/410b536c9474806ab9f7f5f097cedfafde1fbf67))
|
|
||||||
* **ui:** add emby user badge to the userProfile ([b9546e6](https://github.com/fallenbagel/jellyseerr/commit/b9546e6daa8583c60fac7961447a13715bbc7f6b))
|
|
||||||
* **ui:** conditional media server name to add emby to issuedetails play on button ([377a4fd](https://github.com/fallenbagel/jellyseerr/commit/377a4fd85b7194afb48a8ba9bfa4ce4ccf996be8))
|
|
||||||
* **ui:** conditional media server name to add emby to moviedetails ([14d2937](https://github.com/fallenbagel/jellyseerr/commit/14d293799bb37f45449c201ab03638af257623be))
|
|
||||||
* **user email setting:** added field to save user email ([30c48f1](https://github.com/fallenbagel/jellyseerr/commit/30c48f16ca0a74e7551b533bd75bc43304f946b1)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
|
|
||||||
* **user settings:** added email field to user profiel settings ([b22f20b](https://github.com/fallenbagel/jellyseerr/commit/b22f20b6fa5f68398850ccbf9b6e1cc233b3c8f4)), closes [#122](https://github.com/fallenbagel/jellyseerr/issues/122)
|
|
||||||
|
|
||||||
# [1.1.0](https://github.com/fallenbagel/jellyseerr/compare/v1.0.2...v1.1.0) (2022-05-21)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* add Discord ID setting to general user settings page ([#2406](https://github.com/fallenbagel/jellyseerr/issues/2406)) ([eff665e](https://github.com/fallenbagel/jellyseerr/commit/eff665ef4b688aac881408790304b77bd9a31ddb))
|
|
||||||
* add missing route guards to issues pages ([#2235](https://github.com/fallenbagel/jellyseerr/issues/2235)) ([c79dc9f](https://github.com/fallenbagel/jellyseerr/commit/c79dc9f70f512dbec0e3460ee78dbc9feccfbbb1))
|
|
||||||
* address unhandled promise rejections & bump node to v16.13 ([#2398](https://github.com/fallenbagel/jellyseerr/issues/2398)) ([8cba486](https://github.com/fallenbagel/jellyseerr/commit/8cba486249fed88232e93a688c8bfe0f6179c589))
|
|
||||||
* allow basic HTTP auth in hostname validation ([#2307](https://github.com/fallenbagel/jellyseerr/issues/2307)) ([d48a7ba](https://github.com/fallenbagel/jellyseerr/commit/d48a7ba518f9c79d70e499037cb730eb3efe2c08))
|
|
||||||
* **api:** return queried user's requests instead of own requests ([#2174](https://github.com/fallenbagel/jellyseerr/issues/2174)) ([0edb1f4](https://github.com/fallenbagel/jellyseerr/commit/0edb1f452b6ff4a49ae2bde15f7273769788cf4f))
|
|
||||||
* **api:** use query builder for user requests endpoint ([#2119](https://github.com/fallenbagel/jellyseerr/issues/2119)) ([a20f395](https://github.com/fallenbagel/jellyseerr/commit/a20f395c94c97dd7ddbc25590f15def2c9bf13c9))
|
|
||||||
* apply request overrides iff override & selected servers match ([#2164](https://github.com/fallenbagel/jellyseerr/issues/2164)) ([50ce198](https://github.com/fallenbagel/jellyseerr/commit/50ce198471b1a3777a183d68904bbfb39ebd4523))
|
|
||||||
* **auth:** resolve local/password authentication issues ([#2677](https://github.com/fallenbagel/jellyseerr/issues/2677)) ([b75fc7b](https://github.com/fallenbagel/jellyseerr/commit/b75fc7b2384ce760432620faaa92277dcd42b8e1))
|
|
||||||
* **css:** rename form-input to form-input-area ([#2613](https://github.com/fallenbagel/jellyseerr/issues/2613)) ([086f0b6](https://github.com/fallenbagel/jellyseerr/commit/086f0b6ce23f607d20c2cec3c73b2e4d1ce9b426))
|
|
||||||
* disable user-import from mediaserver for non-plex mediaservers until implemented ([4db8e54](https://github.com/fallenbagel/jellyseerr/commit/4db8e5464d6ce3450e2687a0cbee126961d847d2))
|
|
||||||
* **docker:** explicitly install python3 ([#2273](https://github.com/fallenbagel/jellyseerr/issues/2273)) [skip ci] ([f1cd087](https://github.com/fallenbagel/jellyseerr/commit/f1cd0878a5c74bddc864f5f8ce9e2f041bdde5ec))
|
|
||||||
* don't allow login for unimported Jellyfin users if not set in settings ([72ca694](https://github.com/fallenbagel/jellyseerr/commit/72ca694f212ab616ca7b7fe02e428ff61f79c67c))
|
|
||||||
* **email:** do not attempt to display logo if app URL not configured ([#2125](https://github.com/fallenbagel/jellyseerr/issues/2125)) ([b3b421a](https://github.com/fallenbagel/jellyseerr/commit/b3b421a67408a4a48d23c15341fcdf7aaf19b25a))
|
|
||||||
* **email:** enclose PGP encryption logic in try/catch ([#2519](https://github.com/fallenbagel/jellyseerr/issues/2519)) ([a76b608](https://github.com/fallenbagel/jellyseerr/commit/a76b608ab796944c0c660e3296a7aca6615d69f3))
|
|
||||||
* **email:** use decrypted private key ([#2232](https://github.com/fallenbagel/jellyseerr/issues/2232)) ([8d29685](https://github.com/fallenbagel/jellyseerr/commit/8d2968572a569ed77a4d7c14ae1dc69935fa847e))
|
|
||||||
* fix usertype from local user to mediaServerType ([25bee8b](https://github.com/fallenbagel/jellyseerr/commit/25bee8b9f70d7948191ba9cf07d16427da81d425))
|
|
||||||
* **frontend:** disable autocomplete on search field ([#2592](https://github.com/fallenbagel/jellyseerr/issues/2592)) ([82d1617](https://github.com/fallenbagel/jellyseerr/commit/82d16177bf763fe8097b4aae326793e3e21e847d))
|
|
||||||
* **frontend:** more issues-related fixes ([#2234](https://github.com/fallenbagel/jellyseerr/issues/2234)) ([3ec4a9c](https://github.com/fallenbagel/jellyseerr/commit/3ec4a9c76e1f31bee5c8801b389721bf8e5884e0))
|
|
||||||
* **frontend:** notification type validation ([#2207](https://github.com/fallenbagel/jellyseerr/issues/2207)) ([2f204b9](https://github.com/fallenbagel/jellyseerr/commit/2f204b995269a53ae36f2a8733f27ae6ab70da5a))
|
|
||||||
* **frontend:** setup page backdrops ([#2251](https://github.com/fallenbagel/jellyseerr/issues/2251)) ([78a8091](https://github.com/fallenbagel/jellyseerr/commit/78a8091bcd29a7cf50cc7c493c28710389817adf))
|
|
||||||
* **frontend:** theme-color meta tag ([#2420](https://github.com/fallenbagel/jellyseerr/issues/2420)) ([ff28c9b](https://github.com/fallenbagel/jellyseerr/commit/ff28c9bfebf4a930e2542ee3b3c35f8af4e1b97e))
|
|
||||||
* **frontend:** use consistent formatting & strings ([#2231](https://github.com/fallenbagel/jellyseerr/issues/2231)) ([2164471](https://github.com/fallenbagel/jellyseerr/commit/216447121b686b6d01a31b95ec0c8eb005f6b103))
|
|
||||||
* **frontend:** various fixes ([#2524](https://github.com/fallenbagel/jellyseerr/issues/2524)) ([c3dbd0d](https://github.com/fallenbagel/jellyseerr/commit/c3dbd0d6913946e0e1b5308edfbb5ca744740223))
|
|
||||||
* handle Plex library settings migration failure gracefully ([#2254](https://github.com/fallenbagel/jellyseerr/issues/2254)) ([ed53810](https://github.com/fallenbagel/jellyseerr/commit/ed53810fb33f70722361c67d176ff4edf531ba45))
|
|
||||||
* **holiday:** remove special holiday slider ([22f2037](https://github.com/fallenbagel/jellyseerr/commit/22f2037ea6c5a0ba2ffa4d69f2b7cf42bdcf8575))
|
|
||||||
* **issues:** only allow edit of own comments & do not allow non-admin delete of issues with comments ([#2248](https://github.com/fallenbagel/jellyseerr/issues/2248)) ([bba09d6](https://github.com/fallenbagel/jellyseerr/commit/bba09d69c1bc55c2f35db5a7986e7c935cc9619c))
|
|
||||||
* jellyfin user signin after manual user import ([36c3c9d](https://github.com/fallenbagel/jellyseerr/commit/36c3c9d7c60176a5c4090b86313743b3ce433406))
|
|
||||||
* **lang:** add missing string ([#2370](https://github.com/fallenbagel/jellyseerr/issues/2370)) ([d36c1d2](https://github.com/fallenbagel/jellyseerr/commit/d36c1d29295020efb76bac21a443b6f9049802f3))
|
|
||||||
* **lang:** rename 'Media' notification types for clarity ([#2400](https://github.com/fallenbagel/jellyseerr/issues/2400)) ([399b037](https://github.com/fallenbagel/jellyseerr/commit/399b0379186ed34dcc436bd95330fd1a05fef4b3))
|
|
||||||
* **lang:** string edits ([#2229](https://github.com/fallenbagel/jellyseerr/issues/2229)) ([ab20c21](https://github.com/fallenbagel/jellyseerr/commit/ab20c21184639e1c7725f7cae96249c6fa157351))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2625](https://github.com/fallenbagel/jellyseerr/issues/2625)) ([19cdedd](https://github.com/fallenbagel/jellyseerr/commit/19cdedd2a6656b1a852e1cc653bbdb140e978b51))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2639](https://github.com/fallenbagel/jellyseerr/issues/2639)) ([418a533](https://github.com/fallenbagel/jellyseerr/commit/418a533588bbbdbbbb4caee1ef91d57c1ca35717))
|
|
||||||
* **lang:** translations update from Weblate ([#2212](https://github.com/fallenbagel/jellyseerr/issues/2212)) ([85aec4f](https://github.com/fallenbagel/jellyseerr/commit/85aec4f8925746ebae9bcc99d8480b78ccfd851e))
|
|
||||||
* **logs:** handle log message nested extra properties ([#2459](https://github.com/fallenbagel/jellyseerr/issues/2459)) ([d777940](https://github.com/fallenbagel/jellyseerr/commit/d7779408d162949b2eafcacefc8eabe53fae229f))
|
|
||||||
* **logs:** handle unexpected log messages ([#2303](https://github.com/fallenbagel/jellyseerr/issues/2303)) ([f284e4a](https://github.com/fallenbagel/jellyseerr/commit/f284e4ab978e502d2cc08e76226a8ebac91bb48f))
|
|
||||||
* **logs:** lazily parse log message label ([#2359](https://github.com/fallenbagel/jellyseerr/issues/2359)) ([5af06bd](https://github.com/fallenbagel/jellyseerr/commit/5af06bd87226fbc6176b0c5e362824793165a34e))
|
|
||||||
* **notif:** correct issue notif action URLs ([#2333](https://github.com/fallenbagel/jellyseerr/issues/2333)) ([dc7f959](https://github.com/fallenbagel/jellyseerr/commit/dc7f959cb422a8d89bcebc78377f1513412e542c))
|
|
||||||
* **notif:** duplicate notification check logic ([#2424](https://github.com/fallenbagel/jellyseerr/issues/2424)) ([10651ba](https://github.com/fallenbagel/jellyseerr/commit/10651baa675993f7109989bbac67f54661c8693f))
|
|
||||||
* **notif:** only send MEDIA_AVAILABLE notifications for non-declined requests ([#2343](https://github.com/fallenbagel/jellyseerr/issues/2343)) ([fcb0dcf](https://github.com/fallenbagel/jellyseerr/commit/fcb0dcf5be64bf9ca814bfe119586908922099c5))
|
|
||||||
* **notif:** show event in pop up notification for slack ([#2413](https://github.com/fallenbagel/jellyseerr/issues/2413)) ([d4438c8](https://github.com/fallenbagel/jellyseerr/commit/d4438c82e3753c9b29b6269ad406d263b3fcef4c)), closes [#2408](https://github.com/fallenbagel/jellyseerr/issues/2408)
|
|
||||||
* only run scheduled mediaserver jobs that apply to the current mediaserver ([791106a](https://github.com/fallenbagel/jellyseerr/commit/791106a7f5b8356b67119300bad245f587f6dc5f))
|
|
||||||
* play on Jellyfin for TV shows ([d0c5481](https://github.com/fallenbagel/jellyseerr/commit/d0c5481d22ddceee0b5c3d7d82029f44c46dbbd0))
|
|
||||||
* plex Login ([9d54776](https://github.com/fallenbagel/jellyseerr/commit/9d54776a2c4c23a61d5e619ca952b9e5d947a79b))
|
|
||||||
* **plex:** correctly generate uuid for safari ([#2614](https://github.com/fallenbagel/jellyseerr/issues/2614)) ([d06f2cd](https://github.com/fallenbagel/jellyseerr/commit/d06f2cdb08bfa6f05cf7cec2c408a258fa926b09))
|
|
||||||
* **plex:** find TV series in addition to movies from IMDb IDs ([#1830](https://github.com/fallenbagel/jellyseerr/issues/1830)) ([30644f6](https://github.com/fallenbagel/jellyseerr/commit/30644f65ea2e8437676422ae0b083c642a836887))
|
|
||||||
* **plex:** include 'Overseerr' in X-Plex-Device-Name header ([#2635](https://github.com/fallenbagel/jellyseerr/issues/2635)) ([d4f9650](https://github.com/fallenbagel/jellyseerr/commit/d4f9650cd07704a97f8b591b7de7351c1e85b825))
|
|
||||||
* **plex:** use unique client identifier ([#2602](https://github.com/fallenbagel/jellyseerr/issues/2602)) ([648b346](https://github.com/fallenbagel/jellyseerr/commit/648b346cbe5a941c7e1ec4ddfb276fb0e27ed502))
|
|
||||||
* **plex:** user import ([#2442](https://github.com/fallenbagel/jellyseerr/issues/2442)) ([86dff12](https://github.com/fallenbagel/jellyseerr/commit/86dff12cdeef6dca92527dd31757a3a4c7f921bf))
|
|
||||||
* **radarr:** correctly check for existing movies ([#2490](https://github.com/fallenbagel/jellyseerr/issues/2490)) ([5d4b06b](https://github.com/fallenbagel/jellyseerr/commit/5d4b06bbcc6cf6d328f6b4a86c4c0f9b0f3aff3e))
|
|
||||||
* **radarr:** remove PreDB minimum availability option ([#2386](https://github.com/fallenbagel/jellyseerr/issues/2386)) ([3e5eb4e](https://github.com/fallenbagel/jellyseerr/commit/3e5eb4e148a9f88b871abc4ee1784b870f691534))
|
|
||||||
* relax jellyfin url validation to allow local domains ([3a010f8](https://github.com/fallenbagel/jellyseerr/commit/3a010f821189414efd334b4cad2a300501f40a18))
|
|
||||||
* replaced unknown job with jellyfin in jobsandcache and added translations for it ([f09b86a](https://github.com/fallenbagel/jellyseerr/commit/f09b86aa87d84af1ddee07390a04dd8543cff8a6))
|
|
||||||
* **requests:** check for existing media of same type when requesting ([#2445](https://github.com/fallenbagel/jellyseerr/issues/2445)) ([eb9ca2e](https://github.com/fallenbagel/jellyseerr/commit/eb9ca2e86f3be3f4ff8ee2e7c4aecdf337d8976d))
|
|
||||||
* **requests:** do not fail request edits if acting user lacks Manage Users permission ([#2338](https://github.com/fallenbagel/jellyseerr/issues/2338)) ([91bfff7](https://github.com/fallenbagel/jellyseerr/commit/91bfff71b7c05c9b9aad2c95282533eefbb6b2e7))
|
|
||||||
* **scripts:** update migration scripts ([#2208](https://github.com/fallenbagel/jellyseerr/issues/2208)) [skip ci] ([d0ac74e](https://github.com/fallenbagel/jellyseerr/commit/d0ac74ea4bbfcf3d25d30cbd422d9df1c1259a18))
|
|
||||||
* secure session cookie ([#2308](https://github.com/fallenbagel/jellyseerr/issues/2308)) ([7f330af](https://github.com/fallenbagel/jellyseerr/commit/7f330aff2e1d3546e8dd1a3e4b037b9beb1cc7f0))
|
|
||||||
* **servarr:** handle baseurl error when testing connection ([#2294](https://github.com/fallenbagel/jellyseerr/issues/2294)) ([93b5ea2](https://github.com/fallenbagel/jellyseerr/commit/93b5ea20ca590996f6dc90713a76800180d0621c))
|
|
||||||
* **servarr:** handle servaarr server being unavailable when scanning downloads ([#2358](https://github.com/fallenbagel/jellyseerr/issues/2358)) ([488874f](https://github.com/fallenbagel/jellyseerr/commit/488874fc17e4e4719e90d383b83b1e1a5217213b))
|
|
||||||
* **sonarr:** monitor existing series upon request approval ([#2553](https://github.com/fallenbagel/jellyseerr/issues/2553)) ([aa062d9](https://github.com/fallenbagel/jellyseerr/commit/aa062d921c425d4b64bfdb28a5f102b0c92f7d87))
|
|
||||||
* **sonarr:** only scan seasons that exist in TMDb ([#2523](https://github.com/fallenbagel/jellyseerr/issues/2523)) ([6168185](https://github.com/fallenbagel/jellyseerr/commit/61681857b123802aaeff02a8f61b1ba046c5d333))
|
|
||||||
* sort collection parts by release date ([#2368](https://github.com/fallenbagel/jellyseerr/issues/2368)) ([1b3797c](https://github.com/fallenbagel/jellyseerr/commit/1b3797cf6e6ef6b3d8c81e644382f6e3f68cfaaa))
|
|
||||||
* **tautulli:** fetch additional user history as necessary to return 20 unique media ([#2446](https://github.com/fallenbagel/jellyseerr/issues/2446)) ([7d19de6](https://github.com/fallenbagel/jellyseerr/commit/7d19de6a4af6297be18140ca59402b40f7bbb30b))
|
|
||||||
* **ui:** Fix webhook URL validation regex ([#864](https://github.com/fallenbagel/jellyseerr/issues/864)) ([726f62b](https://github.com/fallenbagel/jellyseerr/commit/726f62b9b69b5078e718f129e26abdf358f5cb06))
|
|
||||||
* **ui:** refinements for 'About' page ([#2173](https://github.com/fallenbagel/jellyseerr/issues/2173)) ([084a842](https://github.com/fallenbagel/jellyseerr/commit/084a842a4f9b6caaed22edbe77bc9e414bc1f387))
|
|
||||||
* **ui:** request badge styling in request list ([#2302](https://github.com/fallenbagel/jellyseerr/issues/2302)) ([f2375c9](https://github.com/fallenbagel/jellyseerr/commit/f2375c902b79dcb1f349500862775ae57ea7d406))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **about:** show config directory ([#2600](https://github.com/fallenbagel/jellyseerr/issues/2600)) ([0c7373c](https://github.com/fallenbagel/jellyseerr/commit/0c7373c7e89a4ff717efaa7d6a5854f7ccd6a8d3))
|
|
||||||
* add emby detail url support ([88c2c5e](https://github.com/fallenbagel/jellyseerr/commit/88c2c5ebcddd1eb8aea4a4e72c68a91197dec065))
|
|
||||||
* add production countries to movie/TV detail pages ([#2170](https://github.com/fallenbagel/jellyseerr/issues/2170)) ([30b20df](https://github.com/fallenbagel/jellyseerr/commit/30b20df37a9604ba1c066f89e54a5482a09575ea))
|
|
||||||
* add quotas, advanced options, and toggles to collection request modal ([#1742](https://github.com/fallenbagel/jellyseerr/issues/1742)) ([af40212](https://github.com/fallenbagel/jellyseerr/commit/af40212a738f8d6d9a5bf26dc20c0c87780d6020))
|
|
||||||
* allow Jellyfin to set a playback URL different to the Jellyfin host specified during setup ([9fbc407](https://github.com/fallenbagel/jellyseerr/commit/9fbc4074e491bbeba7880fd54c99d4e3c95c7d01))
|
|
||||||
* **api:** add additional request counts ([#2426](https://github.com/fallenbagel/jellyseerr/issues/2426)) ([2535edc](https://github.com/fallenbagel/jellyseerr/commit/2535edcc7fd6ec66fd45ad754c03929f1fe94871))
|
|
||||||
* **discord:** add 'Enable Mentions' setting ([#1779](https://github.com/fallenbagel/jellyseerr/issues/1779)) ([5f7538a](https://github.com/fallenbagel/jellyseerr/commit/5f7538ae2bf9c6e2feea385cc299bd08df071218))
|
|
||||||
* display release dates for theatrical, digital, and physical release types ([#1492](https://github.com/fallenbagel/jellyseerr/issues/1492)) ([a4dca23](https://github.com/fallenbagel/jellyseerr/commit/a4dca2356b7605026f7bc45b691496e765c3328c))
|
|
||||||
* dynamically fetch login screen backdrop images ([#2206](https://github.com/fallenbagel/jellyseerr/issues/2206)) ([3486d0b](https://github.com/fallenbagel/jellyseerr/commit/3486d0bf5520cbdff60bd8fd023caed76c452973))
|
|
||||||
* **frontend:** add Discovery+ to network slider ([#2345](https://github.com/fallenbagel/jellyseerr/issues/2345)) ([2ded8f5](https://github.com/fallenbagel/jellyseerr/commit/2ded8f5484168bd7b8f45124d9ebdd296a5708d5))
|
|
||||||
* **frontend:** add Hulu to network slider ([#2204](https://github.com/fallenbagel/jellyseerr/issues/2204)) ([1e402f7](https://github.com/fallenbagel/jellyseerr/commit/1e402f710b53c11855aab0abdb4b12c51c30b022))
|
|
||||||
* **frontend:** open media management slideover on status badge click ([#2407](https://github.com/fallenbagel/jellyseerr/issues/2407)) ([1f5785d](https://github.com/fallenbagel/jellyseerr/commit/1f5785d6c53b2ca2da67a8ccee72165c052c61a1))
|
|
||||||
* implement import users from Jellyfin button ([9e2f3f0](https://github.com/fallenbagel/jellyseerr/commit/9e2f3f06393e71ba5d1c0ba3c9512b64a3ce3ad7))
|
|
||||||
* initialize Jellyfin/Emby users with local login ([103350f](https://github.com/fallenbagel/jellyseerr/commit/103350fe146fbf212b12a3348bcfb40399e1a0fc))
|
|
||||||
* issues ([#2180](https://github.com/fallenbagel/jellyseerr/issues/2180)) ([e402c42](https://github.com/fallenbagel/jellyseerr/commit/e402c42aaa7d795cd724856a2e23615bb1a3695d))
|
|
||||||
* **jobs:** allow modifying job schedules ([#1440](https://github.com/fallenbagel/jellyseerr/issues/1440)) ([82614ca](https://github.com/fallenbagel/jellyseerr/commit/82614ca4410782a12d65b4c0a6526ff064be1241))
|
|
||||||
* **lang:** add Albanian display language ([#2605](https://github.com/fallenbagel/jellyseerr/issues/2605)) ([3d32462](https://github.com/fallenbagel/jellyseerr/commit/3d32462f50b4ced0d9205b79003c35d6d1c948a3))
|
|
||||||
* **lang:** add Czech and Danish display languages ([#2176](https://github.com/fallenbagel/jellyseerr/issues/2176)) ([8d8db6c](https://github.com/fallenbagel/jellyseerr/commit/8d8db6cf5d98d4e498a31db339d02f8a98057c8d))
|
|
||||||
* **lang:** add Polish display language ([#2261](https://github.com/fallenbagel/jellyseerr/issues/2261)) ([c760cea](https://github.com/fallenbagel/jellyseerr/commit/c760ceaa5f36c77fa3ce320fae1b4597d2d8b976))
|
|
||||||
* **lang:** translated using Weblate (Chinese (Traditional)) ([#2272](https://github.com/fallenbagel/jellyseerr/issues/2272)) ([d401e33](https://github.com/fallenbagel/jellyseerr/commit/d401e33249cbbca6e707479e5f0207e298ef3248))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2277](https://github.com/fallenbagel/jellyseerr/issues/2277)) ([92732fc](https://github.com/fallenbagel/jellyseerr/commit/92732fcb42c56242d16daab00e2d38740b92dea0))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2315](https://github.com/fallenbagel/jellyseerr/issues/2315)) ([6245be1](https://github.com/fallenbagel/jellyseerr/commit/6245be1e10dda67c869b59522c1290e7c100145f))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2320](https://github.com/fallenbagel/jellyseerr/issues/2320)) ([68112fa](https://github.com/fallenbagel/jellyseerr/commit/68112faefbd64d5c71d3eff21620767f88ccfc34))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2325](https://github.com/fallenbagel/jellyseerr/issues/2325)) ([febf067](https://github.com/fallenbagel/jellyseerr/commit/febf0677b880d2fed2822ce510db7cbb0826a920))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2336](https://github.com/fallenbagel/jellyseerr/issues/2336)) ([3f7ef7a](https://github.com/fallenbagel/jellyseerr/commit/3f7ef7af97a807ef38041f4f2642b565aa33d066))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2341](https://github.com/fallenbagel/jellyseerr/issues/2341)) ([33fe0bd](https://github.com/fallenbagel/jellyseerr/commit/33fe0bdd1e00da40e85b4e4b4780134b31a105d2))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2346](https://github.com/fallenbagel/jellyseerr/issues/2346)) ([50dc934](https://github.com/fallenbagel/jellyseerr/commit/50dc9341dd98cb2d8ef3ef6471882a5a9b060afa))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2364](https://github.com/fallenbagel/jellyseerr/issues/2364)) ([d437cc2](https://github.com/fallenbagel/jellyseerr/commit/d437cc25392e9c0881888371ffabc82892a1b15c))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2366](https://github.com/fallenbagel/jellyseerr/issues/2366)) ([cc2b2bc](https://github.com/fallenbagel/jellyseerr/commit/cc2b2bc7a8ecd89e1feb38a907596b16df9bf0fc))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2374](https://github.com/fallenbagel/jellyseerr/issues/2374)) ([b9bedac](https://github.com/fallenbagel/jellyseerr/commit/b9bedac7d7ba85223ecf1d9b93b96e2a490d571a))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2379](https://github.com/fallenbagel/jellyseerr/issues/2379)) ([bd93168](https://github.com/fallenbagel/jellyseerr/commit/bd93168ba1ed650baf4024569bb6a76811a99820))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2389](https://github.com/fallenbagel/jellyseerr/issues/2389)) ([d2241a4](https://github.com/fallenbagel/jellyseerr/commit/d2241a41877d126a802fc53c925d258af31f34fd))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2404](https://github.com/fallenbagel/jellyseerr/issues/2404)) ([1b29b15](https://github.com/fallenbagel/jellyseerr/commit/1b29b15d7c9a7ec918cb59116d60e1ae2e797dc4))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2405](https://github.com/fallenbagel/jellyseerr/issues/2405)) ([879df20](https://github.com/fallenbagel/jellyseerr/commit/879df20022c8c5d9b32858ac5499d3e4369fc064))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2414](https://github.com/fallenbagel/jellyseerr/issues/2414)) ([88536b1](https://github.com/fallenbagel/jellyseerr/commit/88536b1f9d6e8c1a11e1adf91b85bab4f34b751c))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2425](https://github.com/fallenbagel/jellyseerr/issues/2425)) ([e9d4b63](https://github.com/fallenbagel/jellyseerr/commit/e9d4b6327b50a005ee6c2c3292b6f107e90fc50c))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2428](https://github.com/fallenbagel/jellyseerr/issues/2428)) ([f8b1bcc](https://github.com/fallenbagel/jellyseerr/commit/f8b1bccda44371bb6f3f8f4ceeab900b1df3de31))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2436](https://github.com/fallenbagel/jellyseerr/issues/2436)) ([99c0407](https://github.com/fallenbagel/jellyseerr/commit/99c04072e9f7be8191f25cbcfd5103017b8796eb))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2452](https://github.com/fallenbagel/jellyseerr/issues/2452)) ([b5bd6ee](https://github.com/fallenbagel/jellyseerr/commit/b5bd6ee78f3d4aa14f0c440d1f2a8323dccfa399))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2457](https://github.com/fallenbagel/jellyseerr/issues/2457)) ([92b2d32](https://github.com/fallenbagel/jellyseerr/commit/92b2d32d2e1e1d319410a9e357e1304065a77598))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2489](https://github.com/fallenbagel/jellyseerr/issues/2489)) ([ec08fa6](https://github.com/fallenbagel/jellyseerr/commit/ec08fa67934715ff4a4d618d5b9ff97853913b78))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2508](https://github.com/fallenbagel/jellyseerr/issues/2508)) ([9f4ae34](https://github.com/fallenbagel/jellyseerr/commit/9f4ae34da76707a40e2c89a50c722ffa1c0327c0))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2531](https://github.com/fallenbagel/jellyseerr/issues/2531)) ([54b32eb](https://github.com/fallenbagel/jellyseerr/commit/54b32ebfd6b2eb6aeeea98c25939166eda8cc17f))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2541](https://github.com/fallenbagel/jellyseerr/issues/2541)) ([4549ed3](https://github.com/fallenbagel/jellyseerr/commit/4549ed389e4f25c0946dc01526387e5ac000c3cf))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2611](https://github.com/fallenbagel/jellyseerr/issues/2611)) ([81c75c8](https://github.com/fallenbagel/jellyseerr/commit/81c75c800edf6d36a1082a291ef7e308f338d005))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2629](https://github.com/fallenbagel/jellyseerr/issues/2629)) ([1d0cbd2](https://github.com/fallenbagel/jellyseerr/commit/1d0cbd2e761072be0b4b3de461397ad9f9f681f3))
|
|
||||||
* **lang:** translations update from Hosted Weblate ([#2645](https://github.com/fallenbagel/jellyseerr/issues/2645)) ([341e3b8](https://github.com/fallenbagel/jellyseerr/commit/341e3b8f0657e09f53ad0b813b051290947343c0))
|
|
||||||
* **lang:** translations update from Weblate ([#2101](https://github.com/fallenbagel/jellyseerr/issues/2101)) ([c73cf7b](https://github.com/fallenbagel/jellyseerr/commit/c73cf7b19cbc19e97a777c0facb9264fb0113093))
|
|
||||||
* **lang:** translations update from Weblate ([#2179](https://github.com/fallenbagel/jellyseerr/issues/2179)) ([e3312ce](https://github.com/fallenbagel/jellyseerr/commit/e3312cef33821c8cb76a4a63bd565c78d67b3e0b))
|
|
||||||
* **lang:** translations update from Weblate ([#2185](https://github.com/fallenbagel/jellyseerr/issues/2185)) ([dce10f7](https://github.com/fallenbagel/jellyseerr/commit/dce10f743f52cb04036e2cdaee280e26a81b253b))
|
|
||||||
* **lang:** translations update from Weblate ([#2202](https://github.com/fallenbagel/jellyseerr/issues/2202)) ([492d8e3](https://github.com/fallenbagel/jellyseerr/commit/492d8e3daa5fb99aa9df2a18978085d5ddd581e7))
|
|
||||||
* **lang:** translations update from Weblate ([#2210](https://github.com/fallenbagel/jellyseerr/issues/2210)) ([0a6ef6c](https://github.com/fallenbagel/jellyseerr/commit/0a6ef6cc81376f7a02f1483109be7ae4ab851c48))
|
|
||||||
* **lang:** translations update from Weblate ([#2226](https://github.com/fallenbagel/jellyseerr/issues/2226)) ([62b3dc5](https://github.com/fallenbagel/jellyseerr/commit/62b3dc5471c28f4d0e4399cb3bc8bfab94cff5ea))
|
|
||||||
* **lang:** translations update from Weblate ([#2241](https://github.com/fallenbagel/jellyseerr/issues/2241)) ([2b0b8e0](https://github.com/fallenbagel/jellyseerr/commit/2b0b8e05d9c95ff9218cea858a920a2815871186))
|
|
||||||
* **lang:** translations update from Weblate ([#2244](https://github.com/fallenbagel/jellyseerr/issues/2244)) ([0828b00](https://github.com/fallenbagel/jellyseerr/commit/0828b008badc8b512316799a6787bb7c403658d5))
|
|
||||||
* **lang:** translations update from Weblate ([#2247](https://github.com/fallenbagel/jellyseerr/issues/2247)) ([8c49309](https://github.com/fallenbagel/jellyseerr/commit/8c49309c35c31f7bcd0b84b0a307febc16842f68))
|
|
||||||
* **lang:** translations update from Weblate ([#2252](https://github.com/fallenbagel/jellyseerr/issues/2252)) ([99d5000](https://github.com/fallenbagel/jellyseerr/commit/99d50004e58f6b4594df0a171f6bc668635ec50c))
|
|
||||||
* **lang:** translations update from Weblate ([#2265](https://github.com/fallenbagel/jellyseerr/issues/2265)) ([b1b367a](https://github.com/fallenbagel/jellyseerr/commit/b1b367aac625ed3eb865832c94c2352e5a5c40f5))
|
|
||||||
* **logs:** use separate json file to parse logs for log viewer ([#2399](https://github.com/fallenbagel/jellyseerr/issues/2399)) ([ce31bef](https://github.com/fallenbagel/jellyseerr/commit/ce31bef8a125c5492f2a1cfef0dcf3d8a4e9ee11))
|
|
||||||
* **notif:** 4K media notifications ([#2324](https://github.com/fallenbagel/jellyseerr/issues/2324)) ([88a8c1a](https://github.com/fallenbagel/jellyseerr/commit/88a8c1aa596e1113d6da52e5e8cbe443abc6384f))
|
|
||||||
* **notif:** add Gotify agent ([#2196](https://github.com/fallenbagel/jellyseerr/issues/2196)) ([e0b6abe](https://github.com/fallenbagel/jellyseerr/commit/e0b6abe4796f5a324c0ff78cff317fcaead671f1)), closes [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2077](https://github.com/fallenbagel/jellyseerr/issues/2077) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2077](https://github.com/fallenbagel/jellyseerr/issues/2077) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183) [#2183](https://github.com/fallenbagel/jellyseerr/issues/2183)
|
|
||||||
* **notif:** add Pushbullet and Pushover agents to user notification settings ([#1740](https://github.com/fallenbagel/jellyseerr/issues/1740)) ([aeb7a48](https://github.com/fallenbagel/jellyseerr/commit/aeb7a48d72cec3fa2b857030aad3eaa0a457a896))
|
|
||||||
* **notif:** add Pushbullet channel tag ([#2198](https://github.com/fallenbagel/jellyseerr/issues/2198)) ([f9200b7](https://github.com/fallenbagel/jellyseerr/commit/f9200b7977208f9b8267ce3a74bd8a86d6f28f7b))
|
|
||||||
* **notif:** issue notifications ([#2242](https://github.com/fallenbagel/jellyseerr/issues/2242)) ([c9ffac3](https://github.com/fallenbagel/jellyseerr/commit/c9ffac33f7c04d926f8c45295703689d42fe87af))
|
|
||||||
* **plex:** selective user import ([#2188](https://github.com/fallenbagel/jellyseerr/issues/2188)) ([9cb97db](https://github.com/fallenbagel/jellyseerr/commit/9cb97db13ced5df2dc595cd9033470b1a0750093))
|
|
||||||
* remove email requirement for jellyfin/emby non-admin users ([3e1e11d](https://github.com/fallenbagel/jellyseerr/commit/3e1e11d9d93e5d055c92989361a3ced3b77b1d39))
|
|
||||||
* **search:** close search bar when hitting return ([#2260](https://github.com/fallenbagel/jellyseerr/issues/2260)) ([b423dc1](https://github.com/fallenbagel/jellyseerr/commit/b423dc167d12f0ba49f902876bceb2e876e35f58))
|
|
||||||
* **search:** filter search results by year ([#2460](https://github.com/fallenbagel/jellyseerr/issues/2460)) ([72c825d](https://github.com/fallenbagel/jellyseerr/commit/72c825d2a5109688bcc1991a30249284bf281500))
|
|
||||||
* **search:** search by id ([#2082](https://github.com/fallenbagel/jellyseerr/issues/2082)) ([b31cdbf](https://github.com/fallenbagel/jellyseerr/commit/b31cdbf074d5dbecbbf6da135a9b686aea9e3c0e))
|
|
||||||
* **servarr:** auto fill base url when testing service if missing ([#1995](https://github.com/fallenbagel/jellyseerr/issues/1995)) ([739f667](https://github.com/fallenbagel/jellyseerr/commit/739f667b54d8dec258b74d0cd8fd8b3b88dcf8d5))
|
|
||||||
* Tautulli integration ([#2230](https://github.com/fallenbagel/jellyseerr/issues/2230)) ([0842c23](https://github.com/fallenbagel/jellyseerr/commit/0842c233d0fc56d44824cad18749492cd52cbed5))
|
|
||||||
* **tautulli:** validate upon saving settings ([#2511](https://github.com/fallenbagel/jellyseerr/issues/2511)) ([1dc900d](https://github.com/fallenbagel/jellyseerr/commit/1dc900d5ce9689d179c9d2f554abc74ca50bd9cb))
|
|
||||||
* **ui:** add trakt external link ([#2367](https://github.com/fallenbagel/jellyseerr/issues/2367)) ([4e56bae](https://github.com/fallenbagel/jellyseerr/commit/4e56bae98508c1a60aeb3a08560ba1c00acce7e7))
|
|
||||||
* **ui:** allow admins to edit & approve request from advanced request modal ([#2067](https://github.com/fallenbagel/jellyseerr/issues/2067)) ([340f1a2](https://github.com/fallenbagel/jellyseerr/commit/340f1a211952bd2e8f40f0ea4622b52dbe934e85))
|
|
||||||
* **ui:** link processing/requested status badges to service URL ([#1761](https://github.com/fallenbagel/jellyseerr/issues/1761)) ([032c14a](https://github.com/fallenbagel/jellyseerr/commit/032c14a22680f62f8106943297b081b68645ce61))
|
|
||||||
* verify Plex server access during auth for existing users with Plex IDs ([#2458](https://github.com/fallenbagel/jellyseerr/issues/2458)) ([85bb30e](https://github.com/fallenbagel/jellyseerr/commit/85bb30e252c27047ae367491f0e5bb92a7d52605))
|
|
||||||
|
|
||||||
## [1.29.1](https://github.com/sct/overseerr/compare/v1.29.0...v1.29.1) (2022-04-06)
|
## [1.29.1](https://github.com/sct/overseerr/compare/v1.29.0...v1.29.1) (2022-04-06)
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ 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.
|
1. Be concise and clear, and use as few words as possible to make your point.
|
||||||
2. Use the Oxford comma where appropriate.
|
2. Use the Oxford comma where appropriate.
|
||||||
3. Use the appropriate Unicode characters for ellipses, arrows, and other special characters/symbols.
|
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'.
|
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., IMDb has a lowercase 'b', whereas TMDB and TheTVDB have 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).
|
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.
|
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.
|
7. Ensure that toast notification strings are complete sentences ending in punctuation.
|
||||||
|
|||||||
18
Dockerfile
18
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16.14-alpine AS BUILD_IMAGE
|
FROM node:18.18-alpine AS BUILD_IMAGE
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -7,14 +7,15 @@ ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64}
|
|||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
case "${TARGETPLATFORM}" in \
|
case "${TARGETPLATFORM}" in \
|
||||||
'linux/arm64' | 'linux/arm/v7') \
|
'linux/arm64' | 'linux/arm/v7') \
|
||||||
apk add --no-cache python3 make g++ && \
|
apk update && \
|
||||||
ln -s /usr/bin/python3 /usr/bin/python \
|
apk add --no-cache python3 make g++ gcc libc6-compat bash && \
|
||||||
;; \
|
yarn global add node-gyp \
|
||||||
|
;; \
|
||||||
esac
|
esac
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
RUN yarn install --frozen-lockfile --network-timeout 1000000
|
RUN CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000
|
||||||
|
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
@@ -33,7 +34,10 @@ RUN touch config/DOCKER
|
|||||||
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
||||||
|
|
||||||
|
|
||||||
FROM node:16.14-alpine
|
FROM node:18.18-alpine
|
||||||
|
|
||||||
|
# Metadata for Github Package Registry
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16.14-alpine
|
FROM node:18.18-alpine
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
314
README.md
314
README.md
@@ -5,44 +5,142 @@
|
|||||||
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
||||||
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
|
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
|
||||||
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||||
</p>
|
<!-- 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-29-orange.svg"/></a>
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
|
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
|
||||||
|
|
||||||
|
_The original Overseerr team have been busy and Jellyfin/Emby support aren't on their roadmap, so we started this project as we wanted to bring the Overseerr experience to the Jellyfin/Emby Community!_
|
||||||
|
|
||||||
## Current Features
|
## Current Features
|
||||||
|
|
||||||
- Jellyfin Support
|
- Full Jellyfin/Emby/Plex integration. Authenticate and manage user access with Jellyfin/Emby/Plex!
|
||||||
- Emby Support
|
- Supports Movies, Shows, Mixed Libraries!
|
||||||
|
- Ability to change email addresses for smtp purposes
|
||||||
Along with all the existing Overseerr features:
|
- Ability to import all jellyfin/emby users
|
||||||
|
|
||||||
- Full Plex integration. Authenticate and manage user access with Plex!
|
|
||||||
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
|
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
|
||||||
- Plex library scan, to keep track of the titles which are already available.
|
- Jellyfin/Emby/Plex library scan, 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.
|
- 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!
|
- Incredibly simple request management UI. Don't dig through the app to simply approve recent requests!
|
||||||
- Granular permission system.
|
- Granular permission system.
|
||||||
- Support for various notification agents.
|
- Support for various notification agents.
|
||||||
- Mobile-friendly design, for when you need to approve requests on the go!
|
- Mobile-friendly design, for when you need to approve requests on the go!
|
||||||
|
|
||||||
|
(Upcoming Features include: Multiple Server Instances, and much more!)
|
||||||
|
|
||||||
With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested.
|
With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
#### Pre-requisite (Important)
|
||||||
|
|
||||||
|
_*On Jellyfin/Emby, ensure the `settings > Home > Automatically group content from the following folders into views such as 'Movies', 'Music' and 'TV'` is turned off*_
|
||||||
|
|
||||||
|
### Launching Jellyseerr using Docker (Recommended)
|
||||||
|
|
||||||
Check out our dockerhub for instructions on how to install and run Jellyseerr:
|
Check out our dockerhub for instructions on how to install and run Jellyseerr:
|
||||||
https://hub.docker.com/r/fallenbagel/jellyseerr
|
https://hub.docker.com/r/fallenbagel/jellyseerr
|
||||||
|
|
||||||
### Launching Jellyseerr manually:
|
### Building from source (ADVANCED):
|
||||||
|
|
||||||
```bash
|
#### Windows
|
||||||
yarn install
|
|
||||||
|
Pre-requisites:
|
||||||
|
|
||||||
|
- Nodejs [v18](https://nodejs.org/download/release/v18.18.2)
|
||||||
|
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install)
|
||||||
|
- Download/git clone the source code from the github (Either develop branch or main for stable)
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
npm i -g win-node-env
|
||||||
|
set CYPRESS_INSTALL_BINARY=0
|
||||||
|
yarn install --frozen-lockfile --network-timeout 1000000
|
||||||
yarn run build
|
yarn run build
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
(you can use task scheduler to run a bat script with `@echo off` and `yarn start` to run jellyseerr in the background)
|
||||||
|
|
||||||
|
_to set env variables such as `JELLYFIN_TYPE=emby` create a file called `.env` in the root directory of jellyseerr_
|
||||||
|
|
||||||
|
#### Linux
|
||||||
|
|
||||||
|
**Pre-requisites:**
|
||||||
|
|
||||||
|
- Nodejs [v18](https://nodejs.org/en/download/package-manager)
|
||||||
|
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) (on debian based distros, the package manager provided `yarn` is different and is a package called cmdlet. You can remove that using `apt-remove cmdlet` then install yarn using `npm install -g yarn`)
|
||||||
|
- Git
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Assuming you want the root folder for the jellyseerr source code to be cloned to `/opt`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Then clone the follow commands to clone and checkout to the stable version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Fallenbagel/jellyseerr.git && cd jellyseerr
|
||||||
|
git checkout main
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Then install the dependencies and build the dist
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000
|
||||||
|
yarn run build
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Now you can start jellyseerr using `yarn start` and opening http://localhost:5055 in your browser.
|
||||||
|
|
||||||
|
5. If you want to run jellyseerr as a _Systemd-service:_
|
||||||
|
|
||||||
|
- assuming jellyseerr was cloned to `/opt/`
|
||||||
|
- first create the environmentfile at `/etc/jellyseerr/jellyseerr.conf`
|
||||||
|
|
||||||
|
Environmentfile:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Jellyseerr's default port is 5055, if you want to use both, change this.
|
||||||
|
# specify on which port to listen
|
||||||
|
PORT=5055
|
||||||
|
|
||||||
|
# specify on which interface to listen, by default jellyseerr listens on all interfaces
|
||||||
|
#HOST=127.0.0.1
|
||||||
|
|
||||||
|
# Uncomment if your media server is emby instead of jellyfin.
|
||||||
|
# JELLYFIN_TYPE=emby
|
||||||
|
```
|
||||||
|
|
||||||
|
- Then run the command `which node` to find your node path (assuming it's at `/usr/bin/node`)
|
||||||
|
- Then create the service file using `sudo systemctl edit jellyseerr.service` or creating and editing a file at `/etc/systemd/system/jellyseerr.service`
|
||||||
|
|
||||||
|
Service file contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=Jellyseerr Service
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
EnvironmentFile=/etc/jellyseerr/jellyseerr.conf
|
||||||
|
Environment=NODE_ENV=production
|
||||||
|
Type=exec
|
||||||
|
Restart=on-failure
|
||||||
|
WorkingDirectory=/opt/jellyseerr
|
||||||
|
ExecStart=/usr/bin/node dist/index.js
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
### Packages:
|
### Packages:
|
||||||
|
|
||||||
Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr)
|
Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr)
|
||||||
|
Nixpkg: [Nixpkg](https://search.nixos.org/packages?channel=unstable&show=jellyseerr)
|
||||||
|
Snap: [Snap](https://snapcraft.io/jellyseerr)
|
||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||
@@ -69,3 +167,197 @@ Our [Code of Conduct](https://github.com/fallenbagel/jellyseerr/blob/develop/COD
|
|||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
You can help improve Jellyseerr too! Check out our [Contribution Guide](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md) to get started.
|
You can help improve Jellyseerr too! Check out our [Contribution Guide](https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md) to get started.
|
||||||
|
|
||||||
|
## Contributors ✨
|
||||||
|
|
||||||
|
Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcontributors.org/docs/en/emoji-key)) and all those that contributed directly to Jellyseerr:
|
||||||
|
|
||||||
|
### Jellyseerr Contributors ✨
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fallenbagel"><img src="https://avatars.githubusercontent.com/u/98979876?v=4?s=100" width="100px;" alt="Fallenbagel"/><br /><sub><b>Fallenbagel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fallenbagel" title="Code">💻</a> <a href="#maintenance-Fallenbagel" title="Maintenance">🚧</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/seanzhang98"><img src="https://avatars.githubusercontent.com/u/34902361?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="#translation-seanzhang98" title="Translation">🌍</a> <a href="https://github.com/Fallenbagel/jellyseerr/commits?author=seanzhang98" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notfakie"><img src="https://avatars.githubusercontent.com/u/103784113?v=4?s=100" width="100px;" alt="notfakie"/><br /><sub><b>notfakie</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=notfakie" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jumail"><img src="https://avatars.githubusercontent.com/u/7672055?v=4?s=100" width="100px;" alt="Mohamed Jumail"/><br /><sub><b>Mohamed Jumail</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/pulls?q=is%3Apr+reviewed-by%3AJumail" title="Reviewed Pull Requests">👀</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.heywhale.com"><img src="https://avatars.githubusercontent.com/u/4048787?v=4?s=100" width="100px;" alt="Shilong Jiang"/><br /><sub><b>Shilong Jiang</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jsl9208" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://jinas.me"><img src="https://avatars.githubusercontent.com/u/28459081?v=4?s=100" width="100px;" alt="Boring Dragon"/><br /><sub><b>Boring Dragon</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=boring-dragon" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sambartik"><img src="https://avatars.githubusercontent.com/u/63553146?v=4?s=100" width="100px;" alt="Samuel Bartík"/><br /><sub><b>Samuel Bartík</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=sambartik" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CyferShepard"><img src="https://avatars.githubusercontent.com/u/24864904?v=4?s=100" width="100px;" alt="Thegan Govender"/><br /><sub><b>Thegan Govender</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=CyferShepard" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jab416171"><img src="https://avatars.githubusercontent.com/u/345752?v=4?s=100" width="100px;" alt="jab416171"/><br /><sub><b>jab416171</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jab416171" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://nvds.be"><img src="https://avatars.githubusercontent.com/u/5257222?v=4?s=100" width="100px;" alt="Nicolai Van der Storm"/><br /><sub><b>Nicolai Van der Storm</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=NicolaiVdS" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Smexhy"><img src="https://avatars.githubusercontent.com/u/4880625?v=4?s=100" width="100px;" alt="Smexhy"/><br /><sub><b>Smexhy</b></sub></a><br /><a href="#translation-Smexhy" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://dd06-dev.fr"><img src="https://avatars.githubusercontent.com/u/58089504?v=4?s=100" width="100px;" alt="dd060606"/><br /><sub><b>dd060606</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=dd060606" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://qwer.tz"><img src="https://avatars.githubusercontent.com/u/71837281?v=4?s=100" width="100px;" alt="Daniel"/><br /><sub><b>Daniel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=darmiel" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/undone37"><img src="https://avatars.githubusercontent.com/u/10513808?v=4?s=100" width="100px;" alt="undone37"/><br /><sub><b>undone37</b></sub></a><br /><a href="#translation-undone37" title="Translation">🌍</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CheChu10"><img src="https://avatars.githubusercontent.com/u/32913133?v=4?s=100" width="100px;" alt="Chechu García"/><br /><sub><b>Chechu García</b></sub></a><br /><a href="#translation-CheChu10" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DimitriDR"><img src="https://avatars.githubusercontent.com/u/56969769?v=4?s=100" width="100px;" alt="Dimitri"/><br /><sub><b>Dimitri</b></sub></a><br /><a href="#translation-DimitriDR" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrey4korop"><img src="https://avatars.githubusercontent.com/u/24610708?v=4?s=100" width="100px;" alt="andrey4korop"/><br /><sub><b>andrey4korop</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=andrey4korop" title="Code">💻</a> <a href="#translation-andrey4korop" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://geoffrey-coulaud.fr"><img src="https://avatars.githubusercontent.com/u/20744730?v=4?s=100" width="100px;" alt="Geoffrey Coulaud"/><br /><sub><b>Geoffrey Coulaud</b></sub></a><br /><a href="#translation-GeoffreyCoulaud" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pikachu920"><img src="https://avatars.githubusercontent.com/u/28607612?v=4?s=100" width="100px;" alt="Pikachu920"/><br /><sub><b>Pikachu920</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Pikachu920" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yalagin"><img src="https://avatars.githubusercontent.com/u/12879142?v=4?s=100" width="100px;" alt="Maxim Yalagin"/><br /><sub><b>Maxim Yalagin</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=yalagin" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jeaboswell"><img src="https://avatars.githubusercontent.com/u/11653068?v=4?s=100" width="100px;" alt="Jesse Boswell"/><br /><sub><b>Jesse Boswell</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jeaboswell" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/d-fendrich"><img src="https://avatars.githubusercontent.com/u/27904138?v=4?s=100" width="100px;" alt="d-fendrich"/><br /><sub><b>d-fendrich</b></sub></a><br /><a href="#translation-d-fendrich" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/davidfdezalcoba"><img src="https://avatars.githubusercontent.com/u/15996018?v=4?s=100" width="100px;" alt="David Fernández Alcoba"/><br /><sub><b>David Fernández Alcoba</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=davidfdezalcoba" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Gauvino"><img src="https://avatars.githubusercontent.com/u/68083474?v=4?s=100" width="100px;" alt="Gauvino"/><br /><sub><b>Gauvino</b></sub></a><br /><a href="#translation-Gauvino" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EthanArmbrust"><img src="https://avatars.githubusercontent.com/u/22754714?v=4?s=100" width="100px;" alt="EthanArmbrust"/><br /><sub><b>EthanArmbrust</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=EthanArmbrust" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.piribisoft.com"><img src="https://avatars.githubusercontent.com/u/854646?v=4?s=100" width="100px;" alt="Eduardo"/><br /><sub><b>Eduardo</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=SirMartin" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RickLuiken"><img src="https://avatars.githubusercontent.com/u/34110371?v=4?s=100" width="100px;" alt="RickLuiken"/><br /><sub><b>RickLuiken</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=RickLuiken" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Br33ce"><img src="https://avatars.githubusercontent.com/u/124933490?v=4?s=100" width="100px;" alt="Br33ce"/><br /><sub><b>Br33ce</b></sub></a><br /><a href="#translation-Br33ce" title="Translation">🌍</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://athfan.com"><img src="https://avatars.githubusercontent.com/u/13810742?v=4?s=100" width="100px;" alt="Athfan Khaleel"/><br /><sub><b>Athfan Khaleel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=athphane" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
### Overseerr Contributors ✨
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://sct.dev"><img src="https://avatars1.githubusercontent.com/u/234213?v=4?s=100" width="100px;" alt="sct"/><br /><sub><b>sct</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=sct" title="Code">💻</a> <a href="#design-sct" title="Design">🎨</a> <a href="#ideas-sct" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/azoitos"><img src="https://avatars2.githubusercontent.com/u/26529049?v=4?s=100" width="100px;" alt="Alex Zoitos"/><br /><sub><b>Alex Zoitos</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=azoitos" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/OwsleyJr"><img src="https://avatars3.githubusercontent.com/u/8635678?v=4?s=100" width="100px;" alt="Brandon Cohen"/><br /><sub><b>Brandon Cohen</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=OwsleyJr" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ahreluth"><img src="https://avatars2.githubusercontent.com/u/75682440?v=4?s=100" width="100px;" alt="Ahreluth"/><br /><sub><b>Ahreluth</b></sub></a><br /><a href="#translation-Ahreluth" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KovalevArtem"><img src="https://avatars0.githubusercontent.com/u/36500228?v=4?s=100" width="100px;" alt="KovalevArtem"/><br /><sub><b>KovalevArtem</b></sub></a><br /><a href="#translation-KovalevArtem" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GiyomuWeb"><img src="https://avatars0.githubusercontent.com/u/62489209?v=4?s=100" width="100px;" alt="GiyomuWeb"/><br /><sub><b>GiyomuWeb</b></sub></a><br /><a href="#translation-GiyomuWeb" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/angrycuban13"><img src="https://avatars3.githubusercontent.com/u/39564898?v=4?s=100" width="100px;" alt="Angry Cuban"/><br /><sub><b>Angry Cuban</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=angrycuban13" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jvennik"><img src="https://avatars3.githubusercontent.com/u/6672637?v=4?s=100" width="100px;" alt="jvennik"/><br /><sub><b>jvennik</b></sub></a><br /><a href="#translation-jvennik" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/darknessgp"><img src="https://avatars0.githubusercontent.com/u/1521243?v=4?s=100" width="100px;" alt="darknessgp"/><br /><sub><b>darknessgp</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=darknessgp" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/saltydk"><img src="https://avatars1.githubusercontent.com/u/6587950?v=4?s=100" width="100px;" alt="salty"/><br /><sub><b>salty</b></sub></a><br /><a href="#infra-saltydk" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Shutruk"><img src="https://avatars2.githubusercontent.com/u/9198633?v=4?s=100" width="100px;" alt="Shutruk"/><br /><sub><b>Shutruk</b></sub></a><br /><a href="#translation-Shutruk" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/krystiancharubin"><img src="https://avatars2.githubusercontent.com/u/17775600?v=4?s=100" width="100px;" alt="Krystian Charubin"/><br /><sub><b>Krystian Charubin</b></sub></a><br /><a href="#design-krystiancharubin" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kieron"><img src="https://avatars2.githubusercontent.com/u/8655212?v=4?s=100" width="100px;" alt="Kieron Boswell"/><br /><sub><b>Kieron Boswell</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=kieron" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/samwiseg0"><img src="https://avatars1.githubusercontent.com/u/2241731?v=4?s=100" width="100px;" alt="samwiseg0"/><br /><sub><b>samwiseg0</b></sub></a><br /><a href="#question-samwiseg0" title="Answering Questions">💬</a> <a href="#infra-samwiseg0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ecelebi29"><img src="https://avatars2.githubusercontent.com/u/8337120?v=4?s=100" width="100px;" alt="ecelebi29"/><br /><sub><b>ecelebi29</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=ecelebi29" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mmozeiko"><img src="https://avatars3.githubusercontent.com/u/1665010?v=4?s=100" width="100px;" alt="Mārtiņš Možeiko"/><br /><sub><b>Mārtiņš Možeiko</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=mmozeiko" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mazzetta86"><img src="https://avatars2.githubusercontent.com/u/45591560?v=4?s=100" width="100px;" alt="mazzetta86"/><br /><sub><b>mazzetta86</b></sub></a><br /><a href="#translation-mazzetta86" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Panzer1119"><img src="https://avatars1.githubusercontent.com/u/23016343?v=4?s=100" width="100px;" alt="Paul Hagedorn"/><br /><sub><b>Paul Hagedorn</b></sub></a><br /><a href="#translation-Panzer1119" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Shagon94"><img src="https://avatars3.githubusercontent.com/u/9140783?v=4?s=100" width="100px;" alt="Shagon94"/><br /><sub><b>Shagon94</b></sub></a><br /><a href="#translation-Shagon94" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sebstrgg"><img src="https://avatars3.githubusercontent.com/u/27026694?v=4?s=100" width="100px;" alt="sebstrgg"/><br /><sub><b>sebstrgg</b></sub></a><br /><a href="#translation-sebstrgg" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danshilm"><img src="https://avatars2.githubusercontent.com/u/20923978?v=4?s=100" width="100px;" alt="Danshil Mungur"/><br /><sub><b>Danshil Mungur</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Code">💻</a> <a href="https://github.com/sct/overseerr/commits?author=danshilm" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doob187"><img src="https://avatars1.githubusercontent.com/u/60312740?v=4?s=100" width="100px;" alt="doob187"/><br /><sub><b>doob187</b></sub></a><br /><a href="#infra-doob187" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/johnpyp"><img src="https://avatars2.githubusercontent.com/u/20625636?v=4?s=100" width="100px;" alt="johnpyp"/><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" valign="top" width="14.28%"><a href="https://github.com/ankarhem"><img src="https://avatars1.githubusercontent.com/u/14110063?v=4?s=100" width="100px;" alt="Jakob Ankarhem"/><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" valign="top" width="14.28%"><a href="https://github.com/jayesh100"><img src="https://avatars1.githubusercontent.com/u/8022175?v=4?s=100" width="100px;" alt="Jayesh"/><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" valign="top" width="14.28%"><a href="https://github.com/flying-sausages"><img src="https://avatars1.githubusercontent.com/u/23618693?v=4?s=100" width="100px;" alt="flying-sausages"/><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" valign="top" width="14.28%"><a href="https://github.com/hirenshah"><img src="https://avatars2.githubusercontent.com/u/418112?v=4?s=100" width="100px;" alt="hirenshah"/><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" valign="top" width="14.28%"><a href="https://github.com/TheCatLady"><img src="https://avatars0.githubusercontent.com/u/52870424?v=4?s=100" width="100px;" alt="TheCatLady"/><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" valign="top" width="14.28%"><a href="https://github.com/chriscpritchard"><img src="https://avatars1.githubusercontent.com/u/1839074?v=4?s=100" width="100px;" alt="Chris Pritchard"/><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" valign="top" width="14.28%"><a href="https://github.com/Tamberlox"><img src="https://avatars3.githubusercontent.com/u/56069014?v=4?s=100" width="100px;" alt="Tamberlox"/><br /><sub><b>Tamberlox</b></sub></a><br /><a href="#translation-Tamberlox" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://hmnd.io"><img src="https://avatars.githubusercontent.com/u/12853597?v=4?s=100" width="100px;" alt="David"/><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" valign="top" width="14.28%"><a href="https://www.douglas-parker.com"><img src="https://avatars.githubusercontent.com/u/18235822?v=4?s=100" width="100px;" alt="Douglas Parker"/><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" valign="top" width="14.28%"><a href="https://github.com/dancarter"><img src="https://avatars.githubusercontent.com/u/4387516?v=4?s=100" width="100px;" alt="Daniel Carter"/><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" valign="top" width="14.28%"><a href="https://nuro.dev"><img src="https://avatars.githubusercontent.com/u/4991309?v=4?s=100" width="100px;" alt="nuro"/><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/JonnyWong16"><img src="https://avatars.githubusercontent.com/u/9099342?v=4?s=100" width="100px;" alt="JonnyWong16"/><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" valign="top" width="14.28%"><a href="https://github.com/Roxedus"><img src="https://avatars.githubusercontent.com/u/7110194?v=4?s=100" width="100px;" alt="Roxedus"/><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" valign="top" width="14.28%"><a href="https://github.com/WoisWoi"><img src="https://avatars.githubusercontent.com/u/75491231?v=4?s=100" width="100px;" alt="WoisWoi"/><br /><sub><b>WoisWoi</b></sub></a><br /><a href="#translation-WoisWoi" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HubDuck"><img src="https://avatars.githubusercontent.com/u/77843475?v=4?s=100" width="100px;" alt="HubDuck"/><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" valign="top" width="14.28%"><a href="https://github.com/costaht"><img src="https://avatars.githubusercontent.com/u/50637431?v=4?s=100" width="100px;" alt="costaht"/><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" valign="top" width="14.28%"><a href="https://github.com/Shjosan"><img src="https://avatars.githubusercontent.com/u/20847626?v=4?s=100" width="100px;" alt="Shjosan"/><br /><sub><b>Shjosan</b></sub></a><br /><a href="#translation-Shjosan" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kobaubarr"><img src="https://avatars.githubusercontent.com/u/28481522?v=4?s=100" width="100px;" alt="kobaubarr"/><br /><sub><b>kobaubarr</b></sub></a><br /><a href="#translation-kobaubarr" title="Translation">🌍</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notorius28"><img src="https://avatars.githubusercontent.com/u/1621513?v=4?s=100" width="100px;" alt="Ricardo González"/><br /><sub><b>Ricardo González</b></sub></a><br /><a href="#translation-notorius28" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://torkili.uz"><img src="https://avatars.githubusercontent.com/u/460764?v=4?s=100" width="100px;" alt="Torkil"/><br /><sub><b>Torkil</b></sub></a><br /><a href="#translation-Torkiliuz" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://www.jagandeepbrar.io"><img src="https://avatars.githubusercontent.com/u/3048295?v=4?s=100" width="100px;" alt="Jagandeep Brar"/><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" valign="top" width="14.28%"><a href="http://dtalens.com"><img src="https://avatars.githubusercontent.com/u/6631832?v=4?s=100" width="100px;" alt="dtalens"/><br /><sub><b>dtalens</b></sub></a><br /><a href="#translation-dtalens" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/acortelyou"><img src="https://avatars.githubusercontent.com/u/1689668?v=4?s=100" width="100px;" alt="Alex Cortelyou"/><br /><sub><b>Alex Cortelyou</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=acortelyou" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://nz.linkedin.com/in/jonocairns"><img src="https://avatars.githubusercontent.com/u/182836?v=4?s=100" width="100px;" alt="Jono Cairns"/><br /><sub><b>Jono Cairns</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jonocairns" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://scias.net/"><img src="https://avatars.githubusercontent.com/u/439655?v=4?s=100" width="100px;" alt="DJScias"/><br /><sub><b>DJScias</b></sub></a><br /><a href="#translation-DJScias" title="Translation">🌍</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Dabu-dot"><img src="https://avatars.githubusercontent.com/u/52525576?v=4?s=100" width="100px;" alt="Dabu-dot"/><br /><sub><b>Dabu-dot</b></sub></a><br /><a href="#translation-Dabu-dot" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jabster28"><img src="https://avatars.githubusercontent.com/u/29015942?v=4?s=100" width="100px;" alt="Jabster28"/><br /><sub><b>Jabster28</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Jabster28" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlerooster"><img src="https://avatars.githubusercontent.com/u/83890654?v=4?s=100" width="100px;" alt="littlerooster"/><br /><sub><b>littlerooster</b></sub></a><br /><a href="#translation-littlerooster" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dphildebrandt"><img src="https://avatars.githubusercontent.com/u/154459?v=4?s=100" width="100px;" alt="Dustin Hildebrandt"/><br /><sub><b>Dustin Hildebrandt</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=dphildebrandt" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Generator"><img src="https://avatars.githubusercontent.com/u/44146?v=4?s=100" width="100px;" alt="Bruno Guerreiro"/><br /><sub><b>Bruno Guerreiro</b></sub></a><br /><a href="#translation-Generator" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/iceHtwoO"><img src="https://avatars.githubusercontent.com/u/27020492?v=4?s=100" width="100px;" alt="Alexander Neuhäuser"/><br /><sub><b>Alexander Neuhäuser</b></sub></a><br /><a href="#translation-iceHtwoO" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://www.unext.co.jp"><img src="https://avatars.githubusercontent.com/u/37431541?v=4?s=100" width="100px;" alt="Livio"/><br /><sub><b>Livio</b></sub></a><br /><a href="#design-liviokanone" title="Design">🎨</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tangentThought"><img src="https://avatars.githubusercontent.com/u/25516090?v=4?s=100" width="100px;" alt="tangentThought"/><br /><sub><b>tangentThought</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=tangentThought" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicospz"><img src="https://avatars.githubusercontent.com/u/31373060?v=4?s=100" width="100px;" alt="Nicolás Espinoza"/><br /><sub><b>Nicolás Espinoza</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=nicospz" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sootylunatic"><img src="https://avatars.githubusercontent.com/u/36486087?v=4?s=100" width="100px;" alt="sootylunatic"/><br /><sub><b>sootylunatic</b></sub></a><br /><a href="#translation-sootylunatic" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JoKerIsCraZy"><img src="https://avatars.githubusercontent.com/u/47474211?v=4?s=100" width="100px;" alt="JoKerIsCraZy"/><br /><sub><b>JoKerIsCraZy</b></sub></a><br /><a href="#translation-JoKerIsCraZy" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://daddie.dev"><img src="https://avatars.githubusercontent.com/u/33762262?v=4?s=100" width="100px;" alt="Daddie0"/><br /><sub><b>Daddie0</b></sub></a><br /><a href="#translation-GoByeBye" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://ungaro.me"><img src="https://avatars.githubusercontent.com/u/43807696?v=4?s=100" width="100px;" alt="Simone"/><br /><sub><b>Simone</b></sub></a><br /><a href="#translation-Simoneu01" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/adan89lion"><img src="https://avatars.githubusercontent.com/u/6585644?v=4?s=100" width="100px;" alt="Seohyun Joo"/><br /><sub><b>Seohyun Joo</b></sub></a><br /><a href="#translation-adan89lion" title="Translation">🌍</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ty4ko"><img src="https://avatars.githubusercontent.com/u/21213535?v=4?s=100" width="100px;" alt="Sergey"/><br /><sub><b>Sergey</b></sub></a><br /><a href="#translation-ty4ko" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/skafte1990"><img src="https://avatars.githubusercontent.com/u/31465453?v=4?s=100" width="100px;" alt="Shaaft"/><br /><sub><b>Shaaft</b></sub></a><br /><a href="#translation-skafte1990" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sr093906"><img src="https://avatars.githubusercontent.com/u/8369201?v=4?s=100" width="100px;" alt="sr093906"/><br /><sub><b>sr093906</b></sub></a><br /><a href="#translation-sr093906" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Nackophilz"><img src="https://avatars.githubusercontent.com/u/61667226?v=4?s=100" width="100px;" alt="Nackophilz"/><br /><sub><b>Nackophilz</b></sub></a><br /><a href="#translation-Nackophilz" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/schambers"><img src="https://avatars.githubusercontent.com/u/31563?v=4?s=100" width="100px;" alt="Sean Chambers"/><br /><sub><b>Sean Chambers</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=schambers" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/deniscerri"><img src="https://avatars.githubusercontent.com/u/64997243?v=4?s=100" width="100px;" alt="deniscerri"/><br /><sub><b>deniscerri</b></sub></a><br /><a href="#translation-deniscerri" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomgacz"><img src="https://avatars.githubusercontent.com/u/14138209?v=4?s=100" width="100px;" alt="tomgacz"/><br /><sub><b>tomgacz</b></sub></a><br /><a href="#translation-tomgacz" title="Translation">🌍</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Andersborrits"><img src="https://avatars.githubusercontent.com/u/29452218?v=4?s=100" width="100px;" alt="Andersborrits"/><br /><sub><b>Andersborrits</b></sub></a><br /><a href="#translation-Andersborrits" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://maxentrouault.fr"><img src="https://avatars.githubusercontent.com/u/67283154?v=4?s=100" width="100px;" alt="Maxent"/><br /><sub><b>Maxent</b></sub></a><br /><a href="#translation-Maxentr" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sambartik"><img src="https://avatars.githubusercontent.com/u/63553146?v=4?s=100" width="100px;" alt="Samuel Bartík"/><br /><sub><b>Samuel Bartík</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=sambartik" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/frank-cywong"><img src="https://avatars.githubusercontent.com/u/90653148?v=4?s=100" width="100px;" alt="Chun Yeung Wong"/><br /><sub><b>Chun Yeung Wong</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=frank-cywong" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TheMeanCanEHdian"><img src="https://avatars.githubusercontent.com/u/16025103?v=4?s=100" width="100px;" alt="TheMeanCanEHdian"/><br /><sub><b>TheMeanCanEHdian</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=TheMeanCanEHdian" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Gylesie"><img src="https://avatars.githubusercontent.com/u/86306812?v=4?s=100" width="100px;" alt="Gylesie"/><br /><sub><b>Gylesie</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Gylesie" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fhd-pro"><img src="https://avatars.githubusercontent.com/u/82862079?v=4?s=100" width="100px;" alt="Fhd-pro"/><br /><sub><b>Fhd-pro</b></sub></a><br /><a href="#translation-Fhd-pro" title="Translation">🌍</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PovilasID"><img src="https://avatars.githubusercontent.com/u/396243?v=4?s=100" width="100px;" alt="PovilasID"/><br /><sub><b>PovilasID</b></sub></a><br /><a href="#translation-PovilasID" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/byakurau"><img src="https://avatars.githubusercontent.com/u/1811683?v=4?s=100" width="100px;" alt="byakurau"/><br /><sub><b>byakurau</b></sub></a><br /><a href="#translation-byakurau" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miknii"><img src="https://avatars.githubusercontent.com/u/109232569?v=4?s=100" width="100px;" alt="miknii"/><br /><sub><b>miknii</b></sub></a><br /><a href="#translation-miknii" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Eclipseop"><img src="https://avatars.githubusercontent.com/u/5846213?v=4?s=100" width="100px;" alt="Mackenzie"/><br /><sub><b>Mackenzie</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Eclipseop" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s0up4200"><img src="https://avatars.githubusercontent.com/u/18177310?v=4?s=100" width="100px;" alt="soup"/><br /><sub><b>soup</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=s0up4200" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ceptonit"><img src="https://avatars.githubusercontent.com/u/12678743?v=4?s=100" width="100px;" alt="ceptonit"/><br /><sub><b>ceptonit</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=ceptonit" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aedelbro"><img src="https://avatars.githubusercontent.com/u/36162221?v=4?s=100" width="100px;" alt="aedelbro"/><br /><sub><b>aedelbro</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=aedelbro" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/lunks/"><img src="https://avatars.githubusercontent.com/u/91118?v=4?s=100" width="100px;" alt="Pedro Nascimento"/><br /><sub><b>Pedro Nascimento</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=lunks" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://voke.dev"><img src="https://avatars.githubusercontent.com/u/1899334?v=4?s=100" width="100px;" alt="Owen Voke"/><br /><sub><b>Owen Voke</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=owenvoke" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Nimelrian"><img src="https://avatars.githubusercontent.com/u/8960836?v=4?s=100" width="100px;" alt="Sebastian K"/><br /><sub><b>Sebastian K</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Nimelrian" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jariz"><img src="https://avatars.githubusercontent.com/u/1415847?v=4?s=100" width="100px;" alt="jariz"/><br /><sub><b>jariz</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=jariz" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://arouillard.fr"><img src="https://avatars.githubusercontent.com/u/13947260?v=4?s=100" width="100px;" alt="Alex"/><br /><sub><b>Alex</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Alexays" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zebebles"><img src="https://avatars.githubusercontent.com/u/11425451?v=4?s=100" width="100px;" alt="Zeb Muller"/><br /><sub><b>Zeb Muller</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Zebebles" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://smoores.dev"><img src="https://avatars.githubusercontent.com/u/5354254?v=4?s=100" width="100px;" alt="Shane Friedman"/><br /><sub><b>Shane Friedman</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=SMores" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://izaacj.me"><img src="https://avatars.githubusercontent.com/u/711323?v=4?s=100" width="100px;" alt="Izaac Brånn"/><br /><sub><b>Izaac Brånn</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=IzaacJ" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SalmanTariq"><img src="https://avatars.githubusercontent.com/u/13284494?v=4?s=100" width="100px;" alt="Salman Tariq"/><br /><sub><b>Salman Tariq</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=SalmanTariq" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrew-kennedy"><img src="https://avatars.githubusercontent.com/u/2387159?v=4?s=100" width="100px;" alt="Andrew Kennedy"/><br /><sub><b>Andrew Kennedy</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=andrew-kennedy" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fallenbagel"><img src="https://avatars.githubusercontent.com/u/98979876?v=4?s=100" width="100px;" alt="Fallenbagel"/><br /><sub><b>Fallenbagel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fallenbagel" title="Jellyseerr">🪼⌨️</a> <a href="https://github.com/sct/overseerr/commits?author=Fallenbagel" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://aidoge.xyz"><img src="https://avatars.githubusercontent.com/u/9427639?v=4?s=100" width="100px;" alt="Anton K. (ai Doge)"/><br /><sub><b>Anton K. (ai Doge)</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=scorp200" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://marcofaggian.com"><img src="https://avatars.githubusercontent.com/u/19221001?v=4?s=100" width="100px;" alt="Marco Faggian"/><br /><sub><b>Marco Faggian</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=marcofaggian" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://nemchik.com/"><img src="https://avatars.githubusercontent.com/u/725456?v=4?s=100" width="100px;" alt="Eric Nemchik"/><br /><sub><b>Eric Nemchik</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=nemchik" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RemiRigal"><img src="https://avatars.githubusercontent.com/u/19256051?v=4?s=100" width="100px;" alt="RemiRigal"/><br /><sub><b>RemiRigal</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=RemiRigal" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|||||||
19
cypress.config.ts
Normal file
19
cypress.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
projectId: 'xkm1b4',
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:5055',
|
||||||
|
experimentalSessionAndOrigin: true,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
ADMIN_EMAIL: 'admin@seerr.dev',
|
||||||
|
ADMIN_PASSWORD: 'test1234',
|
||||||
|
USER_EMAIL: 'friend@seerr.dev',
|
||||||
|
USER_PASSWORD: 'test1234',
|
||||||
|
},
|
||||||
|
retries: {
|
||||||
|
runMode: 2,
|
||||||
|
openMode: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
149
cypress/config/settings.cypress.json
Normal file
149
cypress/config/settings.cypress.json
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
{
|
||||||
|
"clientId": "6919275e-142a-48d8-be6b-93594cbd4626",
|
||||||
|
"vapidPrivate": "tmnslaO8ZWN6bNbSEv_rolPeBTlNxOwCCAHrM9oZz3M",
|
||||||
|
"vapidPublic": "BK_EpP8NDm9waor2zn6_S28o3ZYv4kCkJOfYpO3pt3W6jnPmxrgTLANUBNbbyaNatPnSQ12De9CeqSYQrqWzHTs",
|
||||||
|
"main": {
|
||||||
|
"apiKey": "testkey",
|
||||||
|
"applicationTitle": "Overseerr",
|
||||||
|
"applicationUrl": "",
|
||||||
|
"csrfProtection": false,
|
||||||
|
"cacheImages": false,
|
||||||
|
"defaultPermissions": 32,
|
||||||
|
"defaultQuotas": {
|
||||||
|
"movie": {},
|
||||||
|
"tv": {}
|
||||||
|
},
|
||||||
|
"hideAvailable": false,
|
||||||
|
"localLogin": true,
|
||||||
|
"newPlexLogin": true,
|
||||||
|
"region": "",
|
||||||
|
"originalLanguage": "",
|
||||||
|
"trustProxy": false,
|
||||||
|
"partialRequestsEnabled": true,
|
||||||
|
"locale": "en"
|
||||||
|
},
|
||||||
|
"plex": {
|
||||||
|
"name": "Seerr",
|
||||||
|
"ip": "192.168.1.1",
|
||||||
|
"port": 32400,
|
||||||
|
"useSsl": false,
|
||||||
|
"libraries": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "Movies",
|
||||||
|
"enabled": true,
|
||||||
|
"type": "movie"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"machineId": "test"
|
||||||
|
},
|
||||||
|
"tautulli": {},
|
||||||
|
"radarr": [],
|
||||||
|
"sonarr": [],
|
||||||
|
"public": {
|
||||||
|
"initialized": true
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"agents": {
|
||||||
|
"email": {
|
||||||
|
"enabled": false,
|
||||||
|
"options": {
|
||||||
|
"emailFrom": "",
|
||||||
|
"smtpHost": "",
|
||||||
|
"smtpPort": 587,
|
||||||
|
"secure": false,
|
||||||
|
"ignoreTls": false,
|
||||||
|
"requireTls": false,
|
||||||
|
"allowSelfSigned": false,
|
||||||
|
"senderName": "Overseerr"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"enabled": false,
|
||||||
|
"types": 0,
|
||||||
|
"options": {
|
||||||
|
"webhookUrl": "",
|
||||||
|
"enableMentions": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lunasea": {
|
||||||
|
"enabled": false,
|
||||||
|
"types": 0,
|
||||||
|
"options": {
|
||||||
|
"webhookUrl": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slack": {
|
||||||
|
"enabled": false,
|
||||||
|
"types": 0,
|
||||||
|
"options": {
|
||||||
|
"webhookUrl": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
"enabled": false,
|
||||||
|
"types": 0,
|
||||||
|
"options": {
|
||||||
|
"botAPI": "",
|
||||||
|
"chatId": "",
|
||||||
|
"sendSilently": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pushbullet": {
|
||||||
|
"enabled": false,
|
||||||
|
"types": 0,
|
||||||
|
"options": {
|
||||||
|
"accessToken": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pushover": {
|
||||||
|
"enabled": false,
|
||||||
|
"types": 0,
|
||||||
|
"options": {
|
||||||
|
"accessToken": "",
|
||||||
|
"userToken": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webhook": {
|
||||||
|
"enabled": false,
|
||||||
|
"types": 0,
|
||||||
|
"options": {
|
||||||
|
"webhookUrl": "",
|
||||||
|
"jsonPayload": "IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webpush": {
|
||||||
|
"enabled": false,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"gotify": {
|
||||||
|
"enabled": false,
|
||||||
|
"types": 0,
|
||||||
|
"options": {
|
||||||
|
"url": "",
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jobs": {
|
||||||
|
"plex-recently-added-scan": {
|
||||||
|
"schedule": "0 */5 * * * *"
|
||||||
|
},
|
||||||
|
"plex-full-scan": {
|
||||||
|
"schedule": "0 0 3 * * *"
|
||||||
|
},
|
||||||
|
"radarr-scan": {
|
||||||
|
"schedule": "0 0 4 * * *"
|
||||||
|
},
|
||||||
|
"sonarr-scan": {
|
||||||
|
"schedule": "0 30 4 * * *"
|
||||||
|
},
|
||||||
|
"download-sync": {
|
||||||
|
"schedule": "0 * * * * *"
|
||||||
|
},
|
||||||
|
"download-sync-reset": {
|
||||||
|
"schedule": "0 0 1 * * *"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
cypress/e2e/discover.cy.ts
Normal file
214
cypress/e2e/discover.cy.ts
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
const clickFirstTitleCardInSlider = (sliderTitle: string): void => {
|
||||||
|
cy.contains('.slider-header', sliderTitle)
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]')
|
||||||
|
.first()
|
||||||
|
.trigger('mouseover')
|
||||||
|
.find('[data-testid=title-card-title]')
|
||||||
|
.invoke('text')
|
||||||
|
.then((text) => {
|
||||||
|
cy.contains('.slider-header', sliderTitle)
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
cy.get('[data-testid=media-title]').should('contain', text);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Discover', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads a trending item', () => {
|
||||||
|
cy.intercept('/api/v1/discover/trending*').as('getTrending');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.wait('@getTrending');
|
||||||
|
clickFirstTitleCardInSlider('Trending');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads popular movies', () => {
|
||||||
|
cy.intercept('/api/v1/discover/movies*').as('getPopularMovies');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.wait('@getPopularMovies');
|
||||||
|
clickFirstTitleCardInSlider('Popular Movies');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads upcoming movies', () => {
|
||||||
|
cy.intercept('/api/v1/discover/movies?page=1&primaryReleaseDateGte*').as(
|
||||||
|
'getUpcomingMovies'
|
||||||
|
);
|
||||||
|
cy.visit('/');
|
||||||
|
cy.wait('@getUpcomingMovies');
|
||||||
|
clickFirstTitleCardInSlider('Upcoming Movies');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads popular series', () => {
|
||||||
|
cy.intercept('/api/v1/discover/tv*').as('getPopularTv');
|
||||||
|
cy.visit('/');
|
||||||
|
cy.wait('@getPopularTv');
|
||||||
|
clickFirstTitleCardInSlider('Popular Series');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads upcoming series', () => {
|
||||||
|
cy.intercept('/api/v1/discover/tv?page=1&firstAirDateGte=*').as(
|
||||||
|
'getUpcomingSeries'
|
||||||
|
);
|
||||||
|
cy.visit('/');
|
||||||
|
cy.wait('@getUpcomingSeries');
|
||||||
|
clickFirstTitleCardInSlider('Upcoming Series');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error for media with invalid TMDB ID', () => {
|
||||||
|
cy.intercept('GET', '/api/v1/media?*', {
|
||||||
|
pageInfo: { pages: 1, pageSize: 20, results: 1, page: 1 },
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
downloadStatus: [],
|
||||||
|
downloadStatus4k: [],
|
||||||
|
id: 1922,
|
||||||
|
mediaType: 'movie',
|
||||||
|
tmdbId: 998814,
|
||||||
|
tvdbId: null,
|
||||||
|
imdbId: null,
|
||||||
|
status: 5,
|
||||||
|
status4k: 1,
|
||||||
|
createdAt: '2022-08-18T18:11:13.000Z',
|
||||||
|
updatedAt: '2022-08-18T19:56:41.000Z',
|
||||||
|
lastSeasonChange: '2022-08-18T19:56:41.000Z',
|
||||||
|
mediaAddedAt: '2022-08-18T19:56:41.000Z',
|
||||||
|
serviceId: null,
|
||||||
|
serviceId4k: null,
|
||||||
|
externalServiceId: null,
|
||||||
|
externalServiceId4k: null,
|
||||||
|
externalServiceSlug: null,
|
||||||
|
externalServiceSlug4k: null,
|
||||||
|
ratingKey: null,
|
||||||
|
ratingKey4k: null,
|
||||||
|
seasons: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).as('getMedia');
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
cy.wait('@getMedia');
|
||||||
|
cy.contains('.slider-header', 'Recently Added')
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]')
|
||||||
|
.first()
|
||||||
|
.find('[data-testid=title-card-title]')
|
||||||
|
.contains('Movie Not Found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays error for request with invalid TMDB ID', () => {
|
||||||
|
cy.intercept('GET', '/api/v1/request?*', {
|
||||||
|
pageInfo: { pages: 1, pageSize: 10, results: 1, page: 1 },
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 582,
|
||||||
|
status: 1,
|
||||||
|
createdAt: '2022-08-18T18:11:13.000Z',
|
||||||
|
updatedAt: '2022-08-18T18:11:13.000Z',
|
||||||
|
type: 'movie',
|
||||||
|
is4k: false,
|
||||||
|
serverId: null,
|
||||||
|
profileId: null,
|
||||||
|
rootFolder: null,
|
||||||
|
languageProfileId: null,
|
||||||
|
tags: null,
|
||||||
|
media: {
|
||||||
|
downloadStatus: [],
|
||||||
|
downloadStatus4k: [],
|
||||||
|
id: 1922,
|
||||||
|
mediaType: 'movie',
|
||||||
|
tmdbId: 998814,
|
||||||
|
tvdbId: null,
|
||||||
|
imdbId: null,
|
||||||
|
status: 2,
|
||||||
|
status4k: 1,
|
||||||
|
createdAt: '2022-08-18T18:11:13.000Z',
|
||||||
|
updatedAt: '2022-08-18T18:11:13.000Z',
|
||||||
|
lastSeasonChange: '2022-08-18T18:11:13.000Z',
|
||||||
|
mediaAddedAt: null,
|
||||||
|
serviceId: null,
|
||||||
|
serviceId4k: null,
|
||||||
|
externalServiceId: null,
|
||||||
|
externalServiceId4k: null,
|
||||||
|
externalServiceSlug: null,
|
||||||
|
externalServiceSlug4k: null,
|
||||||
|
ratingKey: null,
|
||||||
|
ratingKey4k: null,
|
||||||
|
},
|
||||||
|
seasons: [],
|
||||||
|
modifiedBy: null,
|
||||||
|
requestedBy: {
|
||||||
|
permissions: 4194336,
|
||||||
|
id: 18,
|
||||||
|
email: 'friend@seerr.dev',
|
||||||
|
plexUsername: null,
|
||||||
|
username: '',
|
||||||
|
recoveryLinkExpirationDate: null,
|
||||||
|
userType: 2,
|
||||||
|
avatar:
|
||||||
|
'https://gravatar.com/avatar/c77fdc27cab83732b8623d2ea873d330?default=mm&size=200',
|
||||||
|
movieQuotaLimit: null,
|
||||||
|
movieQuotaDays: null,
|
||||||
|
tvQuotaLimit: null,
|
||||||
|
tvQuotaDays: null,
|
||||||
|
createdAt: '2022-08-17T04:55:28.000Z',
|
||||||
|
updatedAt: '2022-08-17T04:55:28.000Z',
|
||||||
|
requestCount: 1,
|
||||||
|
displayName: 'friend@seerr.dev',
|
||||||
|
},
|
||||||
|
seasonCount: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).as('getRequests');
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
cy.wait('@getRequests');
|
||||||
|
cy.contains('.slider-header', 'Recent Requests')
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=request-card]')
|
||||||
|
.first()
|
||||||
|
.find('[data-testid=request-card-title]')
|
||||||
|
.contains('Movie Not Found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads plex watchlist', () => {
|
||||||
|
cy.intercept('/api/v1/discover/watchlist', {
|
||||||
|
fixture: 'watchlist.json',
|
||||||
|
}).as('getWatchlist');
|
||||||
|
// Wait for one of the watchlist movies to resolve
|
||||||
|
cy.intercept('/api/v1/movie/361743').as('getTmdbMovie');
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.wait('@getWatchlist');
|
||||||
|
|
||||||
|
const sliderHeader = cy.contains('.slider-header', 'Watchlist');
|
||||||
|
|
||||||
|
sliderHeader.scrollIntoView();
|
||||||
|
|
||||||
|
cy.wait('@getTmdbMovie');
|
||||||
|
// Wait a little longer to make sure the movie component reloaded
|
||||||
|
cy.wait(500);
|
||||||
|
|
||||||
|
sliderHeader
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]')
|
||||||
|
.first()
|
||||||
|
.trigger('mouseover')
|
||||||
|
.find('[data-testid=title-card-title]')
|
||||||
|
.invoke('text')
|
||||||
|
.then((text) => {
|
||||||
|
cy.contains('.slider-header', 'Watchlist')
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
cy.get('[data-testid=media-title]').should('contain', text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
13
cypress/e2e/login.cy.ts
Normal file
13
cypress/e2e/login.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
describe('Login Page', () => {
|
||||||
|
it('succesfully logs in as an admin', () => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
cy.visit('/');
|
||||||
|
cy.contains('Trending');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('succesfully logs in as a local user', () => {
|
||||||
|
cy.loginAsUser();
|
||||||
|
cy.visit('/');
|
||||||
|
cy.contains('Trending');
|
||||||
|
});
|
||||||
|
});
|
||||||
12
cypress/e2e/movie-details.cy.ts
Normal file
12
cypress/e2e/movie-details.cy.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
describe('Movie Details', () => {
|
||||||
|
it('loads a movie page', () => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
// Try to load minions: rise of gru
|
||||||
|
cy.visit('/movie/438148');
|
||||||
|
|
||||||
|
cy.get('[data-testid=media-title]').should(
|
||||||
|
'contain',
|
||||||
|
'Minions: The Rise of Gru (2022)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
cypress/e2e/pull-to-refresh.cy.ts
Normal file
25
cypress/e2e/pull-to-refresh.cy.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
describe('Pull To Refresh', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
|
||||||
|
cy.viewport(390, 844);
|
||||||
|
cy.visitMobile('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reloads the current page', () => {
|
||||||
|
cy.wait(500);
|
||||||
|
|
||||||
|
cy.intercept({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v1/*',
|
||||||
|
}).as('apiCall');
|
||||||
|
|
||||||
|
cy.get('.searchbar').swipe('bottom', [190, 500]);
|
||||||
|
|
||||||
|
cy.wait('@apiCall').then((interception) => {
|
||||||
|
assert.isNotNull(
|
||||||
|
interception.response.body,
|
||||||
|
'API was called and received data'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
163
cypress/e2e/settings/discover-customization.cy.ts
Normal file
163
cypress/e2e/settings/discover-customization.cy.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
describe('Discover Customization', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
cy.intercept('/api/v1/settings/discover').as('getDiscoverSliders');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('show the discover customization settings', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-start-editing]').click();
|
||||||
|
|
||||||
|
cy.get('[data-testid=create-slider-header')
|
||||||
|
.should('contain', 'Create New Slider')
|
||||||
|
.scrollIntoView();
|
||||||
|
|
||||||
|
// There should be some built in options
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]').should(
|
||||||
|
'contain',
|
||||||
|
'Recently Added'
|
||||||
|
);
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]').should(
|
||||||
|
'contain',
|
||||||
|
'Recent Requests'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can drag to re-order elements and save to persist the changes', () => {
|
||||||
|
let dataTransfer = new DataTransfer();
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-start-editing]').click();
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.first()
|
||||||
|
.trigger('dragstart', { dataTransfer });
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.eq(1)
|
||||||
|
.trigger('drop', { dataTransfer });
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.eq(1)
|
||||||
|
.trigger('dragend', { dataTransfer });
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.eq(1)
|
||||||
|
.should('contain', 'Recently Added');
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-customize-submit').click();
|
||||||
|
cy.wait('@getDiscoverSliders');
|
||||||
|
|
||||||
|
cy.reload();
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-start-editing]').click();
|
||||||
|
|
||||||
|
dataTransfer = new DataTransfer();
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.eq(1)
|
||||||
|
.should('contain', 'Recently Added');
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.first()
|
||||||
|
.trigger('dragstart', { dataTransfer });
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.eq(1)
|
||||||
|
.trigger('drop', { dataTransfer });
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.eq(1)
|
||||||
|
.trigger('dragend', { dataTransfer });
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.eq(1)
|
||||||
|
.should('contain', 'Recent Requests');
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-customize-submit').click();
|
||||||
|
cy.wait('@getDiscoverSliders');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can create a new discover option and remove it', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.intercept('/api/v1/settings/discover/*').as('discoverSlider');
|
||||||
|
cy.intercept('/api/v1/search/keyword*').as('searchKeyword');
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-start-editing]').click();
|
||||||
|
|
||||||
|
const sliderTitle = 'Custom Keyword Slider';
|
||||||
|
|
||||||
|
cy.get('#sliderType').select('TMDB Movie Keyword');
|
||||||
|
|
||||||
|
cy.get('#title').type(sliderTitle);
|
||||||
|
// First confirm that an invalid keyword doesn't allow us to submit anything
|
||||||
|
cy.get('#data').type('invalidkeyword{enter}', { delay: 100 });
|
||||||
|
cy.wait('@searchKeyword');
|
||||||
|
|
||||||
|
cy.get('[data-testid=create-discover-option-form]')
|
||||||
|
.find('button')
|
||||||
|
.should('be.disabled');
|
||||||
|
|
||||||
|
cy.get('#data').clear();
|
||||||
|
cy.get('#data').type('christmas{enter}', { delay: 100 });
|
||||||
|
|
||||||
|
// Confirming we have some results
|
||||||
|
cy.contains('.slider-header', sliderTitle)
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]');
|
||||||
|
|
||||||
|
cy.get('[data-testid=create-discover-option-form]').submit();
|
||||||
|
|
||||||
|
cy.wait('@discoverSlider');
|
||||||
|
cy.wait('@getDiscoverSliders');
|
||||||
|
cy.wait(1000);
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.first()
|
||||||
|
.should('contain', sliderTitle);
|
||||||
|
|
||||||
|
// Make sure its still there even if we reload
|
||||||
|
cy.reload();
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-start-editing]').click();
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.first()
|
||||||
|
.should('contain', sliderTitle);
|
||||||
|
|
||||||
|
// Verify it's not rendering on our discover page (its still disabled!)
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.get('.slider-header').should('not.contain', sliderTitle);
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-start-editing]').click();
|
||||||
|
|
||||||
|
// Enable it, and check again
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.first()
|
||||||
|
.find('[role="checkbox"]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-customize-submit').click();
|
||||||
|
cy.wait('@getDiscoverSliders');
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.contains('.slider-header', sliderTitle)
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]');
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-start-editing]').click();
|
||||||
|
|
||||||
|
// let's delete it and confirm its deleted.
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.first()
|
||||||
|
.find('[data-testid=discover-slider-remove-button]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait('@discoverSlider');
|
||||||
|
cy.wait('@getDiscoverSliders');
|
||||||
|
cy.wait(1000);
|
||||||
|
|
||||||
|
cy.get('[data-testid=discover-slider-edit-mode]')
|
||||||
|
.first()
|
||||||
|
.should('not.contain', sliderTitle);
|
||||||
|
});
|
||||||
|
});
|
||||||
32
cypress/e2e/settings/general-settings.cy.ts
Normal file
32
cypress/e2e/settings/general-settings.cy.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
describe('General Settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the settings page from the home page', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.get('[data-testid=sidebar-toggle]').click();
|
||||||
|
cy.get('[data-testid=sidebar-menu-settings-mobile]').click();
|
||||||
|
|
||||||
|
cy.get('.heading').should('contain', 'General Settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('modifies setting that requires restart', () => {
|
||||||
|
cy.visit('/settings');
|
||||||
|
|
||||||
|
cy.get('#trustProxy').click();
|
||||||
|
cy.get('[data-testid=settings-main-form]').submit();
|
||||||
|
cy.get('[data-testid=modal-title]').should(
|
||||||
|
'contain',
|
||||||
|
'Server Restart Required'
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.get('[data-testid=modal-ok-button]').click();
|
||||||
|
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||||
|
|
||||||
|
cy.get('[type=checkbox]#trustProxy').click();
|
||||||
|
cy.get('[data-testid=settings-main-form]').submit();
|
||||||
|
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
28
cypress/e2e/tv-details.cy.ts
Normal file
28
cypress/e2e/tv-details.cy.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
describe('TV Details', () => {
|
||||||
|
it('loads a tv details page', () => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
// Try to load stranger things
|
||||||
|
cy.visit('/tv/66732');
|
||||||
|
|
||||||
|
cy.get('[data-testid=media-title]').should(
|
||||||
|
'contain',
|
||||||
|
'Stranger Things (2016)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows seasons and expands episodes', () => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
|
||||||
|
// Try to load stranger things
|
||||||
|
cy.visit('/tv/66732');
|
||||||
|
|
||||||
|
// intercept request for season info
|
||||||
|
cy.intercept('/api/v1/tv/66732/season/4').as('season4');
|
||||||
|
|
||||||
|
cy.contains('Season 4').should('be.visible').scrollIntoView().click();
|
||||||
|
|
||||||
|
cy.wait('@season4');
|
||||||
|
|
||||||
|
cy.contains('Chapter Nine').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
74
cypress/e2e/user/auto-request-settings.cy.ts
Normal file
74
cypress/e2e/user/auto-request-settings.cy.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
const visitUserEditPage = (email: string): void => {
|
||||||
|
cy.visit('/users');
|
||||||
|
|
||||||
|
cy.contains('[data-testid=user-list-row]', email).contains('Edit').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Auto Request Settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not see watchlist sync settings on an account without permissions', () => {
|
||||||
|
visitUserEditPage(Cypress.env('USER_EMAIL'));
|
||||||
|
|
||||||
|
cy.contains('Auto-Request Movies').should('not.exist');
|
||||||
|
cy.contains('Auto-Request Series').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see watchlist sync settings on an admin account', () => {
|
||||||
|
visitUserEditPage(Cypress.env('ADMIN_EMAIL'));
|
||||||
|
|
||||||
|
cy.contains('Auto-Request Movies').should('exist');
|
||||||
|
cy.contains('Auto-Request Series').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see auto-request settings after being given permission', () => {
|
||||||
|
visitUserEditPage(Cypress.env('USER_EMAIL'));
|
||||||
|
|
||||||
|
cy.get('[data-testid=settings-nav-desktop').contains('Permissions').click();
|
||||||
|
|
||||||
|
cy.get('#autorequest').should('not.be.checked').click();
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/user/*/settings/permissions').as('userPermissions');
|
||||||
|
|
||||||
|
cy.contains('Save Changes').click();
|
||||||
|
|
||||||
|
cy.wait('@userPermissions');
|
||||||
|
|
||||||
|
cy.reload();
|
||||||
|
|
||||||
|
cy.get('#autorequest').should('be.checked');
|
||||||
|
cy.get('#autorequestmovies').should('be.checked');
|
||||||
|
cy.get('#autorequesttv').should('be.checked');
|
||||||
|
|
||||||
|
cy.get('[data-testid=settings-nav-desktop').contains('General').click();
|
||||||
|
|
||||||
|
cy.contains('Auto-Request Movies').should('exist');
|
||||||
|
cy.contains('Auto-Request Series').should('exist');
|
||||||
|
|
||||||
|
cy.get('#watchlistSyncMovies').should('not.be.checked').click();
|
||||||
|
cy.get('#watchlistSyncTv').should('not.be.checked').click();
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/user/*/settings/main').as('userMain');
|
||||||
|
|
||||||
|
cy.contains('Save Changes').click();
|
||||||
|
|
||||||
|
cy.wait('@userMain');
|
||||||
|
|
||||||
|
cy.reload();
|
||||||
|
|
||||||
|
cy.get('#watchlistSyncMovies').should('be.checked').click();
|
||||||
|
cy.get('#watchlistSyncTv').should('be.checked').click();
|
||||||
|
|
||||||
|
cy.contains('Save Changes').click();
|
||||||
|
|
||||||
|
cy.wait('@userMain');
|
||||||
|
|
||||||
|
cy.get('[data-testid=settings-nav-desktop').contains('Permissions').click();
|
||||||
|
|
||||||
|
cy.get('#autorequest').should('be.checked').click();
|
||||||
|
|
||||||
|
cy.contains('Save Changes').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
50
cypress/e2e/user/profile.cy.ts
Normal file
50
cypress/e2e/user/profile.cy.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
describe('User Profile', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens user profile page from the home page', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.get('[data-testid=user-menu]').click();
|
||||||
|
cy.get('[data-testid=user-menu-profile]').click();
|
||||||
|
|
||||||
|
cy.get('h1').should('contain', Cypress.env('ADMIN_EMAIL'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads plex watchlist', () => {
|
||||||
|
cy.intercept('/api/v1/user/[0-9]*/watchlist', {
|
||||||
|
fixture: 'watchlist.json',
|
||||||
|
}).as('getWatchlist');
|
||||||
|
// Wait for one of the watchlist movies to resolve
|
||||||
|
cy.intercept('/api/v1/movie/361743').as('getTmdbMovie');
|
||||||
|
|
||||||
|
cy.visit('/profile');
|
||||||
|
|
||||||
|
cy.wait('@getWatchlist');
|
||||||
|
|
||||||
|
const sliderHeader = cy.contains('.slider-header', 'Plex Watchlist');
|
||||||
|
|
||||||
|
sliderHeader.scrollIntoView();
|
||||||
|
|
||||||
|
cy.wait('@getTmdbMovie');
|
||||||
|
// Wait a little longer to make sure the movie component reloaded
|
||||||
|
cy.wait(500);
|
||||||
|
|
||||||
|
sliderHeader
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]')
|
||||||
|
.first()
|
||||||
|
.trigger('mouseover')
|
||||||
|
.find('[data-testid=title-card-title]')
|
||||||
|
.invoke('text')
|
||||||
|
.then((text) => {
|
||||||
|
cy.contains('.slider-header', 'Plex Watchlist')
|
||||||
|
.next('[data-testid=media-slider]')
|
||||||
|
.find('[data-testid=title-card]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
cy.get('[data-testid=media-title]').should('contain', text);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
70
cypress/e2e/user/user-list.cy.ts
Normal file
70
cypress/e2e/user/user-list.cy.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const testUser = {
|
||||||
|
displayName: 'Test User',
|
||||||
|
emailAddress: 'test@seeerr.dev',
|
||||||
|
password: 'test1234',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('User List', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.loginAsAdmin();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the user list from the home page', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.get('[data-testid=sidebar-toggle]').click();
|
||||||
|
cy.get('[data-testid=sidebar-menu-users-mobile]').click();
|
||||||
|
|
||||||
|
cy.get('[data-testid=page-header]').should('contain', 'User List');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can find the admin user and friend user in the user list', () => {
|
||||||
|
cy.visit('/users');
|
||||||
|
|
||||||
|
cy.get('[data-testid=user-list-row]').contains(Cypress.env('ADMIN_EMAIL'));
|
||||||
|
cy.get('[data-testid=user-list-row]').contains(Cypress.env('USER_EMAIL'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can create a local user', () => {
|
||||||
|
cy.visit('/users');
|
||||||
|
|
||||||
|
cy.contains('Create Local User').click();
|
||||||
|
|
||||||
|
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
||||||
|
|
||||||
|
cy.get('#displayName').type(testUser.displayName);
|
||||||
|
cy.get('#email').type(testUser.emailAddress);
|
||||||
|
cy.get('#password').type(testUser.password);
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
|
||||||
|
|
||||||
|
cy.get('[data-testid=modal-ok-button]').click();
|
||||||
|
|
||||||
|
cy.wait('@user');
|
||||||
|
// Wait a little longer for the user list to fully re-render
|
||||||
|
cy.wait(1000);
|
||||||
|
|
||||||
|
cy.get('[data-testid=user-list-row]').contains(testUser.emailAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can delete the created local test user', () => {
|
||||||
|
cy.visit('/users');
|
||||||
|
|
||||||
|
cy.contains('[data-testid=user-list-row]', testUser.emailAddress)
|
||||||
|
.contains('Delete')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('[data-testid=modal-title]').should('contain', `Delete User`);
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
|
||||||
|
|
||||||
|
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
|
||||||
|
|
||||||
|
cy.wait('@user');
|
||||||
|
cy.wait(1000);
|
||||||
|
|
||||||
|
cy.get('[data-testid=user-list-row]')
|
||||||
|
.contains(testUser.emailAddress)
|
||||||
|
.should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
25
cypress/fixtures/watchlist.json
Normal file
25
cypress/fixtures/watchlist.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"totalPages": 1,
|
||||||
|
"totalResults": 3,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"ratingKey": "5d776be17a53e9001e732ab9",
|
||||||
|
"title": "Top Gun: Maverick",
|
||||||
|
"mediaType": "movie",
|
||||||
|
"tmdbId": 361743
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ratingKey": "5e16338fbc1372003ea68ab3",
|
||||||
|
"title": "Nope",
|
||||||
|
"mediaType": "movie",
|
||||||
|
"tmdbId": 762504
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ratingKey": "5f409b8452f200004161e126",
|
||||||
|
"title": "Hocus Pocus 2",
|
||||||
|
"mediaType": "movie",
|
||||||
|
"tmdbId": 642885
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
cypress/support/commands.ts
Normal file
35
cypress/support/commands.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
import 'cy-mobile-commands';
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', (email, password) => {
|
||||||
|
cy.session(
|
||||||
|
[email, password],
|
||||||
|
() => {
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.contains('Use your Overseerr account').click();
|
||||||
|
|
||||||
|
cy.get('[data-testid=email]').type(email);
|
||||||
|
cy.get('[data-testid=password]').type(password);
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/auth/local').as('localLogin');
|
||||||
|
cy.get('[data-testid=local-signin-button]').click();
|
||||||
|
|
||||||
|
cy.wait('@localLogin');
|
||||||
|
|
||||||
|
cy.url().should('contain', '/');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validate() {
|
||||||
|
cy.request('/api/v1/auth/me').its('status').should('eq', 200);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('loginAsAdmin', () => {
|
||||||
|
cy.login(Cypress.env('ADMIN_EMAIL'), Cypress.env('ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('loginAsUser', () => {
|
||||||
|
cy.login(Cypress.env('USER_EMAIL'), Cypress.env('USER_PASSWORD'));
|
||||||
|
});
|
||||||
7
cypress/support/e2e.ts
Normal file
7
cypress/support/e2e.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import './commands';
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
if (Cypress.env('SEED_DATABASE')) {
|
||||||
|
cy.exec('yarn cypress:prepare');
|
||||||
|
}
|
||||||
|
});
|
||||||
14
cypress/support/index.ts
Normal file
14
cypress/support/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
login(email?: string, password?: string): Chainable<Element>;
|
||||||
|
loginAsAdmin(): Chainable<Element>;
|
||||||
|
loginAsUser(): Chainable<Element>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
10
cypress/tsconfig.json
Normal file
10
cypress/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["es5", "dom"],
|
||||||
|
"types": ["cypress", "node"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
overseerr:
|
jellyseerr:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.local
|
dockerfile: Dockerfile.local
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ To use Fail2ban with Overseerr, create a new file named `overseerr.local` in you
|
|||||||
failregex = .*\[warn\]\[API\]\: Failed sign-in attempt.*"ip":"<HOST>"
|
failregex = .*\[warn\]\[API\]\: 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.
|
You can then add a jail using this filter in `jail.local`. Please see the [Fail2ban documentation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jails) for details on how to configure the jail.
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ location ^~ /overseerr {
|
|||||||
sub_filter 'href="/"' 'href="/$app"';
|
sub_filter 'href="/"' 'href="/$app"';
|
||||||
sub_filter 'href="/login"' 'href="/$app/login"';
|
sub_filter 'href="/login"' 'href="/$app/login"';
|
||||||
sub_filter 'href:"/"' 'href:"/$app"';
|
sub_filter 'href:"/"' 'href:"/$app"';
|
||||||
|
sub_filter '\/_next' '\/$app\/_next';
|
||||||
sub_filter '/_next' '/$app/_next';
|
sub_filter '/_next' '/$app/_next';
|
||||||
sub_filter '/api/v1' '/$app/api/v1';
|
sub_filter '/api/v1' '/$app/api/v1';
|
||||||
sub_filter '/login/plex/loading' '/$app/login/plex/loading';
|
sub_filter '/login/plex/loading' '/$app/login/plex/loading';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
- [LunaSea](https://docs.lunasea.app/modules/overseerr), a self-hosted controller for mobile and macOS
|
- [LunaSea](https://docs.lunasea.app/modules/overseerr), a self-hosted controller for mobile and macOS
|
||||||
- [Requestrr](https://github.com/darkalfx/requestrr/wiki/Configuring-Overseerr), a Discord chatbot
|
- [Requestrr](https://github.com/darkalfx/requestrr/wiki/Configuring-Overseerr), a Discord chatbot
|
||||||
- [Doplarr](https://github.com/kiranshila/Doplarr), a Discord request bot
|
- [Doplarr](https://github.com/kiranshila/Doplarr), a Discord request bot
|
||||||
- [Overseerr Assistant](https://github.com/RemiRigal/Overseerr-Assistant), a browser extension for requesting directly from TMDb and IMDb
|
- [Overseerr Assistant](https://github.com/RemiRigal/Overseerr-Assistant), a browser extension for requesting directly from TMDB and IMDb
|
||||||
- [ha-overseerr](https://github.com/vaparr/ha-overseerr), a custom Home Assistant component
|
- [ha-overseerr](https://github.com/vaparr/ha-overseerr), a custom Home Assistant component
|
||||||
- [OverCLIrr](https://github.com/WillFantom/OverCLIrr), a command-line tool
|
- [OverCLIrr](https://github.com/WillFantom/OverCLIrr), a command-line tool
|
||||||
- [Overseerr Exporter](https://github.com/WillFantom/overseerr-exporter), a Prometheus exporter
|
- [Overseerr Exporter](https://github.com/WillFantom/overseerr-exporter), a Prometheus exporter
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ docker run -d \
|
|||||||
--name overseerr \
|
--name overseerr \
|
||||||
-e LOG_LEVEL=debug \
|
-e LOG_LEVEL=debug \
|
||||||
-e TZ=Asia/Tokyo \
|
-e TZ=Asia/Tokyo \
|
||||||
|
-e PORT=5055 `#optional` \
|
||||||
-p 5055:5055 \
|
-p 5055:5055 \
|
||||||
-v /path/to/appdata/config:/app/config \
|
-v /path/to/appdata/config:/app/config \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
@@ -81,6 +82,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- LOG_LEVEL=debug
|
- LOG_LEVEL=debug
|
||||||
- TZ=Asia/Tokyo
|
- TZ=Asia/Tokyo
|
||||||
|
- PORT=5055 #optional
|
||||||
ports:
|
ports:
|
||||||
- 5055:5055
|
- 5055:5055
|
||||||
volumes:
|
volumes:
|
||||||
@@ -88,7 +90,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, start all services defined in the your Compose file:
|
Then, start all services defined in the Compose file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
@@ -146,8 +148,6 @@ Then, create and start the Overseerr container:
|
|||||||
docker run -d --name overseerr -e LOG_LEVEL=debug -e TZ=Asia/Tokyo -p 5055:5055 -v "overseerr-data:/app/config" --restart unless-stopped fallenbagel/jellyseerr:latest
|
docker run -d --name overseerr -e LOG_LEVEL=debug -e TZ=Asia/Tokyo -p 5055:5055 -v "overseerr-data:/app/config" --restart unless-stopped fallenbagel/jellyseerr:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
If using a named volume like above, you can safely ignore the warning about the `/app/config` folder being incorrectly mounted on the setup page.
|
|
||||||
|
|
||||||
To access the files inside the volume created above, navigate to `\\wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\overseerr-data\_data` using File Explorer.
|
To access the files inside the volume created above, navigate to `\\wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\overseerr-data\_data` using File Explorer.
|
||||||
|
|
||||||
{% hint style="info" %}
|
{% hint style="info" %}
|
||||||
@@ -155,7 +155,7 @@ Docker on Windows works differently than it does on Linux; it runs Docker inside
|
|||||||
|
|
||||||
**If you must run Docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host.** (This also applies to other containers with SQLite databases.)
|
**If you must run Docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host.** (This also applies to other containers with SQLite databases.)
|
||||||
|
|
||||||
Named volumes, like in the example commands above, are automatically mounted inside the VM.
|
Named volumes, like in the example commands above, are automatically mounted inside the VM. Therefore the warning on the setup about the `/app/config` folder being incorrectly mounted page should be ignored.
|
||||||
{% endhint %}
|
{% endhint %}
|
||||||
|
|
||||||
## Linux
|
## Linux
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Overseerr currently supports the following agents:
|
|||||||
- New Plex TV
|
- New Plex TV
|
||||||
- Legacy Plex TV
|
- Legacy Plex TV
|
||||||
- TheTVDB
|
- TheTVDB
|
||||||
- TMDb
|
- TMDB
|
||||||
- [HAMA](https://github.com/ZeroQI/Hama.bundle)
|
- [HAMA](https://github.com/ZeroQI/Hama.bundle)
|
||||||
|
|
||||||
Please verify that your library is using one of the agents previously listed.
|
Please verify that your library is using one of the agents previously listed.
|
||||||
@@ -67,7 +67,7 @@ You can also perform the following to verify the media item has a GUID Overseerr
|
|||||||
1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**.
|
1. Go to the media item in Plex and **"Get info"** and click on **"View XML"**.
|
||||||
2. Verify that the media item's GUID follows one of the below formats:
|
2. Verify that the media item's GUID follows one of the below formats:
|
||||||
|
|
||||||
1. TMDb agent `guid="com.plexapp.agents.themoviedb://1705"`
|
1. TMDB agent `guid="com.plexapp.agents.themoviedb://1705"`
|
||||||
2. New Plex Movie agent `<Guid id="tmdb://464052"/>`
|
2. New Plex Movie agent `<Guid id="tmdb://464052"/>`
|
||||||
3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"`
|
3. TheTVDB agent `guid="com.plexapp.agents.thetvdb://78874/1/1"`
|
||||||
4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"`
|
4. Legacy Plex Movie agent `guid="com.plexapp.agents.imdb://tt0765446"`
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ These following special variables are only included in media-related notificatio
|
|||||||
| Variable | Value |
|
| Variable | Value |
|
||||||
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
|
| -------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||||
| `{{media_type}}` | The media type (`movie` or `tv`) |
|
| `{{media_type}}` | The media type (`movie` or `tv`) |
|
||||||
| `{{media_tmdbid}}` | The media's TMDb ID |
|
| `{{media_tmdbid}}` | The media's TMDB ID |
|
||||||
| `{{media_tvdbid}}` | The media's TheTVDB ID |
|
| `{{media_tvdbid}}` | The media's TheTVDB ID |
|
||||||
| `{{media_status}}` | The media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
|
| `{{media_status}}` | The media's availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
|
||||||
| `{{media_status4k}}` | The media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
|
| `{{media_status4k}}` | The media's 4K availability status (`UNKNOWN`, `PENDING`, `PROCESSING`, `PARTIALLY_AVAILABLE`, or `AVAILABLE`) |
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ If you enable this setting and find yourself unable to access Overseerr, you can
|
|||||||
|
|
||||||
This setting is **disabled** by default.
|
This setting is **disabled** by default.
|
||||||
|
|
||||||
|
### Enable Image Caching
|
||||||
|
|
||||||
|
When enabled, Overseerr will proxy and cache images from pre-configured sources (such as TMDB). This can use a significant amount of disk space.
|
||||||
|
|
||||||
|
Images are saved in the `config/cache/images` and stale images are cleared out every 24 hours.
|
||||||
|
|
||||||
|
You should enable this if you are having issues with loading images directly from TMDB in your browser.
|
||||||
|
|
||||||
### Display Language
|
### Display Language
|
||||||
|
|
||||||
Set the default display language for Overseerr. Users can override this setting in their user settings.
|
Set the default display language for Overseerr. Users can override this setting in their user settings.
|
||||||
|
|||||||
21
merged-prettier-plugin.js
Normal file
21
merged-prettier-plugin.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
const tailwind = require('prettier-plugin-tailwindcss');
|
||||||
|
const organizeImports = require('prettier-plugin-organize-imports');
|
||||||
|
|
||||||
|
const combinedFormatter = {
|
||||||
|
...tailwind,
|
||||||
|
parsers: {
|
||||||
|
...tailwind.parsers,
|
||||||
|
...Object.keys(organizeImports.parsers).reduce((acc, key) => {
|
||||||
|
acc[key] = {
|
||||||
|
...tailwind.parsers[key],
|
||||||
|
preprocess(code, options) {
|
||||||
|
return organizeImports.parsers[key].preprocess(code, options);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = combinedFormatter;
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('next').NextConfig}
|
||||||
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
commitTag: process.env.COMMIT_TAG || 'local',
|
commitTag: process.env.COMMIT_TAG || 'local',
|
||||||
@@ -18,4 +21,8 @@ module.exports = {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
scrollRestoration: true,
|
||||||
|
largePageDataBytes: 256000,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
289
package.json
289
package.json
@@ -1,20 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "jellyseerr",
|
"name": "jellyseerr",
|
||||||
"version": "1.1.1",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"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",
|
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
|
||||||
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates",
|
"build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && tsc-alias -p server/tsconfig.json",
|
||||||
"build:next": "next build",
|
"build:next": "next build",
|
||||||
"build": "yarn build:next && yarn build:server",
|
"build": "yarn build:next && yarn build:server",
|
||||||
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\"",
|
"lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache",
|
||||||
|
"lintfix": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --fix",
|
||||||
"start": "NODE_ENV=production node dist/index.js",
|
"start": "NODE_ENV=production node dist/index.js",
|
||||||
"i18n:extract": "extract-messages -l=en -o src/i18n/locale -d en --flat true --overwriteDefault true \"./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/typeorm/cli.js migration:generate",
|
"migration:generate": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:generate -d server/datasource.ts",
|
||||||
"migration:create": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create",
|
"migration:create": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:create -d server/datasource.ts",
|
||||||
"migration:run": "ts-node --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run",
|
"migration:run": "ts-node -r tsconfig-paths/register --project server/tsconfig.json ./node_modules/typeorm/cli.js migration:run -d server/datasource.ts",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --loglevel warn --write --cache .",
|
||||||
"prepare": "husky install"
|
"format:check": "prettier --check --cache .",
|
||||||
|
"typecheck": "yarn typecheck:server && yarn typecheck:client",
|
||||||
|
"typecheck:server": "tsc --project server/tsconfig.json --noEmit",
|
||||||
|
"typecheck:client": "tsc --noEmit",
|
||||||
|
"prepare": "husky install",
|
||||||
|
"cypress:open": "cypress open",
|
||||||
|
"cypress:prepare": "ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/scripts/prepareTestDb.ts",
|
||||||
|
"cypress:build": "yarn build && yarn cypress:prepare"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -22,129 +30,148 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.5.0",
|
"@formatjs/intl-displaynames": "6.2.6",
|
||||||
"@heroicons/react": "^1.0.6",
|
"@formatjs/intl-locale": "3.1.1",
|
||||||
"@supercharge/request-ip": "^1.2.0",
|
"@formatjs/intl-pluralrules": "5.1.10",
|
||||||
"@svgr/webpack": "^6.2.1",
|
"@formatjs/intl-utils": "3.8.4",
|
||||||
"@tanem/react-nprogress": "^4.0.10",
|
"@headlessui/react": "1.7.12",
|
||||||
"ace-builds": "^1.4.14",
|
"@heroicons/react": "2.0.16",
|
||||||
"axios": "^0.26.1",
|
"@supercharge/request-ip": "1.2.0",
|
||||||
"bcrypt": "^5.0.1",
|
"@svgr/webpack": "6.5.1",
|
||||||
"bowser": "^2.11.0",
|
"@tanem/react-nprogress": "5.0.30",
|
||||||
"connect-typeorm": "^1.1.4",
|
"ace-builds": "1.15.2",
|
||||||
"cookie-parser": "^1.4.6",
|
"axios": "1.3.4",
|
||||||
"copy-to-clipboard": "^3.3.1",
|
"axios-rate-limit": "1.3.0",
|
||||||
"country-flag-icons": "^1.4.21",
|
"bcrypt": "5.1.0",
|
||||||
"csurf": "^1.11.0",
|
"bowser": "2.11.0",
|
||||||
"email-templates": "^8.0.10",
|
"connect-typeorm": "1.1.4",
|
||||||
"email-validator": "^2.0.4",
|
"cookie-parser": "1.4.6",
|
||||||
"express": "^4.17.3",
|
"copy-to-clipboard": "3.3.3",
|
||||||
"express-openapi-validator": "^4.13.6",
|
"country-flag-icons": "1.5.5",
|
||||||
"express-rate-limit": "^6.3.0",
|
"cronstrue": "2.23.0",
|
||||||
"express-session": "^1.17.2",
|
"csurf": "1.11.0",
|
||||||
"formik": "^2.2.9",
|
"date-fns": "2.29.3",
|
||||||
"gravatar-url": "^3.1.0",
|
"dayjs": "1.11.7",
|
||||||
"intl": "^1.2.5",
|
"email-templates": "9.0.0",
|
||||||
"lodash": "^4.17.21",
|
"email-validator": "2.0.4",
|
||||||
"next": "12.1.0",
|
"express": "4.18.2",
|
||||||
"node-cache": "^5.1.2",
|
"express-openapi-validator": "4.13.8",
|
||||||
"node-gyp": "^9.0.0",
|
"express-rate-limit": "6.7.0",
|
||||||
"node-schedule": "^2.1.0",
|
"express-session": "1.17.3",
|
||||||
"nodemailer": "^6.7.2",
|
"formik": "2.2.9",
|
||||||
"openpgp": "^5.2.0",
|
"gravatar-url": "3.1.0",
|
||||||
"plex-api": "^5.3.2",
|
"intl": "1.2.5",
|
||||||
"pug": "^3.0.2",
|
"lodash": "4.17.21",
|
||||||
"react": "17.0.2",
|
"next": "12.3.4",
|
||||||
"react-ace": "^9.5.0",
|
"node-cache": "5.1.2",
|
||||||
"react-animate-height": "^2.0.23",
|
"node-gyp": "9.3.1",
|
||||||
"react-dom": "17.0.2",
|
"node-schedule": "2.1.1",
|
||||||
"react-intersection-observer": "^8.33.1",
|
"nodemailer": "6.9.1",
|
||||||
"react-intl": "5.24.7",
|
"openpgp": "5.7.0",
|
||||||
"react-markdown": "^8.0.0",
|
"plex-api": "5.3.2",
|
||||||
"react-select": "^5.2.2",
|
"pug": "3.0.2",
|
||||||
"react-spring": "^9.4.4",
|
"react": "18.2.0",
|
||||||
"react-toast-notifications": "^2.5.1",
|
"react-ace": "10.1.0",
|
||||||
"react-transition-group": "^4.4.2",
|
"react-animate-height": "2.1.2",
|
||||||
"react-truncate-markup": "^5.1.0",
|
"react-aria": "3.23.0",
|
||||||
"react-use-clipboard": "1.0.7",
|
"react-dom": "18.2.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"react-intersection-observer": "9.4.3",
|
||||||
"secure-random-password": "^0.2.3",
|
"react-intl": "6.2.10",
|
||||||
"semver": "^7.3.5",
|
"react-markdown": "8.0.5",
|
||||||
"sqlite3": "^5.0.2",
|
"react-popper-tooltip": "4.4.2",
|
||||||
"swagger-ui-express": "^4.3.0",
|
"react-select": "5.7.0",
|
||||||
"swr": "^1.2.2",
|
"react-spring": "9.7.1",
|
||||||
"typeorm": "0.2.45",
|
"react-tailwindcss-datepicker-sct": "1.3.4",
|
||||||
"web-push": "^3.4.5",
|
"react-toast-notifications": "2.5.1",
|
||||||
"winston": "^3.6.0",
|
"react-truncate-markup": "5.1.2",
|
||||||
"winston-daily-rotate-file": "^4.6.1",
|
"react-use-clipboard": "1.0.9",
|
||||||
"xml2js": "^0.4.23",
|
"reflect-metadata": "0.1.13",
|
||||||
"yamljs": "^0.3.0",
|
"secure-random-password": "0.2.3",
|
||||||
"yup": "^0.32.11"
|
"semver": "7.3.8",
|
||||||
|
"sqlite3": "5.1.4",
|
||||||
|
"swagger-ui-express": "4.6.2",
|
||||||
|
"swr": "2.0.4",
|
||||||
|
"typeorm": "0.3.12",
|
||||||
|
"web-push": "3.5.0",
|
||||||
|
"winston": "3.8.2",
|
||||||
|
"winston-daily-rotate-file": "4.7.1",
|
||||||
|
"xml2js": "0.4.23",
|
||||||
|
"yamljs": "0.3.0",
|
||||||
|
"yup": "0.32.11",
|
||||||
|
"zod": "3.20.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.17.6",
|
"@babel/cli": "7.21.0",
|
||||||
"@commitlint/cli": "^16.2.1",
|
"@commitlint/cli": "17.4.4",
|
||||||
"@commitlint/config-conventional": "^16.2.1",
|
"@commitlint/config-conventional": "17.4.4",
|
||||||
"@next/eslint-plugin-next": "^12.1.6",
|
"@semantic-release/changelog": "6.0.2",
|
||||||
"@semantic-release/changelog": "^6.0.1",
|
"@semantic-release/commit-analyzer": "9.0.2",
|
||||||
"@semantic-release/commit-analyzer": "^9.0.2",
|
"@semantic-release/exec": "6.0.3",
|
||||||
"@semantic-release/exec": "^6.0.3",
|
"@semantic-release/git": "10.0.1",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@tailwindcss/aspect-ratio": "0.4.2",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.0",
|
"@tailwindcss/forms": "0.5.3",
|
||||||
"@tailwindcss/forms": "^0.5.0",
|
"@tailwindcss/typography": "0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.2",
|
"@types/bcrypt": "5.0.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/cookie-parser": "1.4.3",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/country-flag-icons": "1.2.0",
|
||||||
"@types/country-flag-icons": "^1.2.0",
|
"@types/csurf": "1.11.2",
|
||||||
"@types/csurf": "^1.11.2",
|
"@types/email-templates": "8.0.4",
|
||||||
"@types/email-templates": "^8.0.4",
|
"@types/express": "4.17.17",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express-session": "1.17.6",
|
||||||
"@types/express-session": "^1.17.4",
|
"@types/lodash": "4.14.191",
|
||||||
"@types/lodash": "^4.14.179",
|
"@types/node": "17.0.36",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node-schedule": "2.1.0",
|
||||||
"@types/node-schedule": "^1.3.2",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/nodemailer": "^6.4.4",
|
"@types/react": "18.0.28",
|
||||||
"@types/react": "^17.0.40",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/react-dom": "^17.0.13",
|
"@types/react-transition-group": "4.4.5",
|
||||||
"@types/react-transition-group": "^4.4.4",
|
"@types/secure-random-password": "0.2.1",
|
||||||
"@types/secure-random-password": "^0.2.1",
|
"@types/semver": "7.3.13",
|
||||||
"@types/semver": "^7.3.9",
|
"@types/swagger-ui-express": "4.1.3",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/web-push": "^3.3.2",
|
"@types/xml2js": "0.4.11",
|
||||||
"@types/xml2js": "^0.4.9",
|
"@types/yamljs": "0.2.31",
|
||||||
"@types/yamljs": "^0.2.31",
|
"@types/yup": "0.29.14",
|
||||||
"@types/yup": "^0.29.13",
|
"@typescript-eslint/eslint-plugin": "5.54.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.14.0",
|
"@typescript-eslint/parser": "5.54.0",
|
||||||
"@typescript-eslint/parser": "^5.14.0",
|
"autoprefixer": "10.4.13",
|
||||||
"autoprefixer": "^10.4.2",
|
"babel-plugin-react-intl": "8.2.25",
|
||||||
"babel-plugin-react-intl": "^8.2.25",
|
"babel-plugin-react-intl-auto": "3.3.0",
|
||||||
"babel-plugin-react-intl-auto": "^3.3.0",
|
"commitizen": "4.3.0",
|
||||||
"commitizen": "^4.2.4",
|
"copyfiles": "2.4.1",
|
||||||
"copyfiles": "^2.4.1",
|
"cy-mobile-commands": "0.3.0",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
"cypress": "12.7.0",
|
||||||
"eslint": "^8.11.0",
|
"cz-conventional-changelog": "3.3.0",
|
||||||
"eslint-config-next": "^12.1.0",
|
"eslint": "8.35.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-next": "12.3.4",
|
||||||
"eslint-plugin-formatjs": "^3.0.0",
|
"eslint-config-prettier": "8.6.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-formatjs": "4.9.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||||
"eslint-plugin-react": "^7.29.3",
|
"eslint-plugin-no-relative-import-paths": "1.5.2",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"husky": "^7.0.4",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"lint-staged": "^12.3.5",
|
"extract-react-intl-messages": "4.1.1",
|
||||||
"nodemon": "^2.0.15",
|
"husky": "8.0.3",
|
||||||
"postcss": "^8.4.8",
|
"lint-staged": "13.1.2",
|
||||||
"prettier": "^2.5.1",
|
"nodemon": "2.0.20",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.8",
|
"postcss": "8.4.21",
|
||||||
"semantic-release": "^19.0.2",
|
"prettier": "2.8.4",
|
||||||
"semantic-release-docker-buildx": "^1.0.1",
|
"prettier-plugin-organize-imports": "3.2.2",
|
||||||
"tailwindcss": "^3.0.23",
|
"prettier-plugin-tailwindcss": "0.2.3",
|
||||||
"ts-node": "^10.7.0",
|
"semantic-release": "19.0.5",
|
||||||
"typescript": "^4.6.2"
|
"semantic-release-docker-buildx": "1.0.1",
|
||||||
|
"tailwindcss": "3.2.7",
|
||||||
|
"ts-node": "10.9.1",
|
||||||
|
"tsc-alias": "1.8.2",
|
||||||
|
"tsconfig-paths": "4.1.2",
|
||||||
|
"typescript": "4.9.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"sqlite3/node-gyp": "^8.4.1"
|
"sqlite3/node-gyp": "8.4.1",
|
||||||
|
"@types/react": "18.0.28",
|
||||||
|
"@types/react-dom": "18.0.11",
|
||||||
|
"@types/express-session": "1.17.6"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
@@ -165,10 +192,6 @@
|
|||||||
"@commitlint/config-conventional"
|
"@commitlint/config-conventional"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": {
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
},
|
|
||||||
"release": {
|
"release": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@semantic-release/commit-analyzer",
|
"@semantic-release/commit-analyzer",
|
||||||
@@ -206,7 +229,7 @@
|
|||||||
{
|
{
|
||||||
"path": "semantic-release-docker-buildx",
|
"path": "semantic-release-docker-buildx",
|
||||||
"buildArgs": {
|
"buildArgs": {
|
||||||
"COMMIT_TAG": "$GITHUB_SHA"
|
"COMMIT_TAG": "$GIT_SHA"
|
||||||
},
|
},
|
||||||
"imageNames": [
|
"imageNames": [
|
||||||
"fallenbagel/jellyseerr"
|
"fallenbagel/jellyseerr"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: #6366F1;
|
color: #6366f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<!-- Inline the page's JavaScript file. -->
|
<!-- Inline the page's JavaScript file. -->
|
||||||
<script>
|
<script>
|
||||||
// Manual reload feature.
|
// Manual reload feature.
|
||||||
document.querySelector("button").addEventListener("click", () => {
|
document.querySelector('button').addEventListener('click', () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
[ZoneTransfer]
|
|
||||||
LastWriterPackageFamilyName=Microsoft.ScreenSketch_8wekyb3d8bbwe
|
|
||||||
ZoneId=3
|
|
||||||
74
public/sw.js
74
public/sw.js
@@ -4,30 +4,30 @@
|
|||||||
// This variable is intentionally declared and unused.
|
// This variable is intentionally declared and unused.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const OFFLINE_VERSION = 3;
|
const OFFLINE_VERSION = 3;
|
||||||
const CACHE_NAME = "offline";
|
const CACHE_NAME = 'offline';
|
||||||
// Customize this with a different URL if needed.
|
// Customize this with a different URL if needed.
|
||||||
const OFFLINE_URL = "/offline.html";
|
const OFFLINE_URL = '/offline.html';
|
||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
(async () => {
|
(async () => {
|
||||||
const cache = await caches.open(CACHE_NAME);
|
const cache = await caches.open(CACHE_NAME);
|
||||||
// Setting {cache: 'reload'} in the new request will ensure that the
|
// Setting {cache: 'reload'} in the new request will ensure that the
|
||||||
// response isn't fulfilled from the HTTP cache; i.e., it will be from
|
// response isn't fulfilled from the HTTP cache; i.e., it will be from
|
||||||
// the network.
|
// the network.
|
||||||
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
|
await cache.add(new Request(OFFLINE_URL, { cache: 'reload' }));
|
||||||
})()
|
})()
|
||||||
);
|
);
|
||||||
// Force the waiting service worker to become the active service worker.
|
// Force the waiting service worker to become the active service worker.
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("activate", (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
(async () => {
|
(async () => {
|
||||||
// Enable navigation preload if it's supported.
|
// Enable navigation preload if it's supported.
|
||||||
// See https://developers.google.com/web/updates/2017/02/navigation-preload
|
// See https://developers.google.com/web/updates/2017/02/navigation-preload
|
||||||
if ("navigationPreload" in self.registration) {
|
if ('navigationPreload' in self.registration) {
|
||||||
await self.registration.navigationPreload.enable();
|
await self.registration.navigationPreload.enable();
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -37,10 +37,10 @@ self.addEventListener("activate", (event) => {
|
|||||||
clients.claim();
|
clients.claim();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
// We only want to call event.respondWith() if this is a navigation request
|
// We only want to call event.respondWith() if this is a navigation request
|
||||||
// for an HTML page.
|
// for an HTML page.
|
||||||
if (event.request.mode === "navigate") {
|
if (event.request.mode === 'navigate') {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -59,7 +59,7 @@ self.addEventListener("fetch", (event) => {
|
|||||||
// If fetch() returns a valid HTTP response with a response code in
|
// If fetch() returns a valid HTTP response with a response code in
|
||||||
// the 4xx or 5xx range, the catch() will NOT be called.
|
// the 4xx or 5xx range, the catch() will NOT be called.
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("Fetch failed; returning offline page instead.", error);
|
console.log('Fetch failed; returning offline page instead.', error);
|
||||||
|
|
||||||
const cache = await caches.open(CACHE_NAME);
|
const cache = await caches.open(CACHE_NAME);
|
||||||
const cachedResponse = await cache.match(OFFLINE_URL);
|
const cachedResponse = await cache.match(OFFLINE_URL);
|
||||||
@@ -85,15 +85,13 @@ self.addEventListener('push', (event) => {
|
|||||||
requestId: payload.requestId,
|
requestId: payload.requestId,
|
||||||
},
|
},
|
||||||
actions: [],
|
actions: [],
|
||||||
}
|
};
|
||||||
|
|
||||||
if (payload.actionUrl){
|
if (payload.actionUrl) {
|
||||||
options.actions.push(
|
options.actions.push({
|
||||||
{
|
action: 'view',
|
||||||
action: 'view',
|
title: payload.actionUrlTitle ?? 'View',
|
||||||
title: payload.actionUrlTitle ?? 'View',
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.notificationType === 'MEDIA_PENDING') {
|
if (payload.notificationType === 'MEDIA_PENDING') {
|
||||||
@@ -109,27 +107,29 @@ self.addEventListener('push', (event) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil(self.registration.showNotification(payload.subject, options));
|
||||||
self.registration.showNotification(payload.subject, options)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('notificationclick', (event) => {
|
self.addEventListener(
|
||||||
const notificationData = event.notification.data;
|
'notificationclick',
|
||||||
|
(event) => {
|
||||||
|
const notificationData = event.notification.data;
|
||||||
|
|
||||||
event.notification.close();
|
event.notification.close();
|
||||||
|
|
||||||
if (event.action === 'approve') {
|
if (event.action === 'approve') {
|
||||||
fetch(`/api/v1/request/${notificationData.requestId}/approve`, {
|
fetch(`/api/v1/request/${notificationData.requestId}/approve`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
} else if (event.action === 'decline') {
|
} else if (event.action === 'decline') {
|
||||||
fetch(`/api/v1/request/${notificationData.requestId}/decline`, {
|
fetch(`/api/v1/request/${notificationData.requestId}/decline`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notificationData.actionUrl) {
|
if (notificationData.actionUrl) {
|
||||||
clients.openWindow(notificationData.actionUrl);
|
clients.openWindow(notificationData.actionUrl);
|
||||||
}
|
}
|
||||||
}, false);
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|||||||
21
renovate.json
Normal file
21
renovate.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:js-app",
|
||||||
|
"group:allNonMajor",
|
||||||
|
"docker:disableMajor",
|
||||||
|
"helpers:disableTypesNodeMajor"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["github-actions"],
|
||||||
|
"groupName": "GitHub Actions",
|
||||||
|
"groupSlug": "github-actions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchPackageNames": ["node"],
|
||||||
|
"groupName": "Node.js",
|
||||||
|
"groupSlug": "node"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import logger from '@server/logger';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import xml2js from 'xml2js';
|
|
||||||
import fs, { promises as fsp } from 'fs';
|
import fs, { promises as fsp } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import logger from '../logger';
|
import xml2js from 'xml2js';
|
||||||
|
|
||||||
const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds
|
const UPDATE_INTERVAL_MSEC = 24 * 3600 * 1000; // how often to download new mapping in milliseconds
|
||||||
// originally at https://raw.githubusercontent.com/ScudLee/anime-lists/master/anime-list.xml
|
// originally at https://raw.githubusercontent.com/ScudLee/anime-lists/master/anime-list.xml
|
||||||
@@ -14,7 +14,7 @@ const LOCAL_PATH = process.env.CONFIG_DIRECTORY
|
|||||||
|
|
||||||
const mappingRegexp = new RegExp(/;[0-9]+-([0-9]+)/g);
|
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/
|
// https://github.com/Anime-Lists/anime-lists/
|
||||||
|
|
||||||
interface AnimeMapping {
|
interface AnimeMapping {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
import NodeCache from 'node-cache';
|
import axios from 'axios';
|
||||||
|
import rateLimit from 'axios-rate-limit';
|
||||||
|
import type NodeCache from 'node-cache';
|
||||||
|
|
||||||
// 5 minute default TTL (in seconds)
|
// 5 minute default TTL (in seconds)
|
||||||
const DEFAULT_TTL = 300;
|
const DEFAULT_TTL = 300;
|
||||||
@@ -10,6 +12,10 @@ const DEFAULT_ROLLING_BUFFER = 10000;
|
|||||||
interface ExternalAPIOptions {
|
interface ExternalAPIOptions {
|
||||||
nodeCache?: NodeCache;
|
nodeCache?: NodeCache;
|
||||||
headers?: Record<string, unknown>;
|
headers?: Record<string, unknown>;
|
||||||
|
rateLimit?: {
|
||||||
|
maxRPS: number;
|
||||||
|
maxRequests: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExternalAPI {
|
class ExternalAPI {
|
||||||
@@ -31,6 +37,14 @@ class ExternalAPI {
|
|||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options.rateLimit) {
|
||||||
|
this.axios = rateLimit(this.axios, {
|
||||||
|
maxRequests: options.rateLimit.maxRequests,
|
||||||
|
maxRPS: options.rateLimit.maxRPS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
this.cache = options.nodeCache;
|
this.cache = options.nodeCache;
|
||||||
}
|
}
|
||||||
@@ -55,6 +69,30 @@ class ExternalAPI {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async post<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
ttl?: number
|
||||||
|
): Promise<T> {
|
||||||
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
|
config: config?.params,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
|
if (cachedItem) {
|
||||||
|
return cachedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.axios.post<T>(endpoint, data, config);
|
||||||
|
|
||||||
|
if (this.cache) {
|
||||||
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
protected async getRolling<T>(
|
protected async getRolling<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
config?: AxiosRequestConfig,
|
config?: AxiosRequestConfig,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import cacheManager from '../lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import logger from '../logger';
|
import logger from '@server/logger';
|
||||||
import ExternalAPI from './externalapi';
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
interface GitHubRelease {
|
interface GitHubRelease {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import availabilitySync from '@server/lib/availabilitySync';
|
||||||
import logger from '../logger';
|
import logger from '@server/logger';
|
||||||
|
import type { AxiosInstance } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export interface JellyfinUserResponse {
|
export interface JellyfinUserResponse {
|
||||||
Name: string;
|
Name: string;
|
||||||
@@ -16,7 +18,7 @@ export interface JellyfinLoginResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface JellyfinUserListResponse {
|
export interface JellyfinUserListResponse {
|
||||||
users: Array<JellyfinUserResponse>;
|
users: JellyfinUserResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JellyfinLibrary {
|
export interface JellyfinLibrary {
|
||||||
@@ -37,6 +39,7 @@ export interface JellyfinLibraryItem {
|
|||||||
SeasonId?: string;
|
SeasonId?: string;
|
||||||
SeasonName?: string;
|
SeasonName?: string;
|
||||||
IndexNumber?: number;
|
IndexNumber?: number;
|
||||||
|
IndexNumberEnd?: number;
|
||||||
ParentIndexNumber?: number;
|
ParentIndexNumber?: number;
|
||||||
MediaType: string;
|
MediaType: string;
|
||||||
}
|
}
|
||||||
@@ -169,6 +172,9 @@ class JellyfinAPI {
|
|||||||
|
|
||||||
public async getLibraries(): Promise<JellyfinLibrary[]> {
|
public async getLibraries(): Promise<JellyfinLibrary[]> {
|
||||||
try {
|
try {
|
||||||
|
// TODO: Try to fix automatic grouping without fucking up LDAP users
|
||||||
|
// const libraries = await this.axios.get<any>('/Library/VirtualFolders');
|
||||||
|
|
||||||
const account = await this.axios.get<any>(
|
const account = await this.axios.get<any>(
|
||||||
`/Users/${this.userId ?? 'Me'}/Views`
|
`/Users/${this.userId ?? 'Me'}/Views`
|
||||||
);
|
);
|
||||||
@@ -177,8 +183,10 @@ class JellyfinAPI {
|
|||||||
(Item: any) => {
|
(Item: any) => {
|
||||||
return (
|
return (
|
||||||
Item.Type === 'CollectionFolder' &&
|
Item.Type === 'CollectionFolder' &&
|
||||||
(Item.CollectionType === 'tvshows' ||
|
Item.CollectionType !== 'music' &&
|
||||||
Item.CollectionType === 'movies')
|
Item.CollectionType !== 'books' &&
|
||||||
|
Item.CollectionType !== 'musicvideos' &&
|
||||||
|
Item.CollectionType !== 'homevideos'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
).map((Item: any) => {
|
).map((Item: any) => {
|
||||||
@@ -203,7 +211,7 @@ class JellyfinAPI {
|
|||||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const contents = await this.axios.get<any>(
|
const contents = await this.axios.get<any>(
|
||||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie&Recursive=true&StartIndex=0&ParentId=${id}`
|
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return contents.data.Items.filter(
|
return contents.data.Items.filter(
|
||||||
@@ -234,7 +242,9 @@ class JellyfinAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> {
|
public async getItemData(
|
||||||
|
id: string
|
||||||
|
): Promise<JellyfinLibraryItemExtended | undefined> {
|
||||||
try {
|
try {
|
||||||
const contents = await this.axios.get<any>(
|
const contents = await this.axios.get<any>(
|
||||||
`/Users/${this.userId}/Items/${id}`
|
`/Users/${this.userId}/Items/${id}`
|
||||||
@@ -242,6 +252,11 @@ class JellyfinAPI {
|
|||||||
|
|
||||||
return contents.data;
|
return contents.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (availabilitySync.running) {
|
||||||
|
if (e.response && e.response.status === 500) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
`Something went wrong while getting library content from the Jellyfin server: ${e.message}`,
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API' }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import type { Library, PlexSettings } from '@server/lib/settings';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
import NodePlexAPI from 'plex-api';
|
import NodePlexAPI from 'plex-api';
|
||||||
import { getSettings, Library, PlexSettings } from '../lib/settings';
|
|
||||||
import logger from '../logger';
|
|
||||||
|
|
||||||
export interface PlexLibraryItem {
|
export interface PlexLibraryItem {
|
||||||
ratingKey: string;
|
ratingKey: string;
|
||||||
@@ -130,7 +131,6 @@ class PlexAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
public async getStatus() {
|
public async getStatus() {
|
||||||
return await this.plexClient.query('/');
|
return await this.plexClient.query('/');
|
||||||
}
|
}
|
||||||
@@ -226,12 +226,17 @@ class PlexAPI {
|
|||||||
id: string,
|
id: string,
|
||||||
options: { addedAt: number } = {
|
options: { addedAt: number } = {
|
||||||
addedAt: Date.now() - 1000 * 60 * 60,
|
addedAt: Date.now() - 1000 * 60 * 60,
|
||||||
}
|
},
|
||||||
|
mediaType: 'movie' | 'show'
|
||||||
): Promise<PlexLibraryItem[]> {
|
): Promise<PlexLibraryItem[]> {
|
||||||
const response = await this.plexClient.query<PlexLibraryResponse>({
|
const response = await this.plexClient.query<PlexLibraryResponse>({
|
||||||
uri: `/library/sections/${id}/all?sort=addedAt%3Adesc&addedAt>>=${Math.floor(
|
uri: `/library/sections/${id}/all?type=${
|
||||||
options.addedAt / 1000
|
mediaType === 'show' ? '4' : '1'
|
||||||
)}`,
|
}&sort=addedAt%3Adesc&addedAt>>=${Math.floor(options.addedAt / 1000)}`,
|
||||||
|
extraHeaders: {
|
||||||
|
'X-Plex-Container-Start': `0`,
|
||||||
|
'X-Plex-Container-Size': `500`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.MediaContainer.Metadata;
|
return response.MediaContainer.Metadata;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
||||||
|
import cacheManager from '@server/lib/cache';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
import xml2js from 'xml2js';
|
import xml2js from 'xml2js';
|
||||||
import { PlexDevice } from '../interfaces/api/plexInterfaces';
|
import ExternalAPI from './externalapi';
|
||||||
import { getSettings } from '../lib/settings';
|
|
||||||
import logger from '../logger';
|
|
||||||
|
|
||||||
interface PlexAccountResponse {
|
interface PlexAccountResponse {
|
||||||
user: PlexUser;
|
user: PlexUser;
|
||||||
@@ -81,21 +82,6 @@ interface ServerResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FriendResponse {
|
|
||||||
MediaContainer: {
|
|
||||||
User: {
|
|
||||||
$: {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
thumb: string;
|
|
||||||
};
|
|
||||||
Server?: ServerResponse[];
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UsersResponse {
|
interface UsersResponse {
|
||||||
MediaContainer: {
|
MediaContainer: {
|
||||||
User: {
|
User: {
|
||||||
@@ -111,20 +97,54 @@ interface UsersResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlexTvAPI {
|
interface WatchlistResponse {
|
||||||
|
MediaContainer: {
|
||||||
|
totalSize: number;
|
||||||
|
Metadata?: {
|
||||||
|
ratingKey: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetadataResponse {
|
||||||
|
MediaContainer: {
|
||||||
|
Metadata: {
|
||||||
|
ratingKey: string;
|
||||||
|
type: 'movie' | 'show';
|
||||||
|
title: string;
|
||||||
|
Guid: {
|
||||||
|
id: `imdb://tt${number}` | `tmdb://${number}` | `tvdb://${number}`;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlexWatchlistItem {
|
||||||
|
ratingKey: string;
|
||||||
|
tmdbId: number;
|
||||||
|
tvdbId?: number;
|
||||||
|
type: 'movie' | 'show';
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlexTvAPI extends ExternalAPI {
|
||||||
private authToken: string;
|
private authToken: string;
|
||||||
private axios: AxiosInstance;
|
|
||||||
|
|
||||||
constructor(authToken: string) {
|
constructor(authToken: string) {
|
||||||
|
super(
|
||||||
|
'https://plex.tv',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-Plex-Token': authToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
nodeCache: cacheManager.getCache('plextv').data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.authToken = authToken;
|
this.authToken = authToken;
|
||||||
this.axios = axios.create({
|
|
||||||
baseURL: 'https://plex.tv',
|
|
||||||
headers: {
|
|
||||||
'X-Plex-Token': this.authToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getDevices(): Promise<PlexDevice[]> {
|
public async getDevices(): Promise<PlexDevice[]> {
|
||||||
@@ -199,19 +219,6 @@ class PlexTvAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFriends(): Promise<FriendResponse> {
|
|
||||||
const response = await this.axios.get('/pms/friends/all', {
|
|
||||||
transformResponse: [],
|
|
||||||
responseType: 'text',
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsedXml = (await xml2js.parseStringPromise(
|
|
||||||
response.data
|
|
||||||
)) as FriendResponse;
|
|
||||||
|
|
||||||
return parsedXml;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkUserAccess(userId: number): Promise<boolean> {
|
public async checkUserAccess(userId: number): Promise<boolean> {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
@@ -220,9 +227,9 @@ class PlexTvAPI {
|
|||||||
throw new Error('Plex is not configured!');
|
throw new Error('Plex is not configured!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const friends = await this.getFriends();
|
const usersResponse = await this.getUsers();
|
||||||
|
|
||||||
const users = friends.MediaContainer.User;
|
const users = usersResponse.MediaContainer.User;
|
||||||
|
|
||||||
const user = users.find((u) => parseInt(u.$.id) === userId);
|
const user = users.find((u) => parseInt(u.$.id) === userId);
|
||||||
|
|
||||||
@@ -252,6 +259,83 @@ class PlexTvAPI {
|
|||||||
)) as UsersResponse;
|
)) as UsersResponse;
|
||||||
return parsedXml;
|
return parsedXml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getWatchlist({
|
||||||
|
offset = 0,
|
||||||
|
size = 20,
|
||||||
|
}: { offset?: number; size?: number } = {}): Promise<{
|
||||||
|
offset: number;
|
||||||
|
size: number;
|
||||||
|
totalSize: number;
|
||||||
|
items: PlexWatchlistItem[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const response = await this.axios.get<WatchlistResponse>(
|
||||||
|
'/library/sections/watchlist/all',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
'X-Plex-Container-Start': offset,
|
||||||
|
'X-Plex-Container-Size': size,
|
||||||
|
},
|
||||||
|
baseURL: 'https://metadata.provider.plex.tv',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const watchlistDetails = await Promise.all(
|
||||||
|
(response.data.MediaContainer.Metadata ?? []).map(
|
||||||
|
async (watchlistItem) => {
|
||||||
|
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||||
|
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||||
|
{
|
||||||
|
baseURL: 'https://metadata.provider.plex.tv',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||||
|
|
||||||
|
const tmdbString = metadata.Guid.find((guid) =>
|
||||||
|
guid.id.startsWith('tmdb')
|
||||||
|
);
|
||||||
|
const tvdbString = metadata.Guid.find((guid) =>
|
||||||
|
guid.id.startsWith('tvdb')
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ratingKey: metadata.ratingKey,
|
||||||
|
// This should always be set? But I guess it also cannot be?
|
||||||
|
// We will filter out the 0's afterwards
|
||||||
|
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
||||||
|
tvdbId: tvdbString
|
||||||
|
? Number(tvdbString.id.split('//')[1])
|
||||||
|
: undefined,
|
||||||
|
title: metadata.title,
|
||||||
|
type: metadata.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
offset,
|
||||||
|
size,
|
||||||
|
totalSize: response.data.MediaContainer.totalSize,
|
||||||
|
items: filteredList,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to retrieve watchlist items', {
|
||||||
|
label: 'Plex.TV Metadata API',
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
offset,
|
||||||
|
size,
|
||||||
|
totalSize: 0,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PlexTvAPI;
|
export default PlexTvAPI;
|
||||||
|
|||||||
56
server/api/pushover.ts
Normal file
56
server/api/pushover.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import ExternalAPI from './externalapi';
|
||||||
|
|
||||||
|
interface PushoverSoundsResponse {
|
||||||
|
sounds: {
|
||||||
|
[name: string]: string;
|
||||||
|
};
|
||||||
|
status: number;
|
||||||
|
request: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PushoverSound {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapSounds = (sounds: {
|
||||||
|
[name: string]: string;
|
||||||
|
}): PushoverSound[] =>
|
||||||
|
Object.entries(sounds).map(
|
||||||
|
([name, description]) =>
|
||||||
|
({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
} as PushoverSound)
|
||||||
|
);
|
||||||
|
|
||||||
|
class PushoverAPI extends ExternalAPI {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
'https://api.pushover.net/1',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSounds(appToken: string): Promise<PushoverSound[]> {
|
||||||
|
try {
|
||||||
|
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
|
||||||
|
params: {
|
||||||
|
token: appToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapSounds(data.sounds);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[Pushover] Failed to retrieve sounds: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PushoverAPI;
|
||||||
195
server/api/rating/imdbRadarrProxy.ts
Normal file
195
server/api/rating/imdbRadarrProxy.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
|
import cacheManager from '@server/lib/cache';
|
||||||
|
|
||||||
|
type IMDBRadarrProxyResponse = IMDBMovie[];
|
||||||
|
|
||||||
|
interface IMDBMovie {
|
||||||
|
ImdbId: string;
|
||||||
|
Overview: string;
|
||||||
|
Title: string;
|
||||||
|
OriginalTitle: string;
|
||||||
|
TitleSlug: string;
|
||||||
|
Ratings: Rating[];
|
||||||
|
MovieRatings: MovieRatings;
|
||||||
|
Runtime: number;
|
||||||
|
Images: Image[];
|
||||||
|
Genres: string[];
|
||||||
|
Popularity: number;
|
||||||
|
Premier: string;
|
||||||
|
InCinema: string;
|
||||||
|
PhysicalRelease: any;
|
||||||
|
DigitalRelease: string;
|
||||||
|
Year: number;
|
||||||
|
AlternativeTitles: AlternativeTitle[];
|
||||||
|
Translations: Translation[];
|
||||||
|
Recommendations: Recommendation[];
|
||||||
|
Credits: Credits;
|
||||||
|
Studio: string;
|
||||||
|
YoutubeTrailerId: string;
|
||||||
|
Certifications: Certification[];
|
||||||
|
Status: any;
|
||||||
|
Collection: Collection;
|
||||||
|
OriginalLanguage: string;
|
||||||
|
Homepage: string;
|
||||||
|
TmdbId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Rating {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Origin: string;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieRatings {
|
||||||
|
Tmdb: Tmdb;
|
||||||
|
Imdb: Imdb;
|
||||||
|
Metacritic: Metacritic;
|
||||||
|
RottenTomatoes: RottenTomatoes;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tmdb {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Imdb {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Metacritic {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RottenTomatoes {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
CoverType: string;
|
||||||
|
Url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AlternativeTitle {
|
||||||
|
Title: string;
|
||||||
|
Type: string;
|
||||||
|
Language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Translation {
|
||||||
|
Title: string;
|
||||||
|
Overview: string;
|
||||||
|
Language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Recommendation {
|
||||||
|
TmdbId: number;
|
||||||
|
Title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Credits {
|
||||||
|
Cast: Cast[];
|
||||||
|
Crew: Crew[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cast {
|
||||||
|
Name: string;
|
||||||
|
Order: number;
|
||||||
|
Character: string;
|
||||||
|
TmdbId: number;
|
||||||
|
CreditId: string;
|
||||||
|
Images: Image2[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image2 {
|
||||||
|
CoverType: string;
|
||||||
|
Url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Crew {
|
||||||
|
Name: string;
|
||||||
|
Job: string;
|
||||||
|
Department: string;
|
||||||
|
TmdbId: number;
|
||||||
|
CreditId: string;
|
||||||
|
Images: Image3[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image3 {
|
||||||
|
CoverType: string;
|
||||||
|
Url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Certification {
|
||||||
|
Country: string;
|
||||||
|
Certification: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Collection {
|
||||||
|
Name: string;
|
||||||
|
Images: any;
|
||||||
|
Overview: any;
|
||||||
|
Translations: any;
|
||||||
|
Parts: any;
|
||||||
|
TmdbId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMDBRating {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
criticsScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a best-effort API. The IMDB API is technically
|
||||||
|
* private and getting access costs money/requires approval.
|
||||||
|
*
|
||||||
|
* Radarr hosts a public proxy that's in use by all Radarr instances.
|
||||||
|
*/
|
||||||
|
class IMDBRadarrProxy extends ExternalAPI {
|
||||||
|
constructor() {
|
||||||
|
super('https://api.radarr.video/v1', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
nodeCache: cacheManager.getCache('imdb').data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the Radarr IMDB Proxy for the movie
|
||||||
|
*
|
||||||
|
* @param IMDBid Id of IMDB movie
|
||||||
|
*/
|
||||||
|
public async getMovieRatings(IMDBid: string): Promise<IMDBRating | null> {
|
||||||
|
try {
|
||||||
|
const data = await this.get<IMDBRadarrProxyResponse>(
|
||||||
|
`/movie/imdb/${IMDBid}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data?.length || data[0].ImdbId !== IMDBid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: data[0].Title,
|
||||||
|
url: `https://www.imdb.com/title/${data[0].ImdbId}`,
|
||||||
|
criticsScore: data[0].MovieRatings.Imdb.Value,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`[IMDB RADARR PROXY API] Failed to retrieve movie ratings: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IMDBRadarrProxy;
|
||||||
209
server/api/rating/rottentomatoes.ts
Normal file
209
server/api/rating/rottentomatoes.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
|
import cacheManager from '@server/lib/cache';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
|
||||||
|
interface RTAlgoliaSearchResponse {
|
||||||
|
results: {
|
||||||
|
hits: RTAlgoliaHit[];
|
||||||
|
index: 'content_rt' | 'people_rt';
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RTAlgoliaHit {
|
||||||
|
emsId: string;
|
||||||
|
emsVersionId: string;
|
||||||
|
tmsId: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
titles: string[];
|
||||||
|
description: string;
|
||||||
|
releaseYear: number;
|
||||||
|
rating: string;
|
||||||
|
genres: string[];
|
||||||
|
updateDate: string;
|
||||||
|
isEmsSearchable: boolean;
|
||||||
|
rtId: number;
|
||||||
|
vanity: string;
|
||||||
|
aka: string[];
|
||||||
|
posterImageUrl: string;
|
||||||
|
rottenTomatoes: {
|
||||||
|
audienceScore: number;
|
||||||
|
criticsIconUrl: string;
|
||||||
|
wantToSeeCount: number;
|
||||||
|
audienceIconUrl: string;
|
||||||
|
scoreSentiment: string;
|
||||||
|
certifiedFresh: boolean;
|
||||||
|
criticsScore: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RTRating {
|
||||||
|
title: string;
|
||||||
|
year: number;
|
||||||
|
criticsRating: 'Certified Fresh' | 'Fresh' | 'Rotten';
|
||||||
|
criticsScore: number;
|
||||||
|
audienceRating?: 'Upright' | 'Spilled';
|
||||||
|
audienceScore?: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a best-effort API. The Rotten Tomatoes API is technically
|
||||||
|
* private and getting access costs money/requires approval.
|
||||||
|
*
|
||||||
|
* They do, however, have a "public" api that they use to request the
|
||||||
|
* data on their own site. We use this to get ratings for movies/tv shows.
|
||||||
|
*
|
||||||
|
* Unfortunately, we need to do it by searching for the movie name, so it's
|
||||||
|
* not always accurate.
|
||||||
|
*/
|
||||||
|
class RottenTomatoes extends ExternalAPI {
|
||||||
|
constructor() {
|
||||||
|
const settings = getSettings();
|
||||||
|
super(
|
||||||
|
'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
|
||||||
|
{
|
||||||
|
'x-algolia-agent':
|
||||||
|
'Algolia%20for%20JavaScript%20(4.14.3)%3B%20Browser%20(lite)',
|
||||||
|
'x-algolia-api-key': '175588f6e5f8319b27702e4cc4013561',
|
||||||
|
'x-algolia-application-id': '79FRDP12PN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
'x-algolia-usertoken': settings.clientId,
|
||||||
|
},
|
||||||
|
nodeCache: cacheManager.getCache('rt').data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search the RT algolia api for the movie title
|
||||||
|
*
|
||||||
|
* We compare the release date to make sure its the correct
|
||||||
|
* match. But it's not guaranteed to have results.
|
||||||
|
*
|
||||||
|
* @param name Movie name
|
||||||
|
* @param year Release Year
|
||||||
|
*/
|
||||||
|
public async getMovieRatings(
|
||||||
|
name: string,
|
||||||
|
year: number
|
||||||
|
): Promise<RTRating | null> {
|
||||||
|
try {
|
||||||
|
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
||||||
|
requests: [
|
||||||
|
{
|
||||||
|
indexName: 'content_rt',
|
||||||
|
query: name,
|
||||||
|
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
||||||
|
|
||||||
|
if (!contentResults) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, attempt to match exact name and year
|
||||||
|
let movie = contentResults.hits.find(
|
||||||
|
(movie) => movie.releaseYear === year && movie.title === name
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we don't find a movie, try to match partial name and year
|
||||||
|
if (!movie) {
|
||||||
|
movie = contentResults.hits.find(
|
||||||
|
(movie) => movie.releaseYear === year && movie.title.includes(name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still dont find a movie, try to match just on year
|
||||||
|
if (!movie) {
|
||||||
|
movie = contentResults.hits.find((movie) => movie.releaseYear === year);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One last try, try exact name match only
|
||||||
|
if (!movie) {
|
||||||
|
movie = contentResults.hits.find((movie) => movie.title === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: movie.title,
|
||||||
|
url: `https://www.rottentomatoes.com/m/${movie.vanity}`,
|
||||||
|
criticsRating: movie.rottenTomatoes.certifiedFresh
|
||||||
|
? 'Certified Fresh'
|
||||||
|
: movie.rottenTomatoes.criticsScore >= 60
|
||||||
|
? 'Fresh'
|
||||||
|
: 'Rotten',
|
||||||
|
criticsScore: movie.rottenTomatoes.criticsScore,
|
||||||
|
audienceRating:
|
||||||
|
movie.rottenTomatoes.audienceScore >= 60 ? 'Upright' : 'Spilled',
|
||||||
|
audienceScore: movie.rottenTomatoes.audienceScore,
|
||||||
|
year: Number(movie.releaseYear),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`[RT API] Failed to retrieve movie ratings: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTVRatings(
|
||||||
|
name: string,
|
||||||
|
year?: number
|
||||||
|
): Promise<RTRating | null> {
|
||||||
|
try {
|
||||||
|
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
||||||
|
requests: [
|
||||||
|
{
|
||||||
|
indexName: 'content_rt',
|
||||||
|
query: name,
|
||||||
|
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
||||||
|
|
||||||
|
if (!contentResults) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tvshow: RTAlgoliaHit | undefined = contentResults.hits[0];
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
tvshow = contentResults.hits.find(
|
||||||
|
(series) => series.releaseYear === year
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tvshow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: tvshow.title,
|
||||||
|
url: `https://www.rottentomatoes.com/tv/${tvshow.vanity}`,
|
||||||
|
criticsRating:
|
||||||
|
tvshow.rottenTomatoes.criticsScore >= 60 ? 'Fresh' : 'Rotten',
|
||||||
|
criticsScore: tvshow.rottenTomatoes.criticsScore,
|
||||||
|
audienceRating:
|
||||||
|
tvshow.rottenTomatoes.audienceScore >= 60 ? 'Upright' : 'Spilled',
|
||||||
|
audienceScore: tvshow.rottenTomatoes.audienceScore,
|
||||||
|
year: Number(tvshow.releaseYear),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[RT API] Failed to retrieve tv ratings: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RottenTomatoes;
|
||||||
7
server/api/ratings.ts
Normal file
7
server/api/ratings.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { type IMDBRating } from '@server/api/rating/imdbRadarrProxy';
|
||||||
|
import { type RTRating } from '@server/api/rating/rottentomatoes';
|
||||||
|
|
||||||
|
export interface RatingResponse {
|
||||||
|
rt?: RTRating;
|
||||||
|
imdb?: IMDBRating;
|
||||||
|
}
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import cacheManager from '../lib/cache';
|
|
||||||
import ExternalAPI from './externalapi';
|
|
||||||
|
|
||||||
interface RTSearchResult {
|
|
||||||
meterClass: 'certified_fresh' | 'fresh' | 'rotten';
|
|
||||||
meterScore: number;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RTTvSearchResult extends RTSearchResult {
|
|
||||||
title: string;
|
|
||||||
startYear: number;
|
|
||||||
endYear: number;
|
|
||||||
}
|
|
||||||
interface RTMovieSearchResult extends RTSearchResult {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
year: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RTMultiSearchResponse {
|
|
||||||
tvCount: number;
|
|
||||||
tvSeries: RTTvSearchResult[];
|
|
||||||
movieCount: number;
|
|
||||||
movies: RTMovieSearchResult[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RTRating {
|
|
||||||
title: string;
|
|
||||||
year: number;
|
|
||||||
criticsRating: 'Certified Fresh' | 'Fresh' | 'Rotten';
|
|
||||||
criticsScore: number;
|
|
||||||
audienceRating?: 'Upright' | 'Spilled';
|
|
||||||
audienceScore?: number;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a best-effort API. The Rotten Tomatoes API is technically
|
|
||||||
* private and getting access costs money/requires approval.
|
|
||||||
*
|
|
||||||
* They do, however, have a "public" api that they use to request the
|
|
||||||
* data on their own site. We use this to get ratings for movies/tv shows.
|
|
||||||
*
|
|
||||||
* Unfortunately, we need to do it by searching for the movie name, so it's
|
|
||||||
* not always accurate.
|
|
||||||
*/
|
|
||||||
class RottenTomatoes extends ExternalAPI {
|
|
||||||
constructor() {
|
|
||||||
super(
|
|
||||||
'https://www.rottentomatoes.com/api/private',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
nodeCache: cacheManager.getCache('rt').data,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search the 1.0 api for the movie title
|
|
||||||
*
|
|
||||||
* We compare the release date to make sure its the correct
|
|
||||||
* match. But it's not guaranteed to have results.
|
|
||||||
*
|
|
||||||
* We use the 1.0 API here because the 2.0 search api does
|
|
||||||
* not return audience ratings.
|
|
||||||
*
|
|
||||||
* @param name Movie name
|
|
||||||
* @param year Release Year
|
|
||||||
*/
|
|
||||||
public async getMovieRatings(
|
|
||||||
name: string,
|
|
||||||
year: number
|
|
||||||
): Promise<RTRating | null> {
|
|
||||||
try {
|
|
||||||
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
|
||||||
params: { q: name, limit: 10 },
|
|
||||||
});
|
|
||||||
|
|
||||||
// First, attempt to match exact name and year
|
|
||||||
let movie = data.movies.find(
|
|
||||||
(movie) => movie.year === year && movie.name === name
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we don't find a movie, try to match partial name and year
|
|
||||||
if (!movie) {
|
|
||||||
movie = data.movies.find(
|
|
||||||
(movie) => movie.year === year && movie.name.includes(name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we still dont find a movie, try to match just on year
|
|
||||||
if (!movie) {
|
|
||||||
movie = data.movies.find((movie) => movie.year === year);
|
|
||||||
}
|
|
||||||
|
|
||||||
// One last try, try exact name match only
|
|
||||||
if (!movie) {
|
|
||||||
movie = data.movies.find((movie) => movie.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!movie) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: movie.name,
|
|
||||||
url: `https://www.rottentomatoes.com${movie.url}`,
|
|
||||||
criticsRating:
|
|
||||||
movie.meterClass === 'certified_fresh'
|
|
||||||
? 'Certified Fresh'
|
|
||||||
: movie.meterClass === 'fresh'
|
|
||||||
? 'Fresh'
|
|
||||||
: 'Rotten',
|
|
||||||
criticsScore: movie.meterScore,
|
|
||||||
year: movie.year,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(
|
|
||||||
`[RT API] Failed to retrieve movie ratings: ${e.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getTVRatings(
|
|
||||||
name: string,
|
|
||||||
year?: number
|
|
||||||
): Promise<RTRating | null> {
|
|
||||||
try {
|
|
||||||
const data = await this.get<RTMultiSearchResponse>('/v2.0/search/', {
|
|
||||||
params: { q: name, limit: 10 },
|
|
||||||
});
|
|
||||||
|
|
||||||
let tvshow: RTTvSearchResult | undefined = data.tvSeries[0];
|
|
||||||
|
|
||||||
if (year) {
|
|
||||||
tvshow = data.tvSeries.find((series) => series.startYear === year);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tvshow) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: tvshow.title,
|
|
||||||
url: `https://www.rottentomatoes.com${tvshow.url}`,
|
|
||||||
criticsRating: tvshow.meterClass === 'fresh' ? 'Fresh' : 'Rotten',
|
|
||||||
criticsScore: tvshow.meterScore,
|
|
||||||
year: tvshow.startYear,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`[RT API] Failed to retrieve tv ratings: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RottenTomatoes;
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
import { DVRSettings } from '../../lib/settings';
|
import type { AvailableCacheIds } from '@server/lib/cache';
|
||||||
import ExternalAPI from '../externalapi';
|
import cacheManager from '@server/lib/cache';
|
||||||
|
import type { DVRSettings } from '@server/lib/settings';
|
||||||
|
|
||||||
export interface SystemStatus {
|
export interface SystemStatus {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -157,7 +158,12 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
|
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<QueueResponse<QueueItemAppendT>>(
|
const response = await this.axios.get<QueueResponse<QueueItemAppendT>>(
|
||||||
`/queue`
|
`/queue`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
includeEpisode: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data.records;
|
return response.data.records;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import logger from '../../logger';
|
import logger from '@server/logger';
|
||||||
import ServarrBase from './base';
|
import ServarrBase from './base';
|
||||||
|
|
||||||
export interface RadarrMovieOptions {
|
export interface RadarrMovieOptions {
|
||||||
@@ -69,7 +69,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error retrieving movie by TMDb ID', {
|
logger.error('Error retrieving movie by TMDB ID', {
|
||||||
label: 'Radarr API',
|
label: 'Radarr API',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
tmdbId: id,
|
tmdbId: id,
|
||||||
@@ -213,6 +213,20 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public removeMovie = async (movieId: number): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { id, title } = await this.getMovieByTmdbId(movieId);
|
||||||
|
await this.axios.delete(`/movie/${id}`, {
|
||||||
|
params: {
|
||||||
|
deleteFiles: true,
|
||||||
|
addImportExclusion: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
logger.info(`[Radarr] Removed movie ${title}`);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RadarrAPI;
|
export default RadarrAPI;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logger from '../../logger';
|
import logger from '@server/logger';
|
||||||
import ServarrBase from './base';
|
import ServarrBase from './base';
|
||||||
|
|
||||||
interface SonarrSeason {
|
export interface SonarrSeason {
|
||||||
seasonNumber: number;
|
seasonNumber: number;
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
statistics?: {
|
statistics?: {
|
||||||
@@ -13,6 +13,21 @@ interface SonarrSeason {
|
|||||||
percentOfEpisodes: number;
|
percentOfEpisodes: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
interface EpisodeResult {
|
||||||
|
seriesId: number;
|
||||||
|
episodeFileId: number;
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeNumber: number;
|
||||||
|
title: string;
|
||||||
|
airDate: string;
|
||||||
|
airDateUtc: string;
|
||||||
|
overview: string;
|
||||||
|
hasFile: boolean;
|
||||||
|
monitored: boolean;
|
||||||
|
absoluteEpisodeNumber: number;
|
||||||
|
unverifiedSceneNumbering: boolean;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SonarrSeries {
|
export interface SonarrSeries {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -61,6 +76,15 @@ export interface SonarrSeries {
|
|||||||
ignoreEpisodesWithoutFiles?: boolean;
|
ignoreEpisodesWithoutFiles?: boolean;
|
||||||
searchForMissingEpisodes?: boolean;
|
searchForMissingEpisodes?: boolean;
|
||||||
};
|
};
|
||||||
|
statistics: {
|
||||||
|
seasonCount: number;
|
||||||
|
episodeFileCount: number;
|
||||||
|
episodeCount: number;
|
||||||
|
totalEpisodeCount: number;
|
||||||
|
sizeOnDisk: number;
|
||||||
|
releaseGroups: string[];
|
||||||
|
percentOfEpisodes: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddSeriesOptions {
|
export interface AddSeriesOptions {
|
||||||
@@ -82,7 +106,11 @@ export interface LanguageProfile {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
|
class SonarrAPI extends ServarrBase<{
|
||||||
|
seriesId: number;
|
||||||
|
episodeId: number;
|
||||||
|
episode: EpisodeResult;
|
||||||
|
}> {
|
||||||
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
constructor({ url, apiKey }: { url: string; apiKey: string }) {
|
||||||
super({ url, apiKey, apiName: 'Sonarr', cacheName: 'sonarr' });
|
super({ url, apiKey, apiName: 'Sonarr', cacheName: 'sonarr' });
|
||||||
}
|
}
|
||||||
@@ -97,6 +125,16 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSeriesById(id: number): Promise<SonarrSeries> {
|
||||||
|
try {
|
||||||
|
const response = await this.axios.get<SonarrSeries>(`/series/${id}`);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
||||||
@@ -302,6 +340,20 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
|
|||||||
|
|
||||||
return newSeasons;
|
return newSeasons;
|
||||||
}
|
}
|
||||||
|
public removeSerie = async (serieId: number): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { id, title } = await this.getSeriesByTvdbId(serieId);
|
||||||
|
await this.axios.delete(`/series/${id}`, {
|
||||||
|
params: {
|
||||||
|
deleteFiles: true,
|
||||||
|
addImportExclusion: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
logger.info(`[Radarr] Removed serie ${title}`);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SonarrAPI;
|
export default SonarrAPI;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import type { User } from '@server/entity/User';
|
||||||
|
import type { TautulliSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
import type { AxiosInstance } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
import { uniqWith } from 'lodash';
|
import { uniqWith } from 'lodash';
|
||||||
import { User } from '../entity/User';
|
|
||||||
import { TautulliSettings } from '../lib/settings';
|
|
||||||
import logger from '../logger';
|
|
||||||
|
|
||||||
export interface TautulliHistoryRecord {
|
export interface TautulliHistoryRecord {
|
||||||
date: number;
|
date: number;
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
|
import cacheManager from '@server/lib/cache';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import cacheManager from '../../lib/cache';
|
import type {
|
||||||
import ExternalAPI from '../externalapi';
|
|
||||||
import {
|
|
||||||
TmdbCollection,
|
TmdbCollection,
|
||||||
|
TmdbCompanySearchResponse,
|
||||||
TmdbExternalIdResponse,
|
TmdbExternalIdResponse,
|
||||||
TmdbGenre,
|
TmdbGenre,
|
||||||
TmdbGenresResult,
|
TmdbGenresResult,
|
||||||
|
TmdbKeyword,
|
||||||
|
TmdbKeywordSearchResponse,
|
||||||
TmdbLanguage,
|
TmdbLanguage,
|
||||||
TmdbMovieDetails,
|
TmdbMovieDetails,
|
||||||
TmdbNetwork,
|
TmdbNetwork,
|
||||||
@@ -19,6 +22,8 @@ import {
|
|||||||
TmdbSeasonWithEpisodes,
|
TmdbSeasonWithEpisodes,
|
||||||
TmdbTvDetails,
|
TmdbTvDetails,
|
||||||
TmdbUpcomingMoviesResponse,
|
TmdbUpcomingMoviesResponse,
|
||||||
|
TmdbWatchProviderDetails,
|
||||||
|
TmdbWatchProviderRegion,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
|
|
||||||
interface SearchOptions {
|
interface SearchOptions {
|
||||||
@@ -32,30 +37,43 @@ interface SingleSearchOptions extends SearchOptions {
|
|||||||
year?: number;
|
year?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SortOptions =
|
||||||
|
| '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'
|
||||||
|
| 'first_air_date.asc'
|
||||||
|
| 'first_air_date.desc';
|
||||||
|
|
||||||
interface DiscoverMovieOptions {
|
interface DiscoverMovieOptions {
|
||||||
page?: number;
|
page?: number;
|
||||||
includeAdult?: boolean;
|
includeAdult?: boolean;
|
||||||
language?: string;
|
language?: string;
|
||||||
primaryReleaseDateGte?: string;
|
primaryReleaseDateGte?: string;
|
||||||
primaryReleaseDateLte?: string;
|
primaryReleaseDateLte?: string;
|
||||||
|
withRuntimeGte?: string;
|
||||||
|
withRuntimeLte?: string;
|
||||||
|
voteAverageGte?: string;
|
||||||
|
voteAverageLte?: string;
|
||||||
|
voteCountGte?: string;
|
||||||
|
voteCountLte?: string;
|
||||||
originalLanguage?: string;
|
originalLanguage?: string;
|
||||||
genre?: number;
|
genre?: string;
|
||||||
studio?: number;
|
studio?: string;
|
||||||
sortBy?:
|
keywords?: string;
|
||||||
| 'popularity.asc'
|
sortBy?: SortOptions;
|
||||||
| 'popularity.desc'
|
watchRegion?: string;
|
||||||
| 'release_date.asc'
|
watchProviders?: string;
|
||||||
| '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 {
|
interface DiscoverTvOptions {
|
||||||
@@ -63,19 +81,20 @@ interface DiscoverTvOptions {
|
|||||||
language?: string;
|
language?: string;
|
||||||
firstAirDateGte?: string;
|
firstAirDateGte?: string;
|
||||||
firstAirDateLte?: string;
|
firstAirDateLte?: string;
|
||||||
|
withRuntimeGte?: string;
|
||||||
|
withRuntimeLte?: string;
|
||||||
|
voteAverageGte?: string;
|
||||||
|
voteAverageLte?: string;
|
||||||
|
voteCountGte?: string;
|
||||||
|
voteCountLte?: string;
|
||||||
includeEmptyReleaseDate?: boolean;
|
includeEmptyReleaseDate?: boolean;
|
||||||
originalLanguage?: string;
|
originalLanguage?: string;
|
||||||
genre?: number;
|
genre?: string;
|
||||||
network?: number;
|
network?: number;
|
||||||
sortBy?:
|
keywords?: string;
|
||||||
| 'popularity.asc'
|
sortBy?: SortOptions;
|
||||||
| 'popularity.desc'
|
watchRegion?: string;
|
||||||
| 'vote_average.asc'
|
watchProviders?: string;
|
||||||
| 'vote_average.desc'
|
|
||||||
| 'vote_count.asc'
|
|
||||||
| 'vote_count.desc'
|
|
||||||
| 'first_air_date.asc'
|
|
||||||
| 'first_air_date.desc';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TheMovieDb extends ExternalAPI {
|
class TheMovieDb extends ExternalAPI {
|
||||||
@@ -92,6 +111,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
nodeCache: cacheManager.getCache('tmdb').data,
|
nodeCache: cacheManager.getCache('tmdb').data,
|
||||||
|
rateLimit: {
|
||||||
|
maxRequests: 20,
|
||||||
|
maxRPS: 50,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.region = region;
|
this.region = region;
|
||||||
@@ -192,7 +215,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch person details: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch person details: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,7 +237,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[TMDb] Failed to fetch person combined credits: ${e.message}`
|
`[TMDB] Failed to fetch person combined credits: ${e.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -233,7 +256,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
params: {
|
params: {
|
||||||
language,
|
language,
|
||||||
append_to_response:
|
append_to_response:
|
||||||
'credits,external_ids,videos,release_dates,watch/providers',
|
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
43200
|
43200
|
||||||
@@ -241,7 +264,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch movie details: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch movie details: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -267,7 +290,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,7 +316,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch TV show details: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch TV show details: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -319,7 +342,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +368,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +394,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch movies by keyword: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch movies by keyword: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +421,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[TMDb] Failed to fetch TV recommendations: ${e.message}`
|
`[TMDB] Failed to fetch TV recommendations: ${e.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,7 +445,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch TV similar: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch TV similar: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,8 +459,27 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
originalLanguage,
|
originalLanguage,
|
||||||
genre,
|
genre,
|
||||||
studio,
|
studio,
|
||||||
|
keywords,
|
||||||
|
withRuntimeGte,
|
||||||
|
withRuntimeLte,
|
||||||
|
voteAverageGte,
|
||||||
|
voteAverageLte,
|
||||||
|
voteCountGte,
|
||||||
|
voteCountLte,
|
||||||
|
watchProviders,
|
||||||
|
watchRegion,
|
||||||
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
}: DiscoverMovieOptions = {}): Promise<TmdbSearchMovieResponse> => {
|
||||||
try {
|
try {
|
||||||
|
const defaultFutureDate = new Date(
|
||||||
|
Date.now() + 1000 * 60 * 60 * 24 * (365 * 1.5)
|
||||||
|
)
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[0];
|
||||||
|
|
||||||
|
const defaultPastDate = new Date('1900-01-01')
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[0];
|
||||||
|
|
||||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||||
params: {
|
params: {
|
||||||
sort_by: sortBy,
|
sort_by: sortBy,
|
||||||
@@ -445,17 +487,39 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
include_adult: includeAdult,
|
include_adult: includeAdult,
|
||||||
language,
|
language,
|
||||||
region: this.region,
|
region: this.region,
|
||||||
with_original_language: originalLanguage ?? this.originalLanguage,
|
with_original_language:
|
||||||
'primary_release_date.gte': primaryReleaseDateGte,
|
originalLanguage && originalLanguage !== 'all'
|
||||||
'primary_release_date.lte': primaryReleaseDateLte,
|
? originalLanguage
|
||||||
|
: originalLanguage === 'all'
|
||||||
|
? undefined
|
||||||
|
: this.originalLanguage,
|
||||||
|
// Set our release date values, but check if one is set and not the other,
|
||||||
|
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||||
|
'primary_release_date.gte':
|
||||||
|
!primaryReleaseDateGte && primaryReleaseDateLte
|
||||||
|
? defaultPastDate
|
||||||
|
: primaryReleaseDateGte,
|
||||||
|
'primary_release_date.lte':
|
||||||
|
!primaryReleaseDateLte && primaryReleaseDateGte
|
||||||
|
? defaultFutureDate
|
||||||
|
: primaryReleaseDateLte,
|
||||||
with_genres: genre,
|
with_genres: genre,
|
||||||
with_companies: studio,
|
with_companies: studio,
|
||||||
|
with_keywords: keywords,
|
||||||
|
'with_runtime.gte': withRuntimeGte,
|
||||||
|
'with_runtime.lte': withRuntimeLte,
|
||||||
|
'vote_average.gte': voteAverageGte,
|
||||||
|
'vote_average.lte': voteAverageLte,
|
||||||
|
'vote_count.gte': voteCountGte,
|
||||||
|
'vote_count.lte': voteCountLte,
|
||||||
|
watch_region: watchRegion,
|
||||||
|
with_watch_providers: watchProviders,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch discover movies: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch discover movies: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -469,26 +533,67 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
originalLanguage,
|
originalLanguage,
|
||||||
genre,
|
genre,
|
||||||
network,
|
network,
|
||||||
|
keywords,
|
||||||
|
withRuntimeGte,
|
||||||
|
withRuntimeLte,
|
||||||
|
voteAverageGte,
|
||||||
|
voteAverageLte,
|
||||||
|
voteCountGte,
|
||||||
|
voteCountLte,
|
||||||
|
watchProviders,
|
||||||
|
watchRegion,
|
||||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||||
try {
|
try {
|
||||||
|
const defaultFutureDate = new Date(
|
||||||
|
Date.now() + 1000 * 60 * 60 * 24 * (365 * 1.5)
|
||||||
|
)
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[0];
|
||||||
|
|
||||||
|
const defaultPastDate = new Date('1900-01-01')
|
||||||
|
.toISOString()
|
||||||
|
.split('T')[0];
|
||||||
|
|
||||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||||
params: {
|
params: {
|
||||||
sort_by: sortBy,
|
sort_by: sortBy,
|
||||||
page,
|
page,
|
||||||
language,
|
language,
|
||||||
region: this.region,
|
region: this.region,
|
||||||
'first_air_date.gte': firstAirDateGte,
|
// Set our release date values, but check if one is set and not the other,
|
||||||
'first_air_date.lte': firstAirDateLte,
|
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||||
with_original_language: originalLanguage ?? this.originalLanguage,
|
'first_air_date.gte':
|
||||||
|
!firstAirDateGte && firstAirDateLte
|
||||||
|
? defaultPastDate
|
||||||
|
: firstAirDateGte,
|
||||||
|
'first_air_date.lte':
|
||||||
|
!firstAirDateLte && firstAirDateGte
|
||||||
|
? defaultFutureDate
|
||||||
|
: firstAirDateLte,
|
||||||
|
with_original_language:
|
||||||
|
originalLanguage && originalLanguage !== 'all'
|
||||||
|
? originalLanguage
|
||||||
|
: originalLanguage === 'all'
|
||||||
|
? undefined
|
||||||
|
: this.originalLanguage,
|
||||||
include_null_first_air_dates: includeEmptyReleaseDate,
|
include_null_first_air_dates: includeEmptyReleaseDate,
|
||||||
with_genres: genre,
|
with_genres: genre,
|
||||||
with_networks: network,
|
with_networks: network,
|
||||||
|
with_keywords: keywords,
|
||||||
|
'with_runtime.gte': withRuntimeGte,
|
||||||
|
'with_runtime.lte': withRuntimeLte,
|
||||||
|
'vote_average.gte': voteAverageGte,
|
||||||
|
'vote_average.lte': voteAverageLte,
|
||||||
|
'vote_count.gte': voteCountGte,
|
||||||
|
'vote_count.lte': voteCountLte,
|
||||||
|
with_watch_providers: watchProviders,
|
||||||
|
watch_region: watchRegion,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch discover TV: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch discover TV: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -514,7 +619,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch upcoming movies: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch upcoming movies: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -541,7 +646,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -564,7 +669,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -587,7 +692,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch all trending: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch all trending: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -619,7 +724,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to find by external ID: ${e.message}`);
|
throw new Error(`[TMDB] Failed to find by external ID: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,7 +762,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
throw new Error(`No movie or show returned from API for ID ${imdbId}`);
|
throw new Error(`No movie or show returned from API for ID ${imdbId}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[TMDb] Failed to find media using external IMDb ID: ${e.message}`
|
`[TMDB] Failed to find media using external IMDb ID: ${e.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,7 +792,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
throw new Error(`No show returned from API for ID ${tvdbId}`);
|
throw new Error(`No show returned from API for ID ${tvdbId}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[TMDb] Failed to get TV show using the external TVDB ID: ${e.message}`
|
`[TMDB] Failed to get TV show using the external TVDB ID: ${e.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,7 +816,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch collection: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch collection: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,7 +832,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return regions;
|
return regions;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch countries: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch countries: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,7 +848,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return languages;
|
return languages;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch langauges: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch langauges: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,7 +860,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch movie studio: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch movie studio: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,7 +870,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch TV network: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch TV network: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,7 +921,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return movieGenres;
|
return movieGenres;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch movie genres: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch movie genres: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,7 +972,153 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
|
|
||||||
return tvGenres;
|
return tvGenres;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[TMDb] Failed to fetch TV genres: ${e.message}`);
|
throw new Error(`[TMDB] Failed to fetch TV genres: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getKeywordDetails({
|
||||||
|
keywordId,
|
||||||
|
}: {
|
||||||
|
keywordId: number;
|
||||||
|
}): Promise<TmdbKeyword> {
|
||||||
|
try {
|
||||||
|
const data = await this.get<TmdbKeyword>(
|
||||||
|
`/keyword/${keywordId}`,
|
||||||
|
undefined,
|
||||||
|
604800 // 7 days
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[TMDB] Failed to fetch keyword: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async searchKeyword({
|
||||||
|
query,
|
||||||
|
page = 1,
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
page?: number;
|
||||||
|
}): Promise<TmdbKeywordSearchResponse> {
|
||||||
|
try {
|
||||||
|
const data = await this.get<TmdbKeywordSearchResponse>(
|
||||||
|
'/search/keyword',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
86400 // 24 hours
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[TMDB] Failed to search keyword: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async searchCompany({
|
||||||
|
query,
|
||||||
|
page = 1,
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
page?: number;
|
||||||
|
}): Promise<TmdbCompanySearchResponse> {
|
||||||
|
try {
|
||||||
|
const data = await this.get<TmdbCompanySearchResponse>(
|
||||||
|
'/search/company',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
86400 // 24 hours
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`[TMDB] Failed to search companies: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAvailableWatchProviderRegions({
|
||||||
|
language,
|
||||||
|
}: {
|
||||||
|
language?: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
|
||||||
|
'/watch/providers/regions',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
language: language ?? this.originalLanguage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
86400 // 24 hours
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.results;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`[TMDB] Failed to fetch available watch regions: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMovieWatchProviders({
|
||||||
|
language,
|
||||||
|
watchRegion,
|
||||||
|
}: {
|
||||||
|
language?: string;
|
||||||
|
watchRegion: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||||
|
'/watch/providers/movie',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
language: language ?? this.originalLanguage,
|
||||||
|
watch_region: watchRegion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
86400 // 24 hours
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.results;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`[TMDB] Failed to fetch movie watch providers: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTvWatchProviders({
|
||||||
|
language,
|
||||||
|
watchRegion,
|
||||||
|
}: {
|
||||||
|
language?: string;
|
||||||
|
watchRegion: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||||
|
'/watch/providers/tv',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
language: language ?? this.originalLanguage,
|
||||||
|
watch_region: watchRegion,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
86400 // 24 hours
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.results;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`[TMDB] Failed to fetch TV watch providers: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ export interface TmdbTvResult extends TmdbMediaResult {
|
|||||||
first_air_date: string;
|
first_air_date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TmdbCollectionResult {
|
||||||
|
id: number;
|
||||||
|
media_type: 'collection';
|
||||||
|
title: string;
|
||||||
|
original_title: string;
|
||||||
|
adult: boolean;
|
||||||
|
poster_path?: string;
|
||||||
|
backdrop_path?: string;
|
||||||
|
overview: string;
|
||||||
|
original_language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TmdbPersonResult {
|
export interface TmdbPersonResult {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -45,7 +57,12 @@ interface TmdbPaginatedResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
export interface TmdbSearchMultiResponse extends TmdbPaginatedResponse {
|
||||||
results: (TmdbMovieResult | TmdbTvResult | TmdbPersonResult)[];
|
results: (
|
||||||
|
| TmdbMovieResult
|
||||||
|
| TmdbTvResult
|
||||||
|
| TmdbPersonResult
|
||||||
|
| TmdbCollectionResult
|
||||||
|
)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
export interface TmdbSearchMovieResponse extends TmdbPaginatedResponse {
|
||||||
@@ -171,6 +188,9 @@ export interface TmdbMovieDetails {
|
|||||||
id: number;
|
id: number;
|
||||||
results?: { [iso_3166_1: string]: TmdbWatchProviders };
|
results?: { [iso_3166_1: string]: TmdbWatchProviders };
|
||||||
};
|
};
|
||||||
|
keywords: {
|
||||||
|
keywords: TmdbKeyword[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbVideo {
|
export interface TmdbVideo {
|
||||||
@@ -191,7 +211,7 @@ export interface TmdbVideo {
|
|||||||
|
|
||||||
export interface TmdbTvEpisodeResult {
|
export interface TmdbTvEpisodeResult {
|
||||||
id: number;
|
id: number;
|
||||||
air_date: string;
|
air_date: string | null;
|
||||||
episode_number: number;
|
episode_number: number;
|
||||||
name: string;
|
name: string;
|
||||||
overview: string;
|
overview: string;
|
||||||
@@ -372,7 +392,8 @@ export interface TmdbPersonCombinedCredits {
|
|||||||
crew: TmdbPersonCreditCrew[];
|
crew: TmdbPersonCreditCrew[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TmdbSeasonWithEpisodes extends TmdbTvSeasonResult {
|
export interface TmdbSeasonWithEpisodes
|
||||||
|
extends Omit<TmdbTvSeasonResult, 'episode_count'> {
|
||||||
episodes: TmdbTvEpisodeResult[];
|
episodes: TmdbTvEpisodeResult[];
|
||||||
external_ids: TmdbExternalIds;
|
external_ids: TmdbExternalIds;
|
||||||
}
|
}
|
||||||
@@ -427,3 +448,24 @@ export interface TmdbWatchProviderDetails {
|
|||||||
provider_id: number;
|
provider_id: number;
|
||||||
provider_name: string;
|
provider_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TmdbKeywordSearchResponse extends TmdbPaginatedResponse {
|
||||||
|
results: TmdbKeyword[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have production companies, but the company search results return less data
|
||||||
|
export interface TmdbCompany {
|
||||||
|
id: number;
|
||||||
|
logo_path?: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TmdbCompanySearchResponse extends TmdbPaginatedResponse {
|
||||||
|
results: TmdbCompany[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TmdbWatchProviderRegion {
|
||||||
|
iso_3166_1: string;
|
||||||
|
english_name: string;
|
||||||
|
native_name: string;
|
||||||
|
}
|
||||||
|
|||||||
100
server/constants/discover.ts
Normal file
100
server/constants/discover.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import type DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
|
|
||||||
|
export enum DiscoverSliderType {
|
||||||
|
RECENTLY_ADDED = 1,
|
||||||
|
RECENT_REQUESTS,
|
||||||
|
PLEX_WATCHLIST,
|
||||||
|
TRENDING,
|
||||||
|
POPULAR_MOVIES,
|
||||||
|
MOVIE_GENRES,
|
||||||
|
UPCOMING_MOVIES,
|
||||||
|
STUDIOS,
|
||||||
|
POPULAR_TV,
|
||||||
|
TV_GENRES,
|
||||||
|
UPCOMING_TV,
|
||||||
|
NETWORKS,
|
||||||
|
TMDB_MOVIE_KEYWORD,
|
||||||
|
TMDB_MOVIE_GENRE,
|
||||||
|
TMDB_TV_KEYWORD,
|
||||||
|
TMDB_TV_GENRE,
|
||||||
|
TMDB_SEARCH,
|
||||||
|
TMDB_STUDIO,
|
||||||
|
TMDB_NETWORK,
|
||||||
|
TMDB_MOVIE_STREAMING_SERVICES,
|
||||||
|
TMDB_TV_STREAMING_SERVICES,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultSliders: Partial<DiscoverSlider>[] = [
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.RECENTLY_ADDED,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.RECENT_REQUESTS,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.PLEX_WATCHLIST,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.TRENDING,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.POPULAR_MOVIES,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.MOVIE_GENRES,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.UPCOMING_MOVIES,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.STUDIOS,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.POPULAR_TV,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.TV_GENRES,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.UPCOMING_TV,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: DiscoverSliderType.NETWORKS,
|
||||||
|
enabled: true,
|
||||||
|
isBuiltIn: true,
|
||||||
|
order: 11,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -2,6 +2,7 @@ export enum MediaRequestStatus {
|
|||||||
PENDING = 1,
|
PENDING = 1,
|
||||||
APPROVED,
|
APPROVED,
|
||||||
DECLINED,
|
DECLINED,
|
||||||
|
FAILED,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MediaType {
|
export enum MediaType {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
const devConfig = {
|
import 'reflect-metadata';
|
||||||
|
import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
const devConfig: DataSourceOptions = {
|
||||||
type: 'sqlite',
|
type: 'sqlite',
|
||||||
database: process.env.CONFIG_DIRECTORY
|
database: process.env.CONFIG_DIRECTORY
|
||||||
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
|
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
|
||||||
@@ -10,31 +14,30 @@ const devConfig = {
|
|||||||
entities: ['server/entity/**/*.ts'],
|
entities: ['server/entity/**/*.ts'],
|
||||||
migrations: ['server/migration/**/*.ts'],
|
migrations: ['server/migration/**/*.ts'],
|
||||||
subscribers: ['server/subscriber/**/*.ts'],
|
subscribers: ['server/subscriber/**/*.ts'],
|
||||||
cli: {
|
|
||||||
entitiesDir: 'server/entity',
|
|
||||||
migrationsDir: 'server/migration',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const prodConfig = {
|
const prodConfig: DataSourceOptions = {
|
||||||
type: 'sqlite',
|
type: 'sqlite',
|
||||||
database: process.env.CONFIG_DIRECTORY
|
database: process.env.CONFIG_DIRECTORY
|
||||||
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
|
? `${process.env.CONFIG_DIRECTORY}/db/db.sqlite3`
|
||||||
: 'config/db/db.sqlite3',
|
: 'config/db/db.sqlite3',
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
|
migrationsRun: false,
|
||||||
logging: false,
|
logging: false,
|
||||||
enableWAL: true,
|
enableWAL: true,
|
||||||
entities: ['dist/entity/**/*.js'],
|
entities: ['dist/entity/**/*.js'],
|
||||||
migrations: ['dist/migration/**/*.js'],
|
migrations: ['dist/migration/**/*.js'],
|
||||||
migrationsRun: false,
|
|
||||||
subscribers: ['dist/subscriber/**/*.js'],
|
subscribers: ['dist/subscriber/**/*.js'],
|
||||||
cli: {
|
|
||||||
entitiesDir: 'dist/entity',
|
|
||||||
migrationsDir: 'dist/migration',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalConfig =
|
const dataSource = new DataSource(
|
||||||
process.env.NODE_ENV !== 'production' ? devConfig : prodConfig;
|
process.env.NODE_ENV !== 'production' ? devConfig : prodConfig
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = finalConfig;
|
export const getRepository = <Entity extends object>(
|
||||||
|
target: EntityTarget<Entity>
|
||||||
|
): Repository<Entity> => {
|
||||||
|
return dataSource.getRepository(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dataSource;
|
||||||
69
server/entity/DiscoverSlider.ts
Normal file
69
server/entity/DiscoverSlider.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { DiscoverSliderType } from '@server/constants/discover';
|
||||||
|
import { defaultSliders } from '@server/constants/discover';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class DiscoverSlider {
|
||||||
|
public static async bootstrapSliders(): Promise<void> {
|
||||||
|
const sliderRepository = getRepository(DiscoverSlider);
|
||||||
|
|
||||||
|
for (const slider of defaultSliders) {
|
||||||
|
const existingSlider = await sliderRepository.findOne({
|
||||||
|
where: {
|
||||||
|
type: slider.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingSlider) {
|
||||||
|
logger.info('Creating built-in discovery slider', {
|
||||||
|
label: 'Discover Slider',
|
||||||
|
slider,
|
||||||
|
});
|
||||||
|
await sliderRepository.save(new DiscoverSlider(slider));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
public id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int' })
|
||||||
|
public type: DiscoverSliderType;
|
||||||
|
|
||||||
|
@Column({ type: 'int' })
|
||||||
|
public order: number;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
public isBuiltIn: boolean;
|
||||||
|
|
||||||
|
@Column({ default: true })
|
||||||
|
public enabled: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
// Title is not required for built in sliders because we will
|
||||||
|
// use translations for them.
|
||||||
|
public title?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public data?: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
public updatedAt: Date;
|
||||||
|
|
||||||
|
constructor(init?: Partial<DiscoverSlider>) {
|
||||||
|
Object.assign(this, init);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DiscoverSlider;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { IssueType } from '@server/constants/issue';
|
||||||
|
import { IssueStatus } from '@server/constants/issue';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
@@ -7,7 +9,6 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { IssueStatus, IssueType } from '../constants/issue';
|
|
||||||
import IssueComment from './IssueComment';
|
import IssueComment from './IssueComment';
|
||||||
import Media from './Media';
|
import Media from './Media';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
|
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||||
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import type { User } from '@server/entity/User';
|
||||||
|
import { Watchlist } from '@server/entity/Watchlist';
|
||||||
|
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||||
|
import downloadTracker from '@server/lib/downloadtracker';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
import {
|
import {
|
||||||
AfterLoad,
|
AfterLoad,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
getRepository,
|
|
||||||
In,
|
|
||||||
Index,
|
Index,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import RadarrAPI from '../api/servarr/radarr';
|
|
||||||
import SonarrAPI from '../api/servarr/sonarr';
|
|
||||||
import { MediaStatus, MediaType } from '../constants/media';
|
|
||||||
import { MediaServerType } from '../constants/server';
|
|
||||||
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
|
|
||||||
import { getSettings } from '../lib/settings';
|
|
||||||
import logger from '../logger';
|
|
||||||
import Issue from './Issue';
|
import Issue from './Issue';
|
||||||
import { MediaRequest } from './MediaRequest';
|
import { MediaRequest } from './MediaRequest';
|
||||||
import Season from './Season';
|
import Season from './Season';
|
||||||
@@ -24,6 +26,7 @@ import Season from './Season';
|
|||||||
@Entity()
|
@Entity()
|
||||||
class Media {
|
class Media {
|
||||||
public static async getRelatedMedia(
|
public static async getRelatedMedia(
|
||||||
|
user: User | undefined,
|
||||||
tmdbIds: number | number[]
|
tmdbIds: number | number[]
|
||||||
): Promise<Media[]> {
|
): Promise<Media[]> {
|
||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
@@ -36,9 +39,16 @@ class Media {
|
|||||||
finalIds = tmdbIds;
|
finalIds = tmdbIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
const media = await mediaRepository.find({
|
const media = await mediaRepository
|
||||||
tmdbId: In(finalIds),
|
.createQueryBuilder('media')
|
||||||
});
|
.leftJoinAndSelect(
|
||||||
|
'media.watchlists',
|
||||||
|
'watchlist',
|
||||||
|
'media.id= watchlist.media and watchlist.requestedBy = :userId',
|
||||||
|
{ userId: user?.id }
|
||||||
|
) //,
|
||||||
|
.where(' media.tmdbId in (:...finalIds)', { finalIds })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
return media;
|
return media;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -56,10 +66,10 @@ class Media {
|
|||||||
try {
|
try {
|
||||||
const media = await mediaRepository.findOne({
|
const media = await mediaRepository.findOne({
|
||||||
where: { tmdbId: id, mediaType },
|
where: { tmdbId: id, mediaType },
|
||||||
relations: ['requests', 'issues'],
|
relations: { requests: true, issues: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
return media;
|
return media ?? undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e.message);
|
logger.error(e.message);
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -93,6 +103,9 @@ class Media {
|
|||||||
@OneToMany(() => MediaRequest, (request) => request.media, { cascade: true })
|
@OneToMany(() => MediaRequest, (request) => request.media, { cascade: true })
|
||||||
public requests: MediaRequest[];
|
public requests: MediaRequest[];
|
||||||
|
|
||||||
|
@OneToMany(() => Watchlist, (watchlist) => watchlist.media)
|
||||||
|
public watchlists: null | Watchlist[];
|
||||||
|
|
||||||
@OneToMany(() => Season, (season) => season.media, {
|
@OneToMany(() => Season, (season) => season.media, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
eager: true,
|
eager: true,
|
||||||
@@ -114,29 +127,29 @@ class Media {
|
|||||||
@Column({ type: 'datetime', nullable: true })
|
@Column({ type: 'datetime', nullable: true })
|
||||||
public mediaAddedAt: Date;
|
public mediaAddedAt: Date;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: 'int' })
|
||||||
public serviceId?: number;
|
public serviceId?: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: 'int' })
|
||||||
public serviceId4k?: number;
|
public serviceId4k?: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: 'int' })
|
||||||
public externalServiceId?: number;
|
public externalServiceId?: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: 'int' })
|
||||||
public externalServiceId4k?: number;
|
public externalServiceId4k?: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: 'varchar' })
|
||||||
public externalServiceSlug?: string;
|
public externalServiceSlug?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: 'varchar' })
|
||||||
public externalServiceSlug4k?: string;
|
public externalServiceSlug4k?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: 'varchar' })
|
||||||
public ratingKey?: string;
|
public ratingKey?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, type: 'varchar' })
|
||||||
public ratingKey4k?: string;
|
public ratingKey4k?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public jellyfinMediaId?: string;
|
public jellyfinMediaId?: string;
|
||||||
@@ -152,6 +165,9 @@ class Media {
|
|||||||
public mediaUrl?: string;
|
public mediaUrl?: string;
|
||||||
public mediaUrl4k?: string;
|
public mediaUrl4k?: string;
|
||||||
|
|
||||||
|
public iOSPlexUrl?: string;
|
||||||
|
public iOSPlexUrl4k?: string;
|
||||||
|
|
||||||
public tautulliUrl?: string;
|
public tautulliUrl?: string;
|
||||||
public tautulliUrl4k?: string;
|
public tautulliUrl4k?: string;
|
||||||
|
|
||||||
@@ -172,35 +188,44 @@ class Media {
|
|||||||
this.ratingKey
|
this.ratingKey
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
this.iOSPlexUrl = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey}&server=${machineId}`;
|
||||||
|
|
||||||
if (tautulliUrl) {
|
if (tautulliUrl) {
|
||||||
this.tautulliUrl = `${tautulliUrl}/info?rating_key=${this.ratingKey}`;
|
this.tautulliUrl = `${tautulliUrl}/info?rating_key=${this.ratingKey}`;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ratingKey4k) {
|
if (this.ratingKey4k) {
|
||||||
this.mediaUrl4k = `${
|
this.mediaUrl4k = `${
|
||||||
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
|
webAppUrl ? webAppUrl : 'https://app.plex.tv/desktop'
|
||||||
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
|
}#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${
|
||||||
this.ratingKey4k
|
this.ratingKey4k
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
if (tautulliUrl) {
|
this.iOSPlexUrl4k = `plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}&server=${machineId}`;
|
||||||
this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`;
|
|
||||||
|
if (tautulliUrl) {
|
||||||
|
this.tautulliUrl4k = `${tautulliUrl}/info?rating_key=${this.ratingKey4k}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pageName =
|
const pageName =
|
||||||
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
|
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
|
||||||
const { serverId, hostname, externalHostname } = getSettings().jellyfin;
|
const { serverId, hostname, externalHostname } = getSettings().jellyfin;
|
||||||
const jellyfinHost =
|
let jellyfinHost =
|
||||||
externalHostname && externalHostname.length > 0
|
externalHostname && externalHostname.length > 0
|
||||||
? externalHostname
|
? externalHostname
|
||||||
: hostname;
|
: hostname;
|
||||||
|
|
||||||
|
jellyfinHost = jellyfinHost.endsWith('/')
|
||||||
|
? jellyfinHost.slice(0, -1)
|
||||||
|
: jellyfinHost;
|
||||||
|
|
||||||
if (this.jellyfinMediaId) {
|
if (this.jellyfinMediaId) {
|
||||||
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
|
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
|
||||||
}
|
}
|
||||||
if (this.jellyfinMediaId4k) {
|
if (this.jellyfinMediaId4k) {
|
||||||
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`;
|
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,7 +300,9 @@ class Media {
|
|||||||
if (this.mediaType === MediaType.MOVIE) {
|
if (this.mediaType === MediaType.MOVIE) {
|
||||||
if (
|
if (
|
||||||
this.externalServiceId !== undefined &&
|
this.externalServiceId !== undefined &&
|
||||||
this.serviceId !== undefined
|
this.externalServiceId !== null &&
|
||||||
|
this.serviceId !== undefined &&
|
||||||
|
this.serviceId !== null
|
||||||
) {
|
) {
|
||||||
this.downloadStatus = downloadTracker.getMovieProgress(
|
this.downloadStatus = downloadTracker.getMovieProgress(
|
||||||
this.serviceId,
|
this.serviceId,
|
||||||
@@ -285,7 +312,9 @@ class Media {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.externalServiceId4k !== undefined &&
|
this.externalServiceId4k !== undefined &&
|
||||||
this.serviceId4k !== undefined
|
this.externalServiceId4k !== null &&
|
||||||
|
this.serviceId4k !== undefined &&
|
||||||
|
this.serviceId4k !== null
|
||||||
) {
|
) {
|
||||||
this.downloadStatus4k = downloadTracker.getMovieProgress(
|
this.downloadStatus4k = downloadTracker.getMovieProgress(
|
||||||
this.serviceId4k,
|
this.serviceId4k,
|
||||||
@@ -297,7 +326,9 @@ class Media {
|
|||||||
if (this.mediaType === MediaType.TV) {
|
if (this.mediaType === MediaType.TV) {
|
||||||
if (
|
if (
|
||||||
this.externalServiceId !== undefined &&
|
this.externalServiceId !== undefined &&
|
||||||
this.serviceId !== undefined
|
this.externalServiceId !== null &&
|
||||||
|
this.serviceId !== undefined &&
|
||||||
|
this.serviceId !== null
|
||||||
) {
|
) {
|
||||||
this.downloadStatus = downloadTracker.getSeriesProgress(
|
this.downloadStatus = downloadTracker.getSeriesProgress(
|
||||||
this.serviceId,
|
this.serviceId,
|
||||||
@@ -307,7 +338,9 @@ class Media {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
this.externalServiceId4k !== undefined &&
|
this.externalServiceId4k !== undefined &&
|
||||||
this.serviceId4k !== undefined
|
this.externalServiceId4k !== null &&
|
||||||
|
this.serviceId4k !== undefined &&
|
||||||
|
this.serviceId4k !== null
|
||||||
) {
|
) {
|
||||||
this.downloadStatus4k = downloadTracker.getSeriesProgress(
|
this.downloadStatus4k = downloadTracker.getSeriesProgress(
|
||||||
this.serviceId4k,
|
this.serviceId4k,
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
|
import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
|
||||||
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
|
import type {
|
||||||
|
AddSeriesOptions,
|
||||||
|
SonarrSeries,
|
||||||
|
} from '@server/api/servarr/sonarr';
|
||||||
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||||
|
import {
|
||||||
|
MediaRequestStatus,
|
||||||
|
MediaStatus,
|
||||||
|
MediaType,
|
||||||
|
} from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||||
|
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||||
|
import { Permission } from '@server/lib/permissions';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
import { isEqual, truncate } from 'lodash';
|
import { isEqual, truncate } from 'lodash';
|
||||||
import {
|
import {
|
||||||
AfterInsert,
|
AfterInsert,
|
||||||
@@ -6,30 +26,347 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
getRepository,
|
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
RelationCount,
|
RelationCount,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import RadarrAPI, { RadarrMovieOptions } from '../api/servarr/radarr';
|
|
||||||
import SonarrAPI, {
|
|
||||||
AddSeriesOptions,
|
|
||||||
SonarrSeries,
|
|
||||||
} from '../api/servarr/sonarr';
|
|
||||||
import TheMovieDb from '../api/themoviedb';
|
|
||||||
import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants';
|
|
||||||
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
|
|
||||||
import notificationManager, { Notification } from '../lib/notifications';
|
|
||||||
import { getSettings } from '../lib/settings';
|
|
||||||
import logger from '../logger';
|
|
||||||
import Media from './Media';
|
import Media from './Media';
|
||||||
import SeasonRequest from './SeasonRequest';
|
import SeasonRequest from './SeasonRequest';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
|
export class RequestPermissionError extends Error {}
|
||||||
|
export class QuotaRestrictedError extends Error {}
|
||||||
|
export class DuplicateMediaRequestError extends Error {}
|
||||||
|
export class NoSeasonsAvailableError extends Error {}
|
||||||
|
|
||||||
|
type MediaRequestOptions = {
|
||||||
|
isAutoRequest?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class MediaRequest {
|
export class MediaRequest {
|
||||||
|
public static async request(
|
||||||
|
requestBody: MediaRequestBody,
|
||||||
|
user: User,
|
||||||
|
options: MediaRequestOptions = {}
|
||||||
|
): Promise<MediaRequest> {
|
||||||
|
const tmdb = new TheMovieDb();
|
||||||
|
const mediaRepository = getRepository(Media);
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
|
||||||
|
let requestUser = user;
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestBody.userId &&
|
||||||
|
!requestUser.hasPermission([
|
||||||
|
Permission.MANAGE_USERS,
|
||||||
|
Permission.MANAGE_REQUESTS,
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
throw new RequestPermissionError(
|
||||||
|
'You do not have permission to modify the request user.'
|
||||||
|
);
|
||||||
|
} else if (requestBody.userId) {
|
||||||
|
requestUser = await userRepository.findOneOrFail({
|
||||||
|
where: { id: requestBody.userId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestUser) {
|
||||||
|
throw new Error('User missing from request context.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
requestBody.mediaType === MediaType.MOVIE &&
|
||||||
|
!requestUser.hasPermission(
|
||||||
|
requestBody.is4k
|
||||||
|
? [Permission.REQUEST_4K, Permission.REQUEST_4K_MOVIE]
|
||||||
|
: [Permission.REQUEST, Permission.REQUEST_MOVIE],
|
||||||
|
{
|
||||||
|
type: 'or',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new RequestPermissionError(
|
||||||
|
`You do not have permission to make ${
|
||||||
|
requestBody.is4k ? '4K ' : ''
|
||||||
|
}movie requests.`
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
requestBody.mediaType === MediaType.TV &&
|
||||||
|
!requestUser.hasPermission(
|
||||||
|
requestBody.is4k
|
||||||
|
? [Permission.REQUEST_4K, Permission.REQUEST_4K_TV]
|
||||||
|
: [Permission.REQUEST, Permission.REQUEST_TV],
|
||||||
|
{
|
||||||
|
type: 'or',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new RequestPermissionError(
|
||||||
|
`You do not have permission to make ${
|
||||||
|
requestBody.is4k ? '4K ' : ''
|
||||||
|
}series requests.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotas = await requestUser.getQuota();
|
||||||
|
|
||||||
|
if (requestBody.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
|
||||||
|
throw new QuotaRestrictedError('Movie Quota exceeded.');
|
||||||
|
} else if (requestBody.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||||
|
throw new QuotaRestrictedError('Series Quota exceeded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmdbMedia =
|
||||||
|
requestBody.mediaType === MediaType.MOVIE
|
||||||
|
? await tmdb.getMovie({ movieId: requestBody.mediaId })
|
||||||
|
: await tmdb.getTvShow({ tvId: requestBody.mediaId });
|
||||||
|
|
||||||
|
let media = await mediaRepository.findOne({
|
||||||
|
where: {
|
||||||
|
tmdbId: requestBody.mediaId,
|
||||||
|
mediaType: requestBody.mediaType,
|
||||||
|
},
|
||||||
|
relations: ['requests'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
media = new Media({
|
||||||
|
tmdbId: tmdbMedia.id,
|
||||||
|
tvdbId: requestBody.tvdbId ?? tmdbMedia.external_ids.tvdb_id,
|
||||||
|
status: !requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||||
|
status4k: requestBody.is4k ? MediaStatus.PENDING : MediaStatus.UNKNOWN,
|
||||||
|
mediaType: requestBody.mediaType,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
|
||||||
|
media.status = MediaStatus.PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.status4k === MediaStatus.UNKNOWN && requestBody.is4k) {
|
||||||
|
media.status4k = MediaStatus.PENDING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await requestRepository
|
||||||
|
.createQueryBuilder('request')
|
||||||
|
.leftJoin('request.media', 'media')
|
||||||
|
.leftJoinAndSelect('request.requestedBy', 'user')
|
||||||
|
.where('request.is4k = :is4k', { is4k: requestBody.is4k })
|
||||||
|
.andWhere('media.tmdbId = :tmdbId', { tmdbId: tmdbMedia.id })
|
||||||
|
.andWhere('media.mediaType = :mediaType', {
|
||||||
|
mediaType: requestBody.mediaType,
|
||||||
|
})
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (existing && existing.length > 0) {
|
||||||
|
// If there is an existing movie request that isn't declined, don't allow a new one.
|
||||||
|
if (
|
||||||
|
requestBody.mediaType === MediaType.MOVIE &&
|
||||||
|
existing[0].status !== MediaRequestStatus.DECLINED
|
||||||
|
) {
|
||||||
|
logger.warn('Duplicate request for media blocked', {
|
||||||
|
tmdbId: tmdbMedia.id,
|
||||||
|
mediaType: requestBody.mediaType,
|
||||||
|
is4k: requestBody.is4k,
|
||||||
|
label: 'Media Request',
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new DuplicateMediaRequestError(
|
||||||
|
'Request for this media already exists.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an existing auto-request for this media exists from the same user,
|
||||||
|
// don't allow a new one.
|
||||||
|
if (
|
||||||
|
existing.find(
|
||||||
|
(r) => r.requestedBy.id === requestUser.id && r.isAutoRequest
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new DuplicateMediaRequestError(
|
||||||
|
'Auto-request for this media and user already exists.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestBody.mediaType === MediaType.MOVIE) {
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
|
||||||
|
const request = new MediaRequest({
|
||||||
|
type: MediaType.MOVIE,
|
||||||
|
media,
|
||||||
|
requestedBy: requestUser,
|
||||||
|
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||||
|
status: user.hasPermission(
|
||||||
|
[
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K
|
||||||
|
: Permission.AUTO_APPROVE,
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||||
|
: Permission.AUTO_APPROVE_MOVIE,
|
||||||
|
Permission.MANAGE_REQUESTS,
|
||||||
|
],
|
||||||
|
{ type: 'or' }
|
||||||
|
)
|
||||||
|
? MediaRequestStatus.APPROVED
|
||||||
|
: MediaRequestStatus.PENDING,
|
||||||
|
modifiedBy: user.hasPermission(
|
||||||
|
[
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K
|
||||||
|
: Permission.AUTO_APPROVE,
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K_MOVIE
|
||||||
|
: Permission.AUTO_APPROVE_MOVIE,
|
||||||
|
Permission.MANAGE_REQUESTS,
|
||||||
|
],
|
||||||
|
{ type: 'or' }
|
||||||
|
)
|
||||||
|
? user
|
||||||
|
: undefined,
|
||||||
|
is4k: requestBody.is4k,
|
||||||
|
serverId: requestBody.serverId,
|
||||||
|
profileId: requestBody.profileId,
|
||||||
|
rootFolder: requestBody.rootFolder,
|
||||||
|
tags: requestBody.tags,
|
||||||
|
isAutoRequest: options.isAutoRequest ?? false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestRepository.save(request);
|
||||||
|
return request;
|
||||||
|
} else {
|
||||||
|
const tmdbMediaShow = tmdbMedia as Awaited<
|
||||||
|
ReturnType<typeof tmdb.getTvShow>
|
||||||
|
>;
|
||||||
|
const requestedSeasons =
|
||||||
|
requestBody.seasons === 'all'
|
||||||
|
? tmdbMediaShow.seasons
|
||||||
|
.map((season) => season.season_number)
|
||||||
|
.filter((sn) => sn > 0)
|
||||||
|
: (requestBody.seasons as number[]);
|
||||||
|
let existingSeasons: number[] = [];
|
||||||
|
|
||||||
|
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||||
|
// already requested. In the case they were, we just throw out any duplicates but still approve the request.
|
||||||
|
// (Unless there are no seasons, in which case we abort)
|
||||||
|
if (media.requests) {
|
||||||
|
existingSeasons = media.requests
|
||||||
|
.filter(
|
||||||
|
(request) =>
|
||||||
|
request.is4k === requestBody.is4k &&
|
||||||
|
request.status !== MediaRequestStatus.DECLINED
|
||||||
|
)
|
||||||
|
.reduce((seasons, request) => {
|
||||||
|
const combinedSeasons = request.seasons.map(
|
||||||
|
(season) => season.seasonNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...seasons, ...combinedSeasons];
|
||||||
|
}, [] as number[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should also check seasons that are available/partially available but don't have existing requests
|
||||||
|
if (media.seasons) {
|
||||||
|
existingSeasons = [
|
||||||
|
...existingSeasons,
|
||||||
|
...media.seasons
|
||||||
|
.filter(
|
||||||
|
(season) =>
|
||||||
|
season[requestBody.is4k ? 'status4k' : 'status'] !==
|
||||||
|
MediaStatus.UNKNOWN
|
||||||
|
)
|
||||||
|
.map((season) => season.seasonNumber),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalSeasons = requestedSeasons.filter(
|
||||||
|
(rs) => !existingSeasons.includes(rs)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (finalSeasons.length === 0) {
|
||||||
|
throw new NoSeasonsAvailableError('No seasons available to request');
|
||||||
|
} else if (
|
||||||
|
quotas.tv.limit &&
|
||||||
|
finalSeasons.length > (quotas.tv.remaining ?? 0)
|
||||||
|
) {
|
||||||
|
throw new QuotaRestrictedError('Series Quota exceeded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
|
||||||
|
const request = new MediaRequest({
|
||||||
|
type: MediaType.TV,
|
||||||
|
media,
|
||||||
|
requestedBy: requestUser,
|
||||||
|
// If the user is an admin or has the "auto approve" permission, automatically approve the request
|
||||||
|
status: user.hasPermission(
|
||||||
|
[
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K
|
||||||
|
: Permission.AUTO_APPROVE,
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K_TV
|
||||||
|
: Permission.AUTO_APPROVE_TV,
|
||||||
|
Permission.MANAGE_REQUESTS,
|
||||||
|
],
|
||||||
|
{ type: 'or' }
|
||||||
|
)
|
||||||
|
? MediaRequestStatus.APPROVED
|
||||||
|
: MediaRequestStatus.PENDING,
|
||||||
|
modifiedBy: user.hasPermission(
|
||||||
|
[
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K
|
||||||
|
: Permission.AUTO_APPROVE,
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K_TV
|
||||||
|
: Permission.AUTO_APPROVE_TV,
|
||||||
|
Permission.MANAGE_REQUESTS,
|
||||||
|
],
|
||||||
|
{ type: 'or' }
|
||||||
|
)
|
||||||
|
? user
|
||||||
|
: undefined,
|
||||||
|
is4k: requestBody.is4k,
|
||||||
|
serverId: requestBody.serverId,
|
||||||
|
profileId: requestBody.profileId,
|
||||||
|
rootFolder: requestBody.rootFolder,
|
||||||
|
languageProfileId: requestBody.languageProfileId,
|
||||||
|
tags: requestBody.tags,
|
||||||
|
seasons: finalSeasons.map(
|
||||||
|
(sn) =>
|
||||||
|
new SeasonRequest({
|
||||||
|
seasonNumber: sn,
|
||||||
|
status: user.hasPermission(
|
||||||
|
[
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K
|
||||||
|
: Permission.AUTO_APPROVE,
|
||||||
|
requestBody.is4k
|
||||||
|
? Permission.AUTO_APPROVE_4K_TV
|
||||||
|
: Permission.AUTO_APPROVE_TV,
|
||||||
|
Permission.MANAGE_REQUESTS,
|
||||||
|
],
|
||||||
|
{ type: 'or' }
|
||||||
|
)
|
||||||
|
? MediaRequestStatus.APPROVED
|
||||||
|
: MediaRequestStatus.PENDING,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
isAutoRequest: options.isAutoRequest ?? false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestRepository.save(request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
public id: number;
|
public id: number;
|
||||||
|
|
||||||
@@ -120,6 +457,9 @@ export class MediaRequest {
|
|||||||
})
|
})
|
||||||
public tags?: number[];
|
public tags?: number[];
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
public isAutoRequest: boolean;
|
||||||
|
|
||||||
constructor(init?: Partial<MediaRequest>) {
|
constructor(init?: Partial<MediaRequest>) {
|
||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
@@ -147,6 +487,10 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sendNotification(media, Notification.MEDIA_PENDING);
|
this.sendNotification(media, Notification.MEDIA_PENDING);
|
||||||
|
|
||||||
|
if (this.isAutoRequest) {
|
||||||
|
this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +535,14 @@ export class MediaRequest {
|
|||||||
: Notification.MEDIA_APPROVED
|
: Notification.MEDIA_APPROVED
|
||||||
: Notification.MEDIA_DECLINED
|
: Notification.MEDIA_DECLINED
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.status === MediaRequestStatus.APPROVED &&
|
||||||
|
autoApproved &&
|
||||||
|
this.isAutoRequest
|
||||||
|
) {
|
||||||
|
this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +559,7 @@ export class MediaRequest {
|
|||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const media = await mediaRepository.findOne({
|
const media = await mediaRepository.findOne({
|
||||||
where: { id: this.media.id },
|
where: { id: this.media.id },
|
||||||
relations: ['requests'],
|
relations: { requests: true },
|
||||||
});
|
});
|
||||||
if (!media) {
|
if (!media) {
|
||||||
logger.error('Media data not found', {
|
logger.error('Media data not found', {
|
||||||
@@ -272,7 +624,7 @@ export class MediaRequest {
|
|||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const fullMedia = await mediaRepository.findOneOrFail({
|
const fullMedia = await mediaRepository.findOneOrFail({
|
||||||
where: { id: this.media.id },
|
where: { id: this.media.id },
|
||||||
relations: ['requests'],
|
relations: { requests: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -352,7 +704,7 @@ export class MediaRequest {
|
|||||||
|
|
||||||
let rootFolder = radarrSettings.activeDirectory;
|
let rootFolder = radarrSettings.activeDirectory;
|
||||||
let qualityProfile = radarrSettings.activeProfileId;
|
let qualityProfile = radarrSettings.activeProfileId;
|
||||||
let tags = radarrSettings.tags;
|
let tags = radarrSettings.tags ? [...radarrSettings.tags] : [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.rootFolder &&
|
this.rootFolder &&
|
||||||
@@ -412,10 +764,51 @@ export class MediaRequest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (radarrSettings.tagRequests) {
|
||||||
|
let userTag = (await radarr.getTags()).find((v) =>
|
||||||
|
v.label.startsWith(this.requestedBy.id + ' - ')
|
||||||
|
);
|
||||||
|
if (!userTag) {
|
||||||
|
logger.info(`Requester has no active tag. Creating new`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
userId: this.requestedBy.id,
|
||||||
|
newTag:
|
||||||
|
this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
userTag = await radarr.createTag({
|
||||||
|
label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (userTag.id) {
|
||||||
|
if (!tags?.find((v) => v === userTag?.id)) {
|
||||||
|
tags?.push(userTag.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`Requester has no tag and failed to add one`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
userId: this.requestedBy.id,
|
||||||
|
radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||||
) {
|
) {
|
||||||
throw new Error('Media already available');
|
logger.warn('Media already exists, marking request as APPROVED', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
this.status = MediaRequestStatus.APPROVED;
|
||||||
|
await requestRepository.save(this);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const radarrMovieOptions: RadarrMovieOptions = {
|
const radarrMovieOptions: RadarrMovieOptions = {
|
||||||
@@ -452,10 +845,13 @@ export class MediaRequest {
|
|||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
})
|
})
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
const requestRepository = getRepository(MediaRequest);
|
||||||
await mediaRepository.save(media);
|
|
||||||
|
this.status = MediaRequestStatus.FAILED;
|
||||||
|
requestRepository.save(this);
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'Something went wrong sending movie request to Radarr, marking status as UNKNOWN',
|
'Something went wrong sending movie request to Radarr, marking status as FAILED',
|
||||||
{
|
{
|
||||||
label: 'Media Request',
|
label: 'Media Request',
|
||||||
requestId: this.id,
|
requestId: this.id,
|
||||||
@@ -543,7 +939,7 @@ export class MediaRequest {
|
|||||||
|
|
||||||
const media = await mediaRepository.findOne({
|
const media = await mediaRepository.findOne({
|
||||||
where: { id: this.media.id },
|
where: { id: this.media.id },
|
||||||
relations: ['requests'],
|
relations: { requests: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!media) {
|
if (!media) {
|
||||||
@@ -553,7 +949,16 @@ export class MediaRequest {
|
|||||||
if (
|
if (
|
||||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||||
) {
|
) {
|
||||||
throw new Error('Media already available');
|
logger.warn('Media already exists, marking request as APPROVED', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
this.status = MediaRequestStatus.APPROVED;
|
||||||
|
await requestRepository.save(this);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tmdb = new TheMovieDb();
|
const tmdb = new TheMovieDb();
|
||||||
@@ -579,7 +984,7 @@ export class MediaRequest {
|
|||||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
seriesType = 'anime';
|
seriesType = sonarrSettings.animeSeriesType ?? 'anime';
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootFolder =
|
let rootFolder =
|
||||||
@@ -597,7 +1002,11 @@ export class MediaRequest {
|
|||||||
let tags =
|
let tags =
|
||||||
seriesType === 'anime'
|
seriesType === 'anime'
|
||||||
? sonarrSettings.animeTags
|
? sonarrSettings.animeTags
|
||||||
: sonarrSettings.tags;
|
? [...sonarrSettings.animeTags]
|
||||||
|
: []
|
||||||
|
: sonarrSettings.tags
|
||||||
|
? [...sonarrSettings.tags]
|
||||||
|
: [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.rootFolder &&
|
this.rootFolder &&
|
||||||
@@ -649,6 +1058,38 @@ export class MediaRequest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sonarrSettings.tagRequests) {
|
||||||
|
let userTag = (await sonarr.getTags()).find((v) =>
|
||||||
|
v.label.startsWith(this.requestedBy.id + ' - ')
|
||||||
|
);
|
||||||
|
if (!userTag) {
|
||||||
|
logger.info(`Requester has no active tag. Creating new`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
userId: this.requestedBy.id,
|
||||||
|
newTag:
|
||||||
|
this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
userTag = await sonarr.createTag({
|
||||||
|
label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (userTag.id) {
|
||||||
|
if (!tags?.find((v) => v === userTag?.id)) {
|
||||||
|
tags?.push(userTag.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`Requester has no tag and failed to add one`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: this.id,
|
||||||
|
mediaId: this.media.id,
|
||||||
|
userId: this.requestedBy.id,
|
||||||
|
sonarrServer: sonarrSettings.hostname + ':' + sonarrSettings.port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sonarrSeriesOptions: AddSeriesOptions = {
|
const sonarrSeriesOptions: AddSeriesOptions = {
|
||||||
profileId: qualityProfile,
|
profileId: qualityProfile,
|
||||||
languageProfileId: languageProfile,
|
languageProfileId: languageProfile,
|
||||||
@@ -670,7 +1111,7 @@ export class MediaRequest {
|
|||||||
// We grab media again here to make sure we have the latest version of it
|
// We grab media again here to make sure we have the latest version of it
|
||||||
const media = await mediaRepository.findOne({
|
const media = await mediaRepository.findOne({
|
||||||
where: { id: this.media.id },
|
where: { id: this.media.id },
|
||||||
relations: ['requests'],
|
relations: { requests: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!media) {
|
if (!media) {
|
||||||
@@ -685,10 +1126,13 @@ export class MediaRequest {
|
|||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
})
|
})
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
const requestRepository = getRepository(MediaRequest);
|
||||||
await mediaRepository.save(media);
|
|
||||||
|
this.status = MediaRequestStatus.FAILED;
|
||||||
|
requestRepository.save(this);
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'Something went wrong sending series request to Sonarr, marking status as UNKNOWN',
|
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
||||||
{
|
{
|
||||||
label: 'Media Request',
|
label: 'Media Request',
|
||||||
requestId: this.id,
|
requestId: this.id,
|
||||||
@@ -723,6 +1167,7 @@ export class MediaRequest {
|
|||||||
const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series';
|
const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series';
|
||||||
let event: string | undefined;
|
let event: string | undefined;
|
||||||
let notifyAdmin = true;
|
let notifyAdmin = true;
|
||||||
|
let notifySystem = true;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Notification.MEDIA_APPROVED:
|
case Notification.MEDIA_APPROVED:
|
||||||
@@ -736,6 +1181,13 @@ export class MediaRequest {
|
|||||||
case Notification.MEDIA_PENDING:
|
case Notification.MEDIA_PENDING:
|
||||||
event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`;
|
event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`;
|
||||||
break;
|
break;
|
||||||
|
case Notification.MEDIA_AUTO_REQUESTED:
|
||||||
|
event = `${
|
||||||
|
this.is4k ? '4K ' : ''
|
||||||
|
}${mediaType} Request Automatically Submitted`;
|
||||||
|
notifyAdmin = false;
|
||||||
|
notifySystem = false;
|
||||||
|
break;
|
||||||
case Notification.MEDIA_AUTO_APPROVED:
|
case Notification.MEDIA_AUTO_APPROVED:
|
||||||
event = `${
|
event = `${
|
||||||
this.is4k ? '4K ' : ''
|
this.is4k ? '4K ' : ''
|
||||||
@@ -752,6 +1204,7 @@ export class MediaRequest {
|
|||||||
media,
|
media,
|
||||||
request: this,
|
request: this,
|
||||||
notifyAdmin,
|
notifyAdmin,
|
||||||
|
notifySystem,
|
||||||
notifyUser: notifyAdmin ? undefined : this.requestedBy,
|
notifyUser: notifyAdmin ? undefined : this.requestedBy,
|
||||||
event,
|
event,
|
||||||
subject: `${movie.title}${
|
subject: `${movie.title}${
|
||||||
@@ -770,6 +1223,7 @@ export class MediaRequest {
|
|||||||
media,
|
media,
|
||||||
request: this,
|
request: this,
|
||||||
notifyAdmin,
|
notifyAdmin,
|
||||||
|
notifySystem,
|
||||||
notifyUser: notifyAdmin ? undefined : this.requestedBy,
|
notifyUser: notifyAdmin ? undefined : this.requestedBy,
|
||||||
event,
|
event,
|
||||||
subject: `${tv.name}${
|
subject: `${tv.name}${
|
||||||
@@ -801,3 +1255,5 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default MediaRequest;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import { MediaStatus } from '@server/constants/media';
|
||||||
import {
|
import {
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
Column,
|
||||||
ManyToOne,
|
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { MediaStatus } from '../constants/media';
|
|
||||||
import Media from './Media';
|
import Media from './Media';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
import { MediaRequestStatus } from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
import {
|
import {
|
||||||
Entity,
|
AfterRemove,
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
Entity,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { MediaRequestStatus } from '../constants/media';
|
|
||||||
import { MediaRequest } from './MediaRequest';
|
import { MediaRequest } from './MediaRequest';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -34,6 +36,18 @@ class SeasonRequest {
|
|||||||
constructor(init?: Partial<SeasonRequest>) {
|
constructor(init?: Partial<SeasonRequest>) {
|
||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterRemove()
|
||||||
|
public async handleRemoveParent(): Promise<void> {
|
||||||
|
const mediaRequestRepository = getRepository(MediaRequest);
|
||||||
|
const requestToBeDeleted = await mediaRequestRepository.findOneOrFail({
|
||||||
|
where: { id: this.request.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestToBeDeleted.seasons.length === 0) {
|
||||||
|
await mediaRequestRepository.delete({ id: this.request.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SeasonRequest;
|
export default SeasonRequest;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ISession } from 'connect-typeorm';
|
import type { ISession } from 'connect-typeorm';
|
||||||
import { Index, Column, PrimaryColumn, Entity } from 'typeorm';
|
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Session implements ISession {
|
export class Session implements ISession {
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
import { MediaRequestStatus, MediaType } from '@server/constants/media';
|
||||||
|
import { UserType } from '@server/constants/user';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import { Watchlist } from '@server/entity/Watchlist';
|
||||||
|
import type { QuotaResponse } from '@server/interfaces/api/userInterfaces';
|
||||||
|
import PreparedEmail from '@server/lib/email';
|
||||||
|
import type { PermissionCheckOptions } from '@server/lib/permissions';
|
||||||
|
import { hasPermission, Permission } from '@server/lib/permissions';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
import { AfterDate } from '@server/utils/dateHelpers';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -7,8 +18,6 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
getRepository,
|
|
||||||
MoreThan,
|
|
||||||
Not,
|
Not,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
@@ -16,17 +25,6 @@ import {
|
|||||||
RelationCount,
|
RelationCount,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
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 Issue from './Issue';
|
import Issue from './Issue';
|
||||||
import { MediaRequest } from './MediaRequest';
|
import { MediaRequest } from './MediaRequest';
|
||||||
import SeasonRequest from './SeasonRequest';
|
import SeasonRequest from './SeasonRequest';
|
||||||
@@ -42,7 +40,7 @@ export class User {
|
|||||||
return users.map((u) => u.filter(showFiltered));
|
return users.map((u) => u.filter(showFiltered));
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly filteredFields: string[] = ['email'];
|
static readonly filteredFields: string[] = ['email', 'plexId'];
|
||||||
|
|
||||||
public displayName: string;
|
public displayName: string;
|
||||||
|
|
||||||
@@ -79,7 +77,7 @@ export class User {
|
|||||||
@Column({ type: 'integer', default: UserType.PLEX })
|
@Column({ type: 'integer', default: UserType.PLEX })
|
||||||
public userType: UserType;
|
public userType: UserType;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true, select: true })
|
||||||
public plexId?: number;
|
public plexId?: number;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
@@ -106,6 +104,9 @@ export class User {
|
|||||||
@OneToMany(() => MediaRequest, (request) => request.requestedBy)
|
@OneToMany(() => MediaRequest, (request) => request.requestedBy)
|
||||||
public requests: MediaRequest[];
|
public requests: MediaRequest[];
|
||||||
|
|
||||||
|
@OneToMany(() => Watchlist, (watchlist) => watchlist.requestedBy)
|
||||||
|
public watchlists: Watchlist[];
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public movieQuotaLimit?: number;
|
public movieQuotaLimit?: number;
|
||||||
|
|
||||||
@@ -270,13 +271,14 @@ export class User {
|
|||||||
if (movieQuotaDays) {
|
if (movieQuotaDays) {
|
||||||
movieDate.setDate(movieDate.getDate() - movieQuotaDays);
|
movieDate.setDate(movieDate.getDate() - movieQuotaDays);
|
||||||
}
|
}
|
||||||
const movieQuotaStartDate = movieDate.toJSON();
|
|
||||||
|
|
||||||
const movieQuotaUsed = movieQuotaLimit
|
const movieQuotaUsed = movieQuotaLimit
|
||||||
? await requestRepository.count({
|
? await requestRepository.count({
|
||||||
where: {
|
where: {
|
||||||
requestedBy: this,
|
requestedBy: {
|
||||||
createdAt: MoreThan(movieQuotaStartDate),
|
id: this.id,
|
||||||
|
},
|
||||||
|
createdAt: AfterDate(movieDate),
|
||||||
type: MediaType.MOVIE,
|
type: MediaType.MOVIE,
|
||||||
status: Not(MediaRequestStatus.DECLINED),
|
status: Not(MediaRequestStatus.DECLINED),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import type { NotificationAgentTypes } from '@server/interfaces/api/userSettingsInterfaces';
|
||||||
|
import { hasNotificationType, Notification } from '@server/lib/notifications';
|
||||||
|
import { NotificationAgentKey } from '@server/lib/settings';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
Entity,
|
Entity,
|
||||||
@@ -5,9 +8,6 @@ import {
|
|||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { NotificationAgentTypes } from '../interfaces/api/userSettingsInterfaces';
|
|
||||||
import { hasNotificationType, Notification } from '../lib/notifications';
|
|
||||||
import { NotificationAgentKey } from '../lib/settings';
|
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
export const ALL_NOTIFICATIONS = Object.values(Notification)
|
export const ALL_NOTIFICATIONS = Object.values(Notification)
|
||||||
@@ -51,12 +51,21 @@ export class UserSettings {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public pushoverUserKey?: string;
|
public pushoverUserKey?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public pushoverSound?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public telegramChatId?: string;
|
public telegramChatId?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public telegramSendSilently?: boolean;
|
public telegramSendSilently?: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public watchlistSyncMovies?: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public watchlistSyncTv?: boolean;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|||||||
157
server/entity/Watchlist.ts
Normal file
157
server/entity/Watchlist.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
|
import { MediaType } from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import Media from '@server/entity/Media';
|
||||||
|
import { User } from '@server/entity/User';
|
||||||
|
import type { WatchlistItem } from '@server/interfaces/api/discoverInterfaces';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Unique,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import type { ZodNumber, ZodOptional, ZodString } from 'zod';
|
||||||
|
|
||||||
|
export class DuplicateWatchlistRequestError extends Error {}
|
||||||
|
export class NotFoundError extends Error {
|
||||||
|
constructor(message = 'Not found') {
|
||||||
|
super(message);
|
||||||
|
this.name = 'NotFoundError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Unique('UNIQUE_USER_DB', ['tmdbId', 'requestedBy'])
|
||||||
|
export class Watchlist implements WatchlistItem {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
public ratingKey = '';
|
||||||
|
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
public mediaType: MediaType;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
title = '';
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@Index()
|
||||||
|
public tmdbId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.watchlists, {
|
||||||
|
eager: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
public requestedBy: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Media, (media) => media.watchlists, {
|
||||||
|
eager: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
public media: Media;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
public updatedAt: Date;
|
||||||
|
|
||||||
|
constructor(init?: Partial<Watchlist>) {
|
||||||
|
Object.assign(this, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createWatchlist({
|
||||||
|
watchlistRequest,
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
watchlistRequest: {
|
||||||
|
mediaType: MediaType;
|
||||||
|
ratingKey?: ZodOptional<ZodString>['_output'];
|
||||||
|
title?: ZodOptional<ZodString>['_output'];
|
||||||
|
tmdbId: ZodNumber['_output'];
|
||||||
|
};
|
||||||
|
user: User;
|
||||||
|
}): Promise<Watchlist> {
|
||||||
|
const watchlistRepository = getRepository(this);
|
||||||
|
const mediaRepository = getRepository(Media);
|
||||||
|
const tmdb = new TheMovieDb();
|
||||||
|
|
||||||
|
const tmdbMedia =
|
||||||
|
watchlistRequest.mediaType === MediaType.MOVIE
|
||||||
|
? await tmdb.getMovie({ movieId: watchlistRequest.tmdbId })
|
||||||
|
: await tmdb.getTvShow({ tvId: watchlistRequest.tmdbId });
|
||||||
|
|
||||||
|
const existing = await watchlistRepository
|
||||||
|
.createQueryBuilder('watchlist')
|
||||||
|
.leftJoinAndSelect('watchlist.requestedBy', 'user')
|
||||||
|
.where('user.id = :userId', { userId: user.id })
|
||||||
|
.andWhere('watchlist.tmdbId = :tmdbId', {
|
||||||
|
tmdbId: watchlistRequest.tmdbId,
|
||||||
|
})
|
||||||
|
.andWhere('watchlist.mediaType = :mediaType', {
|
||||||
|
mediaType: watchlistRequest.mediaType,
|
||||||
|
})
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (existing && existing.length > 0) {
|
||||||
|
logger.warn('Duplicate request for watchlist blocked', {
|
||||||
|
tmdbId: watchlistRequest.tmdbId,
|
||||||
|
mediaType: watchlistRequest.mediaType,
|
||||||
|
label: 'Watchlist',
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new DuplicateWatchlistRequestError();
|
||||||
|
}
|
||||||
|
|
||||||
|
let media = await mediaRepository.findOne({
|
||||||
|
where: {
|
||||||
|
tmdbId: watchlistRequest.tmdbId,
|
||||||
|
mediaType: watchlistRequest.mediaType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
media = new Media({
|
||||||
|
tmdbId: tmdbMedia.id,
|
||||||
|
tvdbId: tmdbMedia.external_ids.tvdb_id,
|
||||||
|
mediaType: watchlistRequest.mediaType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchlist = new this({
|
||||||
|
...watchlistRequest,
|
||||||
|
requestedBy: user,
|
||||||
|
media,
|
||||||
|
});
|
||||||
|
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
await watchlistRepository.save(watchlist);
|
||||||
|
return watchlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteWatchlist(
|
||||||
|
tmdbId: Watchlist['tmdbId'],
|
||||||
|
user: User
|
||||||
|
): Promise<Watchlist | null> {
|
||||||
|
const watchlistRepository = getRepository(this);
|
||||||
|
const watchlist = await watchlistRepository.findOneBy({
|
||||||
|
tmdbId,
|
||||||
|
requestedBy: { id: user.id },
|
||||||
|
});
|
||||||
|
if (!watchlist) {
|
||||||
|
throw new NotFoundError('not Found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watchlist) {
|
||||||
|
await watchlistRepository.delete(watchlist.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return watchlist;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,40 @@
|
|||||||
|
import PlexAPI from '@server/api/plexapi';
|
||||||
|
import dataSource, { getRepository } from '@server/datasource';
|
||||||
|
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
|
import { Session } from '@server/entity/Session';
|
||||||
|
import { User } from '@server/entity/User';
|
||||||
|
import { startJobs } from '@server/job/schedule';
|
||||||
|
import notificationManager from '@server/lib/notifications';
|
||||||
|
import DiscordAgent from '@server/lib/notifications/agents/discord';
|
||||||
|
import EmailAgent from '@server/lib/notifications/agents/email';
|
||||||
|
import GotifyAgent from '@server/lib/notifications/agents/gotify';
|
||||||
|
import LunaSeaAgent from '@server/lib/notifications/agents/lunasea';
|
||||||
|
import PushbulletAgent from '@server/lib/notifications/agents/pushbullet';
|
||||||
|
import PushoverAgent from '@server/lib/notifications/agents/pushover';
|
||||||
|
import SlackAgent from '@server/lib/notifications/agents/slack';
|
||||||
|
import TelegramAgent from '@server/lib/notifications/agents/telegram';
|
||||||
|
import WebhookAgent from '@server/lib/notifications/agents/webhook';
|
||||||
|
import WebPushAgent from '@server/lib/notifications/agents/webpush';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
import clearCookies from '@server/middleware/clearcookies';
|
||||||
|
import routes from '@server/routes';
|
||||||
|
import imageproxy from '@server/routes/imageproxy';
|
||||||
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
|
import restartFlag from '@server/utils/restartFlag';
|
||||||
import { getClientIp } from '@supercharge/request-ip';
|
import { getClientIp } from '@supercharge/request-ip';
|
||||||
import { TypeormStore } from 'connect-typeorm/out';
|
import { TypeormStore } from 'connect-typeorm/out';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import csurf from 'csurf';
|
import csurf from 'csurf';
|
||||||
import express, { NextFunction, Request, Response } from 'express';
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
|
import express from 'express';
|
||||||
import * as OpenApiValidator from 'express-openapi-validator';
|
import * as OpenApiValidator from 'express-openapi-validator';
|
||||||
import session, { Store } from 'express-session';
|
import type { Store } from 'express-session';
|
||||||
|
import session from 'express-session';
|
||||||
import next from 'next';
|
import next from 'next';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
import { createConnection, getRepository } from 'typeorm';
|
|
||||||
import YAML from 'yamljs';
|
import YAML from 'yamljs';
|
||||||
import PlexAPI from './api/plexapi';
|
|
||||||
import { Session } from './entity/Session';
|
|
||||||
import { User } from './entity/User';
|
|
||||||
import { startJobs } from './job/schedule';
|
|
||||||
import notificationManager from './lib/notifications';
|
|
||||||
import DiscordAgent from './lib/notifications/agents/discord';
|
|
||||||
import EmailAgent from './lib/notifications/agents/email';
|
|
||||||
import GotifyAgent from './lib/notifications/agents/gotify';
|
|
||||||
import LunaSeaAgent from './lib/notifications/agents/lunasea';
|
|
||||||
import PushbulletAgent from './lib/notifications/agents/pushbullet';
|
|
||||||
import PushoverAgent from './lib/notifications/agents/pushover';
|
|
||||||
import SlackAgent from './lib/notifications/agents/slack';
|
|
||||||
import TelegramAgent from './lib/notifications/agents/telegram';
|
|
||||||
import WebhookAgent from './lib/notifications/agents/webhook';
|
|
||||||
import WebPushAgent from './lib/notifications/agents/webpush';
|
|
||||||
import { getSettings } from './lib/settings';
|
|
||||||
import logger from './logger';
|
|
||||||
import routes from './routes';
|
|
||||||
import { getAppVersion } from './utils/appVersion';
|
|
||||||
|
|
||||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||||
|
|
||||||
@@ -40,7 +46,7 @@ const handle = app.getRequestHandler();
|
|||||||
app
|
app
|
||||||
.prepare()
|
.prepare()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
const dbConnection = await createConnection();
|
const dbConnection = await dataSource.initialize();
|
||||||
|
|
||||||
// Run migrations in production
|
// Run migrations in production
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
@@ -51,6 +57,7 @@ app
|
|||||||
|
|
||||||
// Load Settings
|
// Load Settings
|
||||||
const settings = getSettings().load();
|
const settings = getSettings().load();
|
||||||
|
restartFlag.initializeSettings(settings.main);
|
||||||
|
|
||||||
// Migrate library types
|
// Migrate library types
|
||||||
if (
|
if (
|
||||||
@@ -59,8 +66,8 @@ app
|
|||||||
) {
|
) {
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const admin = await userRepository.findOne({
|
const admin = await userRepository.findOne({
|
||||||
select: ['id', 'plexToken'],
|
select: { id: true, plexToken: true },
|
||||||
order: { id: 'ASC' },
|
where: { id: 1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (admin) {
|
if (admin) {
|
||||||
@@ -87,8 +94,21 @@ app
|
|||||||
new WebPushAgent(),
|
new WebPushAgent(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Start Jobs
|
const userRepository = getRepository(User);
|
||||||
startJobs();
|
const totalUsers = await userRepository.count();
|
||||||
|
if (totalUsers > 0) {
|
||||||
|
startJobs();
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`Skipping starting the scheduled jobs as we have no Plex/Jellyfin/Emby servers setup yet`,
|
||||||
|
{
|
||||||
|
label: 'Server',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap Discovery Sliders
|
||||||
|
await DiscoverSlider.bootstrapSliders();
|
||||||
|
|
||||||
const server = express();
|
const server = express();
|
||||||
if (settings.main.trustProxy) {
|
if (settings.main.trustProxy) {
|
||||||
@@ -142,7 +162,7 @@ app
|
|||||||
cookie: {
|
cookie: {
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: true,
|
sameSite: settings.main.csrfProtection ? 'strict' : 'lax',
|
||||||
secure: 'auto',
|
secure: 'auto',
|
||||||
},
|
},
|
||||||
store: new TypeormStore({
|
store: new TypeormStore({
|
||||||
@@ -172,6 +192,10 @@ app
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
server.use('/api/v1', routes);
|
server.use('/api/v1', routes);
|
||||||
|
|
||||||
|
// Do not set cookies so CDNs can cache them
|
||||||
|
server.use('/imageproxy', clearCookies, imageproxy);
|
||||||
|
|
||||||
server.get('*', (req, res) => handle(req, res));
|
server.get('*', (req, res) => handle(req, res));
|
||||||
server.use(
|
server.use(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -3,3 +3,17 @@ export interface GenreSliderItem {
|
|||||||
name: string;
|
name: string;
|
||||||
backdrops: string[];
|
backdrops: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WatchlistItem {
|
||||||
|
ratingKey: string;
|
||||||
|
tmdbId: number;
|
||||||
|
mediaType: 'movie' | 'tv';
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatchlistResponse {
|
||||||
|
page: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalResults: number;
|
||||||
|
results: WatchlistItem[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Issue from '../../entity/Issue';
|
import type Issue from '@server/entity/Issue';
|
||||||
import { PaginatedResponse } from './common';
|
import type { PaginatedResponse } from './common';
|
||||||
|
|
||||||
export interface IssueResultsResponse extends PaginatedResponse {
|
export interface IssueResultsResponse extends PaginatedResponse {
|
||||||
results: Issue[];
|
results: Issue[];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type Media from '../../entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
import { User } from '../../entity/User';
|
import type { User } from '@server/entity/User';
|
||||||
import { PaginatedResponse } from './common';
|
import type { PaginatedResponse } from './common';
|
||||||
|
|
||||||
export interface MediaResultsResponse extends PaginatedResponse {
|
export interface MediaResultsResponse extends PaginatedResponse {
|
||||||
results: Media[];
|
results: Media[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PersonCreditCast, PersonCreditCrew } from '../../models/Person';
|
import type { PersonCreditCast, PersonCreditCrew } from '@server/models/Person';
|
||||||
|
|
||||||
export interface PersonCombinedCreditsResponse {
|
export interface PersonCombinedCreditsResponse {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PlexSettings } from '../../lib/settings';
|
import type { PlexSettings } from '@server/lib/settings';
|
||||||
|
|
||||||
export interface PlexStatus {
|
export interface PlexStatus {
|
||||||
settings: PlexSettings;
|
settings: PlexSettings;
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
|
import type { MediaType } from '@server/constants/media';
|
||||||
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { PaginatedResponse } from './common';
|
import type { PaginatedResponse } from './common';
|
||||||
import type { MediaRequest } from '../../entity/MediaRequest';
|
|
||||||
|
|
||||||
export interface RequestResultsResponse extends PaginatedResponse {
|
export interface RequestResultsResponse extends PaginatedResponse {
|
||||||
results: MediaRequest[];
|
results: MediaRequest[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MediaRequestBody = {
|
||||||
|
mediaType: MediaType;
|
||||||
|
mediaId: number;
|
||||||
|
tvdbId?: number;
|
||||||
|
seasons?: number[] | 'all';
|
||||||
|
is4k?: boolean;
|
||||||
|
serverId?: number;
|
||||||
|
profileId?: number;
|
||||||
|
rootFolder?: string;
|
||||||
|
languageProfileId?: number;
|
||||||
|
userId?: number;
|
||||||
|
tags?: number[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { QualityProfile, RootFolder, Tag } from '../../api/servarr/base';
|
import type { QualityProfile, RootFolder, Tag } from '@server/api/servarr/base';
|
||||||
import { LanguageProfile } from '../../api/servarr/sonarr';
|
import type { LanguageProfile } from '@server/api/servarr/sonarr';
|
||||||
|
|
||||||
export interface ServiceCommonServer {
|
export interface ServiceCommonServer {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface SettingsAboutResponse {
|
|||||||
|
|
||||||
export interface PublicSettingsResponse {
|
export interface PublicSettingsResponse {
|
||||||
jellyfinHost?: string;
|
jellyfinHost?: string;
|
||||||
|
jellyfinExternalHost?: string;
|
||||||
jellyfinServerName?: string;
|
jellyfinServerName?: string;
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
applicationTitle: string;
|
applicationTitle: string;
|
||||||
@@ -54,9 +55,15 @@ export interface CacheItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CacheResponse {
|
||||||
|
apiCaches: CacheItem[];
|
||||||
|
imageCache: Record<'tmdb', { size: number; imageCount: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StatusResponse {
|
export interface StatusResponse {
|
||||||
version: string;
|
version: string;
|
||||||
commitTag: string;
|
commitTag: string;
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
commitsBehind: number;
|
commitsBehind: number;
|
||||||
|
restartRequired: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Media from '../../entity/Media';
|
import type Media from '@server/entity/Media';
|
||||||
import { MediaRequest } from '../../entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { User } from '../../entity/User';
|
import type { User } from '@server/entity/User';
|
||||||
import { PaginatedResponse } from './common';
|
import type { PaginatedResponse } from './common';
|
||||||
|
|
||||||
export interface UserResultsResponse extends PaginatedResponse {
|
export interface UserResultsResponse extends PaginatedResponse {
|
||||||
results: User[];
|
results: User[];
|
||||||
@@ -23,6 +23,7 @@ export interface QuotaResponse {
|
|||||||
movie: QuotaStatus;
|
movie: QuotaStatus;
|
||||||
tv: QuotaStatus;
|
tv: QuotaStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserWatchDataResponse {
|
export interface UserWatchDataResponse {
|
||||||
recentlyWatched: Media[];
|
recentlyWatched: Media[];
|
||||||
playCount: number;
|
playCount: number;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user