Compare commits
270 Commits
gh-pages
...
preview-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a23f62a02 | ||
|
|
29034b350d | ||
|
|
7438042757 | ||
|
|
0b0b76e58c | ||
|
|
a5cb505609 | ||
|
|
7cb127ec3f | ||
|
|
1635932375 | ||
|
|
c1aeab9538 | ||
|
|
70fb1f2b00 | ||
|
|
4cd02babba | ||
|
|
f5b3a526cb | ||
|
|
e5ab847547 | ||
|
|
40539cc4b1 | ||
|
|
0bd6d57834 | ||
|
|
f884ac9c66 | ||
|
|
c2d9d00b41 | ||
|
|
77a36f9714 | ||
|
|
f773e0fb2a | ||
|
|
767a24164d | ||
|
|
8394eb5ad4 | ||
|
|
b8425d6388 | ||
|
|
ebb7f00305 | ||
|
|
418d51590d | ||
|
|
a6dd4a8fed | ||
|
|
4d1163c343 | ||
|
|
b085e12ff9 | ||
|
|
33e7a153aa | ||
|
|
9891a7577c | ||
|
|
077e355c77 | ||
|
|
21ab20bba9 | ||
|
|
cdfb30ea16 | ||
|
|
771ecdf781 | ||
|
|
863b675c77 | ||
|
|
5b998bef82 | ||
|
|
0113612ced | ||
|
|
f8c9689745 | ||
|
|
af8d6b475c | ||
|
|
dcc13080bc | ||
|
|
e97a13e1e4 | ||
|
|
1de518d915 | ||
|
|
4e44282387 | ||
|
|
67bd639a43 | ||
|
|
ada467ecf4 | ||
|
|
9cc6930fed | ||
|
|
3b4d6bf5b8 | ||
|
|
07e4662205 | ||
|
|
4eddbaa71b | ||
|
|
27112be933 | ||
|
|
a790b1abcc | ||
|
|
f0a6055774 | ||
|
|
a3f4773a35 | ||
|
|
73d8efaa54 | ||
|
|
9712f56054 | ||
|
|
b1f07f0eb2 | ||
|
|
64f05bcad6 | ||
|
|
80927b9705 | ||
|
|
d563b36186 | ||
|
|
117617188e | ||
|
|
525a538f34 | ||
|
|
0d2273ff6e | ||
|
|
e035cd84ae | ||
|
|
438ccfe9c3 | ||
|
|
c181cee328 | ||
|
|
98a5b05816 | ||
|
|
b29959b063 | ||
|
|
9a2c12e51c | ||
|
|
620135aeac | ||
|
|
2dbd1096d2 | ||
|
|
24d3f523fc | ||
|
|
2b7974fa06 | ||
|
|
907ba6fdea | ||
|
|
efaad21554 | ||
|
|
6ab463285d | ||
|
|
418f0c2eb8 | ||
|
|
002557d2d0 | ||
|
|
62c1a70b37 | ||
|
|
1b325e7c32 | ||
|
|
f247642b76 | ||
|
|
396cd968ef | ||
|
|
ca739315b2 | ||
|
|
9143a6c027 | ||
|
|
d7fc03650f | ||
|
|
80fc5c1a78 | ||
|
|
95737d36e6 | ||
|
|
0fd6ca85a4 | ||
|
|
7cee9b475d | ||
|
|
ff9af866f8 | ||
|
|
5ffe6419ee | ||
|
|
8afcf5a8d8 | ||
|
|
17d93a8cb9 | ||
|
|
549082c53e | ||
|
|
fbef7e2c72 | ||
|
|
93d2e26ae9 | ||
|
|
f09a432635 | ||
|
|
a8f84d4f74 | ||
|
|
88e96fa163 | ||
|
|
2d814c1416 | ||
|
|
2f4b848b2c | ||
|
|
0ee3e69a61 | ||
|
|
7fcc0eb66d | ||
|
|
5d9f613dd8 | ||
|
|
ae1ee777fa | ||
|
|
b9dc9bceb5 | ||
|
|
1ffcea2065 | ||
|
|
ce1b39f73b | ||
|
|
71143ca76b | ||
|
|
bebad2d814 | ||
|
|
1c6f5362d7 | ||
|
|
4f6192476b | ||
|
|
e97b3dfa9e | ||
|
|
221397db71 | ||
|
|
9ecbd98230 | ||
|
|
3cc34b0db6 | ||
|
|
5a323324f8 | ||
|
|
99e2e17b8a | ||
|
|
0f14cd9247 | ||
|
|
50db3ea27b | ||
|
|
131a5a2b0b | ||
|
|
7d08f58c76 | ||
|
|
425f0c854e | ||
|
|
b8dbfaaed0 | ||
|
|
24c6208a3c | ||
|
|
d71ee58302 | ||
|
|
1755877e66 | ||
|
|
f84d752bca | ||
|
|
0b331ca579 | ||
|
|
656cd91c9c | ||
|
|
81d7473c05 | ||
|
|
f718cec23f | ||
|
|
ac908026db | ||
|
|
d67ec571c5 | ||
|
|
f3ebf6028b | ||
|
|
465d42dd60 | ||
|
|
2f0e493257 | ||
|
|
ebe7d11a53 | ||
|
|
7e94ad7210 | ||
|
|
814a7357c0 | ||
|
|
f8a8ebdf76 | ||
|
|
8da4870997 | ||
|
|
c98becf936 | ||
|
|
9739e18949 | ||
|
|
5fc4ae57c0 | ||
|
|
b6e2e6ce61 | ||
|
|
66948b420f | ||
|
|
9a595296db | ||
|
|
8da02d01b2 | ||
|
|
8a097d5195 | ||
|
|
5345207940 | ||
|
|
f5055035b6 | ||
|
|
59c22ccc08 | ||
|
|
13d15d1dcf | ||
|
|
59b7859f7f | ||
|
|
0491a04ef1 | ||
|
|
d76d794411 | ||
|
|
1da2f258a7 | ||
|
|
347a24a97b | ||
|
|
66a5ab41ab | ||
|
|
7c734bc873 | ||
|
|
de6e591bae | ||
|
|
7daea46eaa | ||
|
|
fa443c05be | ||
|
|
b01f98f7e2 | ||
|
|
39dbb7f7e5 | ||
|
|
e96159d3a5 | ||
|
|
59a713d174 | ||
|
|
19450b46ef | ||
|
|
bc755d3ad3 | ||
|
|
44a9221a9d | ||
|
|
89831f7090 | ||
|
|
84fd884052 | ||
|
|
57767156f7 | ||
|
|
9fa47cbba2 | ||
|
|
17418f82af | ||
|
|
01bbeced65 | ||
|
|
27e3d465bd | ||
|
|
ef5e954db1 | ||
|
|
39a5ccb7f3 | ||
|
|
9b151feb4f | ||
|
|
fe5d016929 | ||
|
|
14f316a9a6 | ||
|
|
5c24e79b1d | ||
|
|
ba84212e68 | ||
|
|
f25b32aec8 | ||
|
|
5a13226877 | ||
|
|
694913c767 | ||
|
|
a2d2fd3c2a | ||
|
|
cb94ad5a2e | ||
|
|
2829c2548a | ||
|
|
64f4610b9f | ||
|
|
2d3b777daf | ||
|
|
cf59102ef9 | ||
|
|
ca838a00fa | ||
|
|
f2ed101e52 | ||
|
|
4b4eeb6ec7 | ||
|
|
d331798b28 | ||
|
|
f2b63156d1 | ||
|
|
326001c3ec | ||
|
|
0bbcfcbd5e | ||
|
|
32e0b129fe | ||
|
|
a2b3408c9a | ||
|
|
cbb1a74526 | ||
|
|
26c37ec067 | ||
|
|
4e48fdf2cb | ||
|
|
a351264b87 | ||
|
|
9de304d17a | ||
|
|
4945b54298 | ||
|
|
a0f80fe764 | ||
|
|
92ba26207d | ||
|
|
96e1d40304 | ||
|
|
a5d22ba5b8 | ||
|
|
f390da4866 | ||
|
|
edfd80444c | ||
|
|
2b05ffface | ||
|
|
818aa60aac | ||
|
|
ee7e91c7c9 | ||
|
|
45ef150e36 | ||
|
|
54cfeefe74 | ||
|
|
89e0a831ec | ||
|
|
e57d2654d1 | ||
|
|
a02a0dd176 | ||
|
|
7423bbbffc | ||
|
|
32343f23a3 | ||
|
|
15cb949f1f | ||
|
|
cfd1bc2535 | ||
|
|
80f63017ac | ||
|
|
0c7e652672 | ||
|
|
bd4da6d5fc | ||
|
|
12f908de7f | ||
|
|
61dcd8e487 | ||
|
|
9aee8887d3 | ||
|
|
2348f23f43 | ||
|
|
74a2d25f15 | ||
|
|
a2c2d261fc | ||
|
|
71acfb1b1f | ||
|
|
29a32d0391 | ||
|
|
f7be4789a2 | ||
|
|
181cb19048 | ||
|
|
32c77f9e94 | ||
|
|
b43c1e350e | ||
|
|
64453320d3 | ||
|
|
36d98a2681 | ||
|
|
d5f817e734 | ||
|
|
422085523e | ||
|
|
fccfca6ed0 | ||
|
|
3fc14c9e22 | ||
|
|
62dbde448c | ||
|
|
0116c13e06 | ||
|
|
c96ca6742e | ||
|
|
c80d9a853a | ||
|
|
6cea8bba59 | ||
|
|
2be9c7dcc1 | ||
|
|
5cc4389825 | ||
|
|
dd6dbf1de9 | ||
|
|
c600566ac0 | ||
|
|
4db1df2ba5 | ||
|
|
3a363ae1ff | ||
|
|
084e1b224e | ||
|
|
b36bb3fa58 | ||
|
|
ae955e9e7c | ||
|
|
7a826a1308 | ||
|
|
f201fee1ff | ||
|
|
316c31bd06 | ||
|
|
9dd175bb2e | ||
|
|
31ca32df99 | ||
|
|
df2f05f129 | ||
|
|
5428b19c62 | ||
|
|
8dde04396d | ||
|
|
3f7c01853c | ||
|
|
31472009ab | ||
|
|
73890ac802 |
@@ -7,7 +7,7 @@
|
|||||||
"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": "jellyseerr",
|
"projectName": "jellyseerr",
|
||||||
"projectOwner": "Fallenbagel",
|
"projectOwner": "fallenbagel",
|
||||||
"repoType": "github",
|
"repoType": "github",
|
||||||
"repoHost": "https://github.com",
|
"repoHost": "https://github.com",
|
||||||
"skipCi": true,
|
"skipCi": true,
|
||||||
@@ -94,7 +94,8 @@
|
|||||||
"avatar_url": "https://avatars.githubusercontent.com/u/345752?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/345752?v=4",
|
||||||
"profile": "https://github.com/jab416171",
|
"profile": "https://github.com/jab416171",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc",
|
||||||
|
"code"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -248,7 +249,8 @@
|
|||||||
"avatar_url": "https://avatars.githubusercontent.com/u/854646?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/854646?v=4",
|
||||||
"profile": "http://www.piribisoft.com",
|
"profile": "http://www.piribisoft.com",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc",
|
||||||
|
"code"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -338,7 +340,8 @@
|
|||||||
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
|
||||||
"profile": "https://gauthierth.fr/",
|
"profile": "https://gauthierth.fr/",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code",
|
||||||
|
"maintenance"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -403,6 +406,411 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "mobihen",
|
||||||
|
"name": "Nir Israel Hen",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/35529491?v=4",
|
||||||
|
"profile": "https://mobihen.com",
|
||||||
|
"contributions": [
|
||||||
|
"translation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "XDark187",
|
||||||
|
"name": "Baraa",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/39034192?v=4",
|
||||||
|
"profile": "https://github.com/XDark187",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "franciscofsales",
|
||||||
|
"name": "Francisco Sales",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/7977645?v=4",
|
||||||
|
"profile": "https://github.com/franciscofsales",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "myselfolli",
|
||||||
|
"name": "Oliver Laing",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/37535998?v=4",
|
||||||
|
"profile": "https://github.com/myselfolli",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "M0NsTeRRR",
|
||||||
|
"name": "Ludovic Ortega",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/37785089?v=4",
|
||||||
|
"profile": "https://github.com/M0NsTeRRR",
|
||||||
|
"contributions": [
|
||||||
|
"security"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "j0srisk",
|
||||||
|
"name": "Joseph Risk",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
|
||||||
|
"profile": "http://josephrisk.com",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Loetwiek",
|
||||||
|
"name": "Loetwiek",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
|
||||||
|
"profile": "https://github.com/Loetwiek",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Fuochi",
|
||||||
|
"name": "Fuochi",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
|
||||||
|
"profile": "https://github.com/Fuochi",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "demrich",
|
||||||
|
"name": "David Emrich",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
|
||||||
|
"profile": "https://github.com/demrich",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "maxnatamo",
|
||||||
|
"name": "Max T. Kristiansen",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
|
||||||
|
"profile": "https://maxtrier.dk",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "DamsDev1",
|
||||||
|
"name": "Damien Fajole",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
|
||||||
|
"profile": "https://damsdev.me",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "AhmedNSidd",
|
||||||
|
"name": "Ahmed Siddiqui",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
|
||||||
|
"profile": "https://github.com/AhmedNSidd",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Zariel",
|
||||||
|
"name": "Chris Bannister",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/2213?v=4",
|
||||||
|
"profile": "https://github.com/Zariel",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "C4J3",
|
||||||
|
"name": "Joe",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/13005453?v=4",
|
||||||
|
"profile": "https://github.com/C4J3",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "guillaumearnx",
|
||||||
|
"name": "Guillaume ARNOUX",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/37373941?v=4",
|
||||||
|
"profile": "https://me.garnx.fr",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "dr-carrot",
|
||||||
|
"name": "dr-carrot",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/17272571?v=4",
|
||||||
|
"profile": "https://github.com/dr-carrot",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "gageorsburn",
|
||||||
|
"name": "Gage Orsburn",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/4692734?v=4",
|
||||||
|
"profile": "https://github.com/gageorsburn",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "GkhnGRBZ",
|
||||||
|
"name": "GkhnGRBZ",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/127258824?v=4",
|
||||||
|
"profile": "https://github.com/GkhnGRBZ",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "benhaney",
|
||||||
|
"name": "Ben Haney",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/31331498?v=4",
|
||||||
|
"profile": "http://benhaney.com",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Wunderharke",
|
||||||
|
"name": "Wunderharke",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5105672?v=4",
|
||||||
|
"profile": "https://github.com/Wunderharke",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "methbkts",
|
||||||
|
"name": "Metin Bektas",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/30674934?v=4",
|
||||||
|
"profile": "https://github.com/methbkts",
|
||||||
|
"contributions": [
|
||||||
|
"infra"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "andrewkolda",
|
||||||
|
"name": "andrewkolda",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/158614532?v=4",
|
||||||
|
"profile": "https://github.com/andrewkolda",
|
||||||
|
"contributions": [
|
||||||
|
"design"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "ishanjain28",
|
||||||
|
"name": "Ishan Jain",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/7921368?v=4",
|
||||||
|
"profile": "https://ishanjain.me",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "michaelhthomas",
|
||||||
|
"name": "Michael Thomas",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/18223295?v=4",
|
||||||
|
"profile": "http://michaelt.xyz",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "j0srisk",
|
||||||
|
"name": "Joseph Risk",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
|
||||||
|
"profile": "http://josephrisk.com",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Loetwiek",
|
||||||
|
"name": "Loetwiek",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
|
||||||
|
"profile": "https://github.com/Loetwiek",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Fuochi",
|
||||||
|
"name": "Fuochi",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
|
||||||
|
"profile": "https://github.com/Fuochi",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "demrich",
|
||||||
|
"name": "David Emrich",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
|
||||||
|
"profile": "https://github.com/demrich",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "maxnatamo",
|
||||||
|
"name": "Max T. Kristiansen",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
|
||||||
|
"profile": "https://maxtrier.dk",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "DamsDev1",
|
||||||
|
"name": "Damien Fajole",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
|
||||||
|
"profile": "https://damsdev.me",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "AhmedNSidd",
|
||||||
|
"name": "Ahmed Siddiqui",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
|
||||||
|
"profile": "https://github.com/AhmedNSidd",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "JackW6809",
|
||||||
|
"name": "JackOXI",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
|
||||||
|
"profile": "https://github.com/JackW6809",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "StancuFlorin",
|
||||||
|
"name": "Stancu Florin",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
|
||||||
|
"profile": "http://indicus.ro",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "RankWeis",
|
||||||
|
"name": "RankWeis",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/733691?v=4",
|
||||||
|
"profile": "https://github.com/RankWeis",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "j0srisk",
|
||||||
|
"name": "Joseph Risk",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
|
||||||
|
"profile": "http://josephrisk.com",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Loetwiek",
|
||||||
|
"name": "Loetwiek",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
|
||||||
|
"profile": "https://github.com/Loetwiek",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Fuochi",
|
||||||
|
"name": "Fuochi",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
|
||||||
|
"profile": "https://github.com/Fuochi",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "demrich",
|
||||||
|
"name": "David Emrich",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
|
||||||
|
"profile": "https://github.com/demrich",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "maxnatamo",
|
||||||
|
"name": "Max T. Kristiansen",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
|
||||||
|
"profile": "https://maxtrier.dk",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "DamsDev1",
|
||||||
|
"name": "Damien Fajole",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
|
||||||
|
"profile": "https://damsdev.me",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "AhmedNSidd",
|
||||||
|
"name": "Ahmed Siddiqui",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
|
||||||
|
"profile": "https://github.com/AhmedNSidd",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "JackW6809",
|
||||||
|
"name": "JackOXI",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
|
||||||
|
"profile": "https://github.com/JackW6809",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "StancuFlorin",
|
||||||
|
"name": "Stancu Florin",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
|
||||||
|
"profile": "http://indicus.ro",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "lmiklosko",
|
||||||
|
"name": "Lukas Miklosko",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/44380311?v=4",
|
||||||
|
"profile": "https://github.com/lmiklosko",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "gauthier-th",
|
||||||
|
"name": "Gauthier",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
|
||||||
|
"profile": "https://gauthierth.fr/",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ config/logs/*
|
|||||||
config/*.json
|
config/*.json
|
||||||
dist
|
dist
|
||||||
Dockerfile*
|
Dockerfile*
|
||||||
docker-compose.yml
|
compose.yaml
|
||||||
docs
|
docs
|
||||||
LICENSE
|
LICENSE
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -40,7 +40,7 @@ docs export-ignore
|
|||||||
.all-contributorsrc export-ignore
|
.all-contributorsrc export-ignore
|
||||||
.editorconfig export-ignore
|
.editorconfig export-ignore
|
||||||
Dockerfile.local export-ignore
|
Dockerfile.local export-ignore
|
||||||
docker-compose.yml export-ignore
|
compose.yaml export-ignore
|
||||||
stylelint.config.js export-ignore
|
stylelint.config.js export-ignore
|
||||||
|
|
||||||
public/os_logo_filled.png export-ignore
|
public/os_logo_filled.png export-ignore
|
||||||
|
|||||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,2 +1,2 @@
|
|||||||
# Global code ownership
|
# Global code ownership
|
||||||
* @Fallenbagel
|
* @Fallenbagel @gauthier-th
|
||||||
|
|||||||
12
.github/ISSUE_TEMPLATE/bug.yml
vendored
12
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: 🐛 Bug Report
|
name: 🐛 Bug Report
|
||||||
description: Report a problem
|
description: Report a problem
|
||||||
labels: ['type:bug', 'awaiting-triage']
|
labels: ['bug', 'awaiting triage']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -55,6 +55,16 @@ body:
|
|||||||
- tablet
|
- tablet
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: database
|
||||||
|
attributes:
|
||||||
|
options:
|
||||||
|
- SQLite (default)
|
||||||
|
- PostgreSQL
|
||||||
|
label: Database
|
||||||
|
description: Which database backend are you using?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
id: device
|
id: device
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
2
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: ✨ Feature Request
|
name: ✨ Feature Request
|
||||||
description: Suggest an idea
|
description: Suggest an idea
|
||||||
labels: ['type:enhancement', 'awaiting-triage']
|
labels: ['enhancement', 'awaiting triage']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
#### To-Dos
|
#### To-Dos
|
||||||
|
|
||||||
- [ ] Successful build `yarn build`
|
- [ ] Successful build `pnpm build`
|
||||||
- [ ] Translation keys `yarn i18n:extract`
|
- [ ] Translation keys `pnpm i18n:extract`
|
||||||
- [ ] Database migration (if required)
|
- [ ] Database migration (if required)
|
||||||
|
|
||||||
#### Issues Fixed or Closed
|
#### Issues Fixed or Closed
|
||||||
|
|||||||
89
.github/workflows/ci.yml
vendored
89
.github/workflows/ci.yml
vendored
@@ -12,8 +12,8 @@ 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-22.04
|
runs-on: ubuntu-24.04
|
||||||
container: node:20-alpine
|
container: node:22-alpine
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -43,15 +43,23 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
build_and_push:
|
build:
|
||||||
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-22.04
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- runner: ubuntu-24.04
|
||||||
|
platform: linux/amd64
|
||||||
|
- runner: ubuntu-24.04-arm
|
||||||
|
platform: linux/arm64
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
outputs:
|
||||||
|
digest-amd64: ${{ steps.set_outputs.outputs.digest-amd64 }}
|
||||||
|
digest-arm64: ${{ steps.set_outputs.outputs.digest-arm64 }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
@@ -70,24 +78,79 @@ jobs:
|
|||||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||||
env:
|
env:
|
||||||
OWNER: ${{ github.repository_owner }}
|
OWNER: ${{ github.repository_owner }}
|
||||||
- name: Build and push
|
- name: Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
fallenbagel/jellyseerr
|
||||||
|
ghcr.io/${{ env.OWNER_LC }}/jellyseerr
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=sha,prefix=,suffix=,format=short
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: ${{ matrix.platform }}
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
COMMIT_TAG=${{ github.sha }}
|
COMMIT_TAG=${{ github.sha }}
|
||||||
tags: |
|
BUILD_VERSION=develop
|
||||||
fallenbagel/jellyseerr:develop
|
BUILD_DATE=${{ github.event.repository.updated_at }}
|
||||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
|
outputs: |
|
||||||
|
type=image,push-by-digest=true,name=fallenbagel/jellyseerr,push=true
|
||||||
|
type=image,push-by-digest=true,name=ghcr.io/${{ env.OWNER_LC }}/jellyseerr,push=true
|
||||||
|
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||||
|
provenance: false
|
||||||
|
- name: Set outputs
|
||||||
|
id: set_outputs
|
||||||
|
run: |
|
||||||
|
platform="${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}"
|
||||||
|
echo "digest-${platform}=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
merge_and_push:
|
||||||
|
name: Create and Push Multi-arch Manifest
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
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: Create and push manifest
|
||||||
|
run: |
|
||||||
|
docker manifest create fallenbagel/jellyseerr:develop \
|
||||||
|
--amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \
|
||||||
|
--amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-arm64 }}
|
||||||
|
docker manifest push fallenbagel/jellyseerr:develop
|
||||||
|
|
||||||
|
# GHCR manifest
|
||||||
|
docker manifest create ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop \
|
||||||
|
--amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \
|
||||||
|
--amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-arm64 }}
|
||||||
|
docker manifest push ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
|
||||||
|
|
||||||
discord:
|
discord:
|
||||||
name: Send Discord Notification
|
name: Send Discord Notification
|
||||||
needs: build_and_push
|
needs: merge_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-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Get Build Job Status
|
- name: Get Build Job Status
|
||||||
uses: technote-space/workflow-conclusion-action@v3
|
uses: technote-space/workflow-conclusion-action@v3
|
||||||
|
|||||||
10
.github/workflows/cypress.yml
vendored
10
.github/workflows/cypress.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
- name: Pnpm Setup
|
- name: Pnpm Setup
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
@@ -36,3 +36,11 @@ jobs:
|
|||||||
# Fix test titles in cypress dashboard
|
# Fix test titles in cypress dashboard
|
||||||
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
|
COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}}
|
||||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
|
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}}
|
||||||
|
- name: Upload video files
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cypress-videos
|
||||||
|
path: |
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
||||||
|
|||||||
1
.github/workflows/docs-deploy.yml
vendored
1
.github/workflows/docs-deploy.yml
vendored
@@ -58,6 +58,7 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
name: Deploy to GitHub Pages
|
name: Deploy to GitHub Pages
|
||||||
needs: build
|
needs: build
|
||||||
|
concurrency: build-deploy-pages
|
||||||
|
|
||||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
135
.github/workflows/helm.yml
vendored
Normal file
135
.github/workflows/helm.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
name: Release Charts
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package-helm-chart:
|
||||||
|
name: Package helm chart
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
outputs:
|
||||||
|
has_artifacts: ${{ steps.check-artifacts.outputs.has_artifacts }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install helm
|
||||||
|
uses: azure/setup-helm@v4
|
||||||
|
|
||||||
|
- name: Install Oras
|
||||||
|
uses: oras-project/setup-oras@v1
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Package helm charts
|
||||||
|
run: |
|
||||||
|
mkdir -p ./.cr-release-packages
|
||||||
|
for chart_path in ./charts/*; do
|
||||||
|
if [ -d "$chart_path" ] && [ -f "$chart_path/Chart.yaml" ]; then
|
||||||
|
chart_name=$(grep '^name:' "$chart_path/Chart.yaml" | awk '{print $2}')
|
||||||
|
# get current version
|
||||||
|
current_version=$(grep '^version:' "$chart_path/Chart.yaml" | awk '{print $2}')
|
||||||
|
# try to get current release version
|
||||||
|
set +e
|
||||||
|
oras discover ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:${current_version}
|
||||||
|
oras_exit_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ $oras_exit_code -ne 0 ]; then
|
||||||
|
helm dependency build "$chart_path"
|
||||||
|
helm package "$chart_path" --destination ./.cr-release-packages
|
||||||
|
else
|
||||||
|
echo "No version change for $chart_name. Skipping."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Skipping $chart_name: Not a valid Helm chart"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Check if artifacts exist
|
||||||
|
id: check-artifacts
|
||||||
|
run: |
|
||||||
|
if ls .cr-release-packages/* >/dev/null 2>&1; then
|
||||||
|
echo "has_artifacts=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_artifacts=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: steps.check-artifacts.outputs.has_artifacts == 'true'
|
||||||
|
with:
|
||||||
|
name: artifacts
|
||||||
|
include-hidden-files: true
|
||||||
|
path: .cr-release-packages/
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publish to ghcr.io
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write # needed for pushing to github registry
|
||||||
|
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||||
|
needs: [package-helm-chart]
|
||||||
|
if: needs.package-helm-chart.outputs.has_artifacts == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install helm
|
||||||
|
uses: azure/setup-helm@v4
|
||||||
|
|
||||||
|
- name: Install Oras
|
||||||
|
uses: oras-project/setup-oras@v1
|
||||||
|
|
||||||
|
- name: Install Cosign
|
||||||
|
uses: sigstore/cosign-installer@v3
|
||||||
|
|
||||||
|
- name: Downloads artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: artifacts
|
||||||
|
path: .cr-release-packages/
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Push charts to GHCR
|
||||||
|
env:
|
||||||
|
COSIGN_YES: true
|
||||||
|
run: |
|
||||||
|
for chart_path in `find .cr-release-packages -name '*.tgz' -print`; do
|
||||||
|
# push chart to OCI
|
||||||
|
chart_release_file=$(basename "$chart_path")
|
||||||
|
chart_name=${chart_release_file%-*}
|
||||||
|
helm push ${chart_path} oci://ghcr.io/${GITHUB_REPOSITORY@L} |& tee helm-push-output.log
|
||||||
|
chart_digest=$(awk -F "[, ]+" '/Digest/{print $NF}' < helm-push-output.log)
|
||||||
|
# sign chart
|
||||||
|
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}@${chart_digest}"
|
||||||
|
# push artifacthub-repo.yml to OCI
|
||||||
|
oras push \
|
||||||
|
ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io \
|
||||||
|
--config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \
|
||||||
|
charts/$chart_name/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml \
|
||||||
|
|& tee oras-push-output.log
|
||||||
|
artifacthub_digest=$(grep "Digest:" oras-push-output.log | awk '{print $2}')
|
||||||
|
# sign artifacthub-repo.yml
|
||||||
|
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io@${artifacthub_digest}"
|
||||||
|
done
|
||||||
33
.github/workflows/lint-helm-charts.yml
vendored
Normal file
33
.github/workflows/lint-helm-charts.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Lint and Test Charts
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/lint-helm-charts.yml'
|
||||||
|
- 'charts/**'
|
||||||
|
jobs:
|
||||||
|
lint-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Helm
|
||||||
|
uses: azure/setup-helm@v4.2.0
|
||||||
|
- name: Ensure documentation is updated
|
||||||
|
uses: docker://jnorwood/helm-docs:v1.14.2
|
||||||
|
- name: Set up chart-testing
|
||||||
|
uses: helm/chart-testing-action@v2.6.1
|
||||||
|
- name: Run chart-testing (list-changed)
|
||||||
|
id: list-changed
|
||||||
|
run: |
|
||||||
|
changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }})
|
||||||
|
if [[ -n "$changed" ]]; then
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
- name: Run chart-testing
|
||||||
|
if: steps.list-changed.outputs.changed == 'true'
|
||||||
|
run: ct lint --target-branch ${{ github.event.repository.default_branch }} --validate-maintainers=false
|
||||||
2
.github/workflows/preview.yml
vendored
2
.github/workflows/preview.yml
vendored
@@ -33,5 +33,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
COMMIT_TAG=${{ github.sha }}
|
COMMIT_TAG=${{ github.sha }}
|
||||||
|
BUILD_VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||||
|
BUILD_DATE=${{ github.event.repository.updated_at }}
|
||||||
tags: |
|
tags: |
|
||||||
fallenbagel/jellyseerr:${{ steps.get_version.outputs.VERSION }}
|
fallenbagel/jellyseerr:${{ steps.get_version.outputs.VERSION }}
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -26,6 +26,12 @@ jobs:
|
|||||||
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@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GH_TOKEN }}
|
||||||
- name: Pnpm Setup
|
- name: Pnpm Setup
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/test-docs-deploy.yml
vendored
2
.github/workflows/test-docs-deploy.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
path:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'gen-docs/**'
|
- 'gen-docs/**'
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,6 +34,7 @@ yarn-error.log*
|
|||||||
# database
|
# database
|
||||||
config/db/*.sqlite3*
|
config/db/*.sqlite3*
|
||||||
config/settings.json
|
config/settings.json
|
||||||
|
config/settings.old.json
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
config/logs/*.log*
|
config/logs/*.log*
|
||||||
|
|||||||
@@ -8,3 +8,7 @@ pnpm-lock.yaml
|
|||||||
# assets
|
# assets
|
||||||
src/assets/
|
src/assets/
|
||||||
public/
|
public/
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# helm charts
|
||||||
|
**/charts
|
||||||
|
|||||||
@@ -3,11 +3,23 @@ module.exports = {
|
|||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
trailingComma: 'es5',
|
trailingComma: 'es5',
|
||||||
overrides: [
|
overrides: [
|
||||||
|
{
|
||||||
|
files: 'pnpm-lock.yaml',
|
||||||
|
options: {
|
||||||
|
rangeEnd: 0, // default: Infinity
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: 'gen-docs/pnpm-lock.yaml',
|
files: 'gen-docs/pnpm-lock.yaml',
|
||||||
options: {
|
options: {
|
||||||
rangeEnd: 0, // default: Infinity
|
rangeEnd: 0, // default: Infinity
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: 'charts/**',
|
||||||
|
options: {
|
||||||
|
rangeEnd: 0, // default: Infinity
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
|||||||
|
|
||||||
- HTML/Typescript/Javascript editor
|
- HTML/Typescript/Javascript editor
|
||||||
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
|
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
|
||||||
- [NodeJS](https://nodejs.org/en/download/) (Node 14.x or higher)
|
- [NodeJS](https://nodejs.org/en/download/) (Node 22.x)
|
||||||
- [Yarn](https://yarnpkg.com/)
|
- [Pnpm](https://pnpm.io/cli/install)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
@@ -18,7 +18,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/YOUR_USERNAME/jellyseerr.git
|
git clone https://github.com/YOUR_USERNAME/jellyseerr.git
|
||||||
cd overseerr/
|
cd jellyseerr/
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Add the remote `upstream`:
|
2. Add the remote `upstream`:
|
||||||
@@ -48,22 +48,37 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
|||||||
4. Run the development environment:
|
4. Run the development environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
pnpm install
|
||||||
yarn dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker-compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
- Alternatively, you can use [Docker](https://www.docker.com/) with `docker compose up -d`. This method does not require installing NodeJS or Yarn on your machine directly.
|
||||||
|
|
||||||
5. Create your patch and test your changes.
|
5. Create your patch and test your changes.
|
||||||
|
|
||||||
- Be sure to follow both the [code](#contributing-code) and [UI text](#ui-text-style) guidelines.
|
- Be sure to follow both the [code](#contributing-code) and [UI text](#ui-text-style) guidelines.
|
||||||
- Should you need to update your fork, you can do so by rebasing from `upstream`:
|
- Should you need to update your fork, you can do so by rebasing from `upstream`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git fetch upstream
|
git fetch upstream
|
||||||
git rebase upstream/develop
|
git rebase upstream/develop
|
||||||
git push origin BRANCH_NAME -f
|
git push origin BRANCH_NAME -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Helm Chart
|
||||||
|
|
||||||
|
Tools Required:
|
||||||
|
|
||||||
|
- [Helm](https://helm.sh/docs/intro/install/)
|
||||||
|
- [helm-docs](https://github.com/norwoodj/helm-docs)
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Make the necessary changes.
|
||||||
|
2. Test your changes.
|
||||||
|
3. Update the `version` in `charts/jellyseerr-chart/Chart.yaml` following [Semantic Versioning (SemVer)](https://semver.org/).
|
||||||
|
4. Run the `helm-docs` command to regenerate the chart's README.
|
||||||
|
|
||||||
### Contributing Code
|
### Contributing Code
|
||||||
|
|
||||||
- If you are taking on an existing bug or feature ticket, please comment on the [issue](https://github.com/fallenbagel/jellyseerr/issues) to avoid multiple people working on the same thing.
|
- If you are taking on an existing bug or feature ticket, please comment on the [issue](https://github.com/fallenbagel/jellyseerr/issues) to avoid multiple people working on the same thing.
|
||||||
@@ -93,14 +108,54 @@ When adding new UI text, please try to adhere to the following guidelines:
|
|||||||
8. If an additional description or "tip" is required for a form field, it should be styled using the global CSS class `label-tip`.
|
8. If an additional description or "tip" is required for a form field, it should be styled using the global CSS class `label-tip`.
|
||||||
9. In full sentences, abbreviations like "info" or "auto" should not be used in place of full words, unless referencing the name/label of a specific setting or option which has an abbreviation in its name.
|
9. In full sentences, abbreviations like "info" or "auto" should not be used in place of full words, unless referencing the name/label of a specific setting or option which has an abbreviation in its name.
|
||||||
10. Do your best to check for spelling errors and grammatical mistakes.
|
10. Do your best to check for spelling errors and grammatical mistakes.
|
||||||
11. Do not misspell "Overseerr."
|
11. Do not misspell "Jellyseerr."
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/) for our translations, and your help with localizing Overseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose).
|
We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-frontend/) for our translations, and your help with localizing Jellyseerr would be greatly appreciated! If your language is not listed below, please [open a feature request](https://github.com/fallenbagel/jellyseerr/issues/new/choose).
|
||||||
|
|
||||||
<a href="https://jellyseerr.borgcube.de/engage/jellysseerr/"><img src="https://jellyseerr.borgcube.de/widget/jellyseerr/multi-auto.svg" alt="Translation status" /></a>
|
<a href="https://jellyseerr.borgcube.de/engage/jellysseerr/"><img src="https://jellyseerr.borgcube.de/widget/jellyseerr/multi-auto.svg" alt="Translation status" /></a>
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
If you are adding a new feature that requires a database migration, you will need to create 2 migrations: one for SQLite and one for PostgreSQL. Here is how you could do it:
|
||||||
|
|
||||||
|
1. Create a PostgreSQL database or use an existing one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo docker run --name postgres-jellyseerr -e POSTGRES_PASSWORD=postgres -d -p 127.0.0.1:5432:5432/tcp postgres:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Reset the SQLite database and the PostgreSQL database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm config/db/db.*
|
||||||
|
rm config/settings.*
|
||||||
|
PGPASSWORD=postgres sudo docker exec -it postgres-jellyseerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "DROP DATABASE IF EXISTS jellyseerr;"
|
||||||
|
PGPASSWORD=postgres sudo docker exec -it postgres-jellyseerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE jellyseerr;"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Checkout the `develop` branch and create the original database for SQLite and PostgreSQL so that TypeORM can automatically generate the migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout develop
|
||||||
|
pnpm i
|
||||||
|
rm -r .next dist; pnpm build
|
||||||
|
pnpm start
|
||||||
|
DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
(You can shutdown the server once the message "Server ready on 5055" appears)
|
||||||
|
|
||||||
|
4. Let TypeORM generate the migrations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b your-feature-branch
|
||||||
|
pnpm i
|
||||||
|
pnpm migration:generate server/migration/sqlite/YourMigrationName
|
||||||
|
DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm migration:generate server/migration/postgres/YourMigrationName
|
||||||
|
```
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This contribution guide was inspired by the [Next.js](https://github.com/vercel/next.js) and [Radarr](https://github.com/Radarr/Radarr) contribution guides.
|
This contribution guide was inspired by the [Next.js](https://github.com/vercel/next.js), [Radarr](https://github.com/Radarr/Radarr), and [Overseerr](https://github.com/sct/Overseerr) contribution guides.
|
||||||
|
|||||||
23
Dockerfile
23
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine AS BUILD_IMAGE
|
FROM node:22-alpine AS BUILD_IMAGE
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ RUN \
|
|||||||
;; \
|
;; \
|
||||||
esac
|
esac
|
||||||
|
|
||||||
RUN npm install --global pnpm
|
RUN npm install --global pnpm@9
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml postinstall-win.js ./
|
COPY package.json pnpm-lock.yaml postinstall-win.js ./
|
||||||
RUN CYPRESS_INSTALL_BINARY=0 pnpm install --frozen-lockfile
|
RUN CYPRESS_INSTALL_BINARY=0 pnpm install --frozen-lockfile
|
||||||
@@ -29,23 +29,32 @@ RUN pnpm build
|
|||||||
# remove development dependencies
|
# remove development dependencies
|
||||||
RUN pnpm prune --prod --ignore-scripts
|
RUN pnpm prune --prod --ignore-scripts
|
||||||
|
|
||||||
RUN rm -rf src server .next/cache
|
RUN rm -rf src server .next/cache charts gen-docs docs
|
||||||
|
|
||||||
RUN touch config/DOCKER
|
RUN touch config/DOCKER
|
||||||
|
|
||||||
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
||||||
|
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
# Metadata for Github Package Registry
|
# OCI Meta information
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr"
|
ARG BUILD_DATE
|
||||||
|
ARG BUILD_VERSION
|
||||||
|
LABEL \
|
||||||
|
org.opencontainers.image.authors="Fallenbagel" \
|
||||||
|
org.opencontainers.image.source="https://github.com/fallenbagel/jellyseerr" \
|
||||||
|
org.opencontainers.image.created=${BUILD_DATE} \
|
||||||
|
org.opencontainers.image.version=${BUILD_VERSION} \
|
||||||
|
org.opencontainers.image.title="Jellyseerr" \
|
||||||
|
org.opencontainers.image.description="Open-source media request and discovery manager for Jellyfin, Plex, and Emby." \
|
||||||
|
org.opencontainers.image.licenses="MIT"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache tzdata tini && rm -rf /tmp/*
|
RUN apk add --no-cache tzdata tini && rm -rf /tmp/*
|
||||||
|
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm@9
|
||||||
|
|
||||||
# copy from build image
|
# copy from build image
|
||||||
COPY --from=BUILD_IMAGE /app ./
|
COPY --from=BUILD_IMAGE /app ./
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
Run npm install --global pnpm
|
RUN npm install --global pnpm@9
|
||||||
|
|
||||||
RUN pnpm install
|
RUN pnpm install
|
||||||
|
|
||||||
|
|||||||
133
README.md
133
README.md
@@ -8,20 +8,20 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/discord/952656177924300932" alt="Discord"></a>
|
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/discord/952656177924300932" 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="http://jellyseerr.borgcube.de/engage/jellyseerr/"><img src="http://jellyseerr.borgcube.de/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
<a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></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>
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
<!-- 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-40-orange.svg"/></a>
|
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-77-orange.svg"/></a>
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||||
It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
|
|
||||||
|
|
||||||
## Current Features
|
## Current Features
|
||||||
|
|
||||||
- Full Jellyfin/Emby/Plex integration including authentication with user import & management
|
- Full Jellyfin/Emby/Plex integration including authentication with user import & management.
|
||||||
- Supports Movies, Shows and Mixed Libraries
|
- Support for **PostgreSQL** and **SQLite** databases.
|
||||||
- Ability to change email addresses for smtp purposes
|
- Supports Movies, Shows and Mixed Libraries.
|
||||||
|
- Ability to change email addresses for SMTP purposes.
|
||||||
- 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!
|
||||||
- Jellyfin/Emby/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.
|
||||||
@@ -29,14 +29,14 @@ It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring sup
|
|||||||
- 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!
|
||||||
|
- Support for watchlisting & blacklisting media.
|
||||||
(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
|
||||||
|
|
||||||
Check out our documentation for instructions on how to install and run Jellyseerr:
|
Check out our documentation for instructions on how to install and run Jellyseerr:
|
||||||
|
|
||||||
https://docs.jellyseerr.dev/getting-started/
|
https://docs.jellyseerr.dev/getting-started/
|
||||||
|
|
||||||
### Packages:
|
### Packages:
|
||||||
@@ -86,56 +86,103 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
|||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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/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/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/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://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://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://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>
|
<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>
|
||||||
<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/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://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> <a href="https://github.com/fallenbagel/jellyseerr/commits?author=jab416171" title="Code">💻</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://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://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://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://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>
|
<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>
|
||||||
<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/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/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://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://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/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/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>
|
<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>
|
||||||
<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/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/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/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="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="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/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>
|
<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>
|
||||||
<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>
|
<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>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdll23"><img src="https://avatars.githubusercontent.com/u/142844478?v=4?s=100" width="100px;" alt="Michael Dallinger"/><br /><sub><b>Michael Dallinger</b></sub></a><br /><a href="#translation-mdll23" title="Translation">🌍</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdll23"><img src="https://avatars.githubusercontent.com/u/142844478?v=4?s=100" width="100px;" alt="Michael Dallinger"/><br /><sub><b>Michael Dallinger</b></sub></a><br /><a href="#translation-mdll23" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a> <a href="#maintenance-gauthier-th" title="Maintenance">🚧</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kara-Zor-El"><img src="https://avatars.githubusercontent.com/u/69772087?v=4?s=100" width="100px;" alt="Kara"/><br /><sub><b>Kara</b></sub></a><br /><a href="#infra-Kara-Zor-El" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kara-Zor-El"><img src="https://avatars.githubusercontent.com/u/69772087?v=4?s=100" width="100px;" alt="Kara"/><br /><sub><b>Kara</b></sub></a><br /><a href="#infra-Kara-Zor-El" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://mobihen.com"><img src="https://avatars.githubusercontent.com/u/35529491?v=4?s=100" width="100px;" alt="Nir Israel Hen"/><br /><sub><b>Nir Israel Hen</b></sub></a><br /><a href="#translation-mobihen" title="Translation">🌍</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XDark187"><img src="https://avatars.githubusercontent.com/u/39034192?v=4?s=100" width="100px;" alt="Baraa"/><br /><sub><b>Baraa</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=XDark187" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franciscofsales"><img src="https://avatars.githubusercontent.com/u/7977645?v=4?s=100" width="100px;" alt="Francisco Sales"/><br /><sub><b>Francisco Sales</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=franciscofsales" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myselfolli"><img src="https://avatars.githubusercontent.com/u/37535998?v=4?s=100" width="100px;" alt="Oliver Laing"/><br /><sub><b>Oliver Laing</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=myselfolli" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/M0NsTeRRR"><img src="https://avatars.githubusercontent.com/u/37785089?v=4?s=100" width="100px;" alt="Ludovic Ortega"/><br /><sub><b>Ludovic Ortega</b></sub></a><br /><a href="#security-M0NsTeRRR" title="Security">🛡️</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=demrich" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=maxnatamo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=DamsDev1" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zariel"><img src="https://avatars.githubusercontent.com/u/2213?v=4?s=100" width="100px;" alt="Chris Bannister"/><br /><sub><b>Chris Bannister</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Zariel" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/C4J3"><img src="https://avatars.githubusercontent.com/u/13005453?v=4?s=100" width="100px;" alt="Joe"/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=C4J3" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://me.garnx.fr"><img src="https://avatars.githubusercontent.com/u/37373941?v=4?s=100" width="100px;" alt="Guillaume ARNOUX"/><br /><sub><b>Guillaume ARNOUX</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=guillaumearnx" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dr-carrot"><img src="https://avatars.githubusercontent.com/u/17272571?v=4?s=100" width="100px;" alt="dr-carrot"/><br /><sub><b>dr-carrot</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=dr-carrot" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gageorsburn"><img src="https://avatars.githubusercontent.com/u/4692734?v=4?s=100" width="100px;" alt="Gage Orsburn"/><br /><sub><b>Gage Orsburn</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=gageorsburn" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GkhnGRBZ"><img src="https://avatars.githubusercontent.com/u/127258824?v=4?s=100" width="100px;" alt="GkhnGRBZ"/><br /><sub><b>GkhnGRBZ</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=GkhnGRBZ" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://benhaney.com"><img src="https://avatars.githubusercontent.com/u/31331498?v=4?s=100" width="100px;" alt="Ben Haney"/><br /><sub><b>Ben Haney</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=benhaney" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wunderharke"><img src="https://avatars.githubusercontent.com/u/5105672?v=4?s=100" width="100px;" alt="Wunderharke"/><br /><sub><b>Wunderharke</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Wunderharke" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/methbkts"><img src="https://avatars.githubusercontent.com/u/30674934?v=4?s=100" width="100px;" alt="Metin Bektas"/><br /><sub><b>Metin Bektas</b></sub></a><br /><a href="#infra-methbkts" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrewkolda"><img src="https://avatars.githubusercontent.com/u/158614532?v=4?s=100" width="100px;" alt="andrewkolda"/><br /><sub><b>andrewkolda</b></sub></a><br /><a href="#design-andrewkolda" title="Design">🎨</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://ishanjain.me"><img src="https://avatars.githubusercontent.com/u/7921368?v=4?s=100" width="100px;" alt="Ishan Jain"/><br /><sub><b>Ishan Jain</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=ishanjain28" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://michaelt.xyz"><img src="https://avatars.githubusercontent.com/u/18223295?v=4?s=100" width="100px;" alt="Michael Thomas"/><br /><sub><b>Michael Thomas</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=michaelhthomas" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=demrich" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=maxnatamo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=DamsDev1" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JackW6809"><img src="https://avatars.githubusercontent.com/u/53652452?v=4?s=100" width="100px;" alt="JackOXI"/><br /><sub><b>JackOXI</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=JackW6809" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RankWeis"><img src="https://avatars.githubusercontent.com/u/733691?v=4?s=100" width="100px;" alt="RankWeis"/><br /><sub><b>RankWeis</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=RankWeis" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -253,7 +300,7 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
|||||||
<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/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/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/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/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> <a href="https://github.com/sct/overseerr/commits?author=s0up4200" title="Code">💻</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/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>
|
<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>
|
||||||
@@ -280,6 +327,16 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=demrich" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=maxnatamo" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=DamsDev1" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JackW6809"><img src="https://avatars.githubusercontent.com/u/53652452?v=4?s=100" width="100px;" alt="JackOXI"/><br /><sub><b>JackOXI</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JackW6809" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lmiklosko"><img src="https://avatars.githubusercontent.com/u/44380311?v=4?s=100" width="100px;" alt="Lukas Miklosko"/><br /><sub><b>Lukas Miklosko</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=lmiklosko" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=gauthier-th" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
25
charts/jellyseerr-chart/.helmignore
Normal file
25
charts/jellyseerr-chart/.helmignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
|
# go template
|
||||||
|
*.gotmpl
|
||||||
13
charts/jellyseerr-chart/Chart.yaml
Normal file
13
charts/jellyseerr-chart/Chart.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
kubeVersion: ">=1.23.0-0"
|
||||||
|
name: jellyseerr-chart
|
||||||
|
description: Jellyseerr helm chart for Kubernetes
|
||||||
|
type: application
|
||||||
|
version: 2.3.2
|
||||||
|
appVersion: "2.5.1"
|
||||||
|
maintainers:
|
||||||
|
- name: Jellyseerr
|
||||||
|
url: https://github.com/Fallenbagel/jellyseerr
|
||||||
|
sources:
|
||||||
|
- https://github.com/Fallenbagel/jellyseerr/tree/main/charts/jellyseerr
|
||||||
|
home: https://github.com/Fallenbagel/jellyseerr
|
||||||
67
charts/jellyseerr-chart/README.md
Normal file
67
charts/jellyseerr-chart/README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# jellyseerr-chart
|
||||||
|
|
||||||
|
  
|
||||||
|
|
||||||
|
Jellyseerr helm chart for Kubernetes
|
||||||
|
|
||||||
|
**Homepage:** <https://github.com/Fallenbagel/jellyseerr>
|
||||||
|
|
||||||
|
## Maintainers
|
||||||
|
|
||||||
|
| Name | Email | Url |
|
||||||
|
| ---- | ------ | --- |
|
||||||
|
| Jellyseerr | | <https://github.com/Fallenbagel/jellyseerr> |
|
||||||
|
|
||||||
|
## Source Code
|
||||||
|
|
||||||
|
* <https://github.com/Fallenbagel/jellyseerr/tree/main/charts/jellyseerr>
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Kubernetes: `>=1.23.0-0`
|
||||||
|
|
||||||
|
## Values
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
|-----|------|---------|-------------|
|
||||||
|
| affinity | object | `{}` | |
|
||||||
|
| config | object | `{"persistence":{"accessModes":["ReadWriteOnce"],"annotations":{},"name":"","size":"5Gi","volumeName":""}}` | Creating PVC to store configuration |
|
||||||
|
| config.persistence.accessModes | list | `["ReadWriteOnce"]` | Access modes of persistent disk |
|
||||||
|
| config.persistence.annotations | object | `{}` | Annotations for PVCs |
|
||||||
|
| config.persistence.name | string | `""` | Config name |
|
||||||
|
| config.persistence.size | string | `"5Gi"` | Size of persistent disk |
|
||||||
|
| config.persistence.volumeName | string | `""` | Name of the permanent volume to reference in the claim. Can be used to bind to existing volumes. |
|
||||||
|
| extraEnv | list | `[]` | Environment variables to add to the jellyseerr pods |
|
||||||
|
| extraEnvFrom | list | `[]` | Environment variables from secrets or configmaps to add to the jellyseerr pods |
|
||||||
|
| fullnameOverride | string | `""` | |
|
||||||
|
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||||
|
| image.registry | string | `"ghcr.io"` | |
|
||||||
|
| image.repository | string | `"fallenbagel/jellyseerr"` | |
|
||||||
|
| image.sha | string | `""` | |
|
||||||
|
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
|
||||||
|
| imagePullSecrets | list | `[]` | |
|
||||||
|
| ingress.annotations | object | `{}` | |
|
||||||
|
| ingress.enabled | bool | `false` | |
|
||||||
|
| ingress.hosts[0].host | string | `"chart-example.local"` | |
|
||||||
|
| ingress.hosts[0].paths[0].path | string | `"/"` | |
|
||||||
|
| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
|
||||||
|
| ingress.ingressClassName | string | `""` | |
|
||||||
|
| ingress.tls | list | `[]` | |
|
||||||
|
| nameOverride | string | `""` | |
|
||||||
|
| nodeSelector | object | `{}` | |
|
||||||
|
| podAnnotations | object | `{}` | |
|
||||||
|
| podLabels | object | `{}` | |
|
||||||
|
| podSecurityContext | object | `{}` | |
|
||||||
|
| replicaCount | int | `1` | |
|
||||||
|
| resources | object | `{}` | |
|
||||||
|
| securityContext | object | `{}` | |
|
||||||
|
| service.port | int | `80` | |
|
||||||
|
| service.type | string | `"ClusterIP"` | |
|
||||||
|
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||||
|
| serviceAccount.automount | bool | `true` | Automatically mount a ServiceAccount's API credentials? |
|
||||||
|
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||||
|
| serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template |
|
||||||
|
| strategy | object | `{"type":"Recreate"}` | Deployment strategy |
|
||||||
|
| tolerations | list | `[]` | |
|
||||||
|
| volumeMounts | list | `[]` | Additional volumeMounts on the output Deployment definition. |
|
||||||
|
| volumes | list | `[]` | Additional volumes on the output Deployment definition. |
|
||||||
17
charts/jellyseerr-chart/README.md.gotmpl
Normal file
17
charts/jellyseerr-chart/README.md.gotmpl
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{{ template "chart.header" . }}
|
||||||
|
|
||||||
|
{{ template "chart.deprecationWarning" . }}
|
||||||
|
|
||||||
|
{{ template "chart.badgesSection" . }}
|
||||||
|
|
||||||
|
{{ template "chart.description" . }}
|
||||||
|
|
||||||
|
{{ template "chart.homepageLine" . }}
|
||||||
|
|
||||||
|
{{ template "chart.maintainersSection" . }}
|
||||||
|
|
||||||
|
{{ template "chart.sourcesSection" . }}
|
||||||
|
|
||||||
|
{{ template "chart.requirementsSection" . }}
|
||||||
|
|
||||||
|
{{ template "chart.valuesSection" . }}
|
||||||
1
charts/jellyseerr-chart/artifacthub-repo.yml
Normal file
1
charts/jellyseerr-chart/artifacthub-repo.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
repositoryID: c6b3f2dc-444c-4e37-b397-6a5ff563ee8b
|
||||||
5
charts/jellyseerr-chart/templates/NOTES.txt
Normal file
5
charts/jellyseerr-chart/templates/NOTES.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
***********************************************************************
|
||||||
|
Welcome to {{ .Chart.Name }}
|
||||||
|
Chart version: {{ .Chart.Version }}
|
||||||
|
App version: {{ .Chart.AppVersion }}
|
||||||
|
***********************************************************************
|
||||||
70
charts/jellyseerr-chart/templates/_helpers.tpl
Normal file
70
charts/jellyseerr-chart/templates/_helpers.tpl
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "jellyseerr.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "jellyseerr.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "jellyseerr.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "jellyseerr.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "jellyseerr.chart" . }}
|
||||||
|
{{ include "jellyseerr.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/part-of: {{ .Chart.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "jellyseerr.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "jellyseerr.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "jellyseerr.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "jellyseerr.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the pvc config to use
|
||||||
|
*/}}
|
||||||
|
{{- define "jellyseerr.configPersistenceName" -}}
|
||||||
|
{{- default (printf "%s-config" (include "jellyseerr.fullname" .)) .Values.config.persistence.name }}
|
||||||
|
{{- end }}
|
||||||
89
charts/jellyseerr-chart/templates/deployment.yaml
Normal file
89
charts/jellyseerr-chart/templates/deployment.yaml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "jellyseerr.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
strategy:
|
||||||
|
type: {{ .Values.strategy.type }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "jellyseerr.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "jellyseerr.labels" . | nindent 8 }}
|
||||||
|
{{- with .Values.podLabels }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "jellyseerr.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
|
{{- if .Values.image.sha }}
|
||||||
|
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}@sha256:{{ .Values.image.sha }}"
|
||||||
|
{{- else }}
|
||||||
|
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
{{- end }}
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 5055
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
{{- with .Values.extraEnv }}
|
||||||
|
env:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.extraEnvFrom }}
|
||||||
|
envFrom:
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /app/config
|
||||||
|
{{- with .Values.volumeMounts }}
|
||||||
|
{{- toYaml . | nindent 12 }}
|
||||||
|
{{- end }}
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "jellyseerr.configPersistenceName" . }}
|
||||||
|
{{- with .Values.volumes }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
41
charts/jellyseerr-chart/templates/ingress.yaml
Normal file
41
charts/jellyseerr-chart/templates/ingress.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "jellyseerr.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.ingressClassName }}
|
||||||
|
ingressClassName: {{ .Values.ingress.ingressClassName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "jellyseerr.fullname" $ }}
|
||||||
|
port:
|
||||||
|
number: {{ $.Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
24
charts/jellyseerr-chart/templates/persistentvolumeclaim.yaml
Normal file
24
charts/jellyseerr-chart/templates/persistentvolumeclaim.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "jellyseerr.configPersistenceName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.config.persistence.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.config.persistence.accessModes }}
|
||||||
|
accessModes:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.config.persistence.volumeName }}
|
||||||
|
volumeName: {{ .Values.config.persistence.volumeName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.config.persistence.storageClass }}
|
||||||
|
storageClassName: {{ if (eq "-" .) }}""{{ else }}{{ . }}{{ end }}
|
||||||
|
{{- end }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: "{{ .Values.config.persistence.size }}"
|
||||||
16
charts/jellyseerr-chart/templates/service.yaml
Normal file
16
charts/jellyseerr-chart/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "jellyseerr.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "jellyseerr.selectorLabels" . | nindent 4 }}
|
||||||
|
ipFamilyPolicy: PreferDualStack
|
||||||
13
charts/jellyseerr-chart/templates/serviceaccount.yaml
Normal file
13
charts/jellyseerr-chart/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "jellyseerr.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||||
|
{{- end }}
|
||||||
15
charts/jellyseerr-chart/templates/tests/test-connection.yaml
Normal file
15
charts/jellyseerr-chart/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: "{{ include "jellyseerr.fullname" . }}-test-connection"
|
||||||
|
labels:
|
||||||
|
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": test
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: wget
|
||||||
|
image: busybox
|
||||||
|
command: ['wget']
|
||||||
|
args: ['{{ include "jellyseerr.fullname" . }}:{{ .Values.service.port }}']
|
||||||
|
restartPolicy: Never
|
||||||
114
charts/jellyseerr-chart/values.yaml
Normal file
114
charts/jellyseerr-chart/values.yaml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
registry: ghcr.io
|
||||||
|
repository: fallenbagel/jellyseerr
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
# -- Overrides the image tag whose default is the chart appVersion.
|
||||||
|
tag: ""
|
||||||
|
sha: ""
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
# -- Deployment strategy
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
|
||||||
|
# -- Environment variables to add to the jellyseerr pods
|
||||||
|
extraEnv: []
|
||||||
|
# -- Environment variables from secrets or configmaps to add to the jellyseerr pods
|
||||||
|
extraEnvFrom: []
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
# -- Specifies whether a service account should be created
|
||||||
|
create: true
|
||||||
|
# -- Automatically mount a ServiceAccount's API credentials?
|
||||||
|
automount: true
|
||||||
|
# -- Annotations to add to the service account
|
||||||
|
annotations: {}
|
||||||
|
# -- The name of the service account to use.
|
||||||
|
# -- If not set and create is true, a name is generated using the fullname template
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
podLabels: {}
|
||||||
|
|
||||||
|
podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
|
||||||
|
securityContext: {}
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
# runAsNonRoot: true
|
||||||
|
# runAsUser: 1000
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
# -- Creating PVC to store configuration
|
||||||
|
config:
|
||||||
|
persistence:
|
||||||
|
# -- Size of persistent disk
|
||||||
|
size: 5Gi
|
||||||
|
# -- Annotations for PVCs
|
||||||
|
annotations: {}
|
||||||
|
# -- Access modes of persistent disk
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
# -- Config name
|
||||||
|
name: ""
|
||||||
|
# -- Name of the permanent volume to reference in the claim.
|
||||||
|
# Can be used to bind to existing volumes.
|
||||||
|
volumeName: ""
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
ingressClassName: ""
|
||||||
|
annotations: {}
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
hosts:
|
||||||
|
- host: chart-example.local
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - chart-example.local
|
||||||
|
|
||||||
|
resources: {}
|
||||||
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
|
# choice for the user. This also increases chances charts run on environments with little
|
||||||
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||||
|
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||||
|
# limits:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
# requests:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
|
||||||
|
# -- Additional volumes on the output Deployment definition.
|
||||||
|
volumes: []
|
||||||
|
# - name: foo
|
||||||
|
# secret:
|
||||||
|
# secretName: mysecret
|
||||||
|
# optional: false
|
||||||
|
|
||||||
|
# -- Additional volumeMounts on the output Deployment definition.
|
||||||
|
volumeMounts: []
|
||||||
|
# - name: foo
|
||||||
|
# mountPath: "/etc/foo"
|
||||||
|
# readOnly: true
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
jellyseerr:
|
jellyseerr:
|
||||||
build:
|
build:
|
||||||
@@ -4,6 +4,7 @@ export default defineConfig({
|
|||||||
projectId: 'xkm1b4',
|
projectId: 'xkm1b4',
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: 'http://localhost:5055',
|
baseUrl: 'http://localhost:5055',
|
||||||
|
video: true,
|
||||||
experimentalSessionAndOrigin: true,
|
experimentalSessionAndOrigin: true,
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"vapidPublic": "BK_EpP8NDm9waor2zn6_S28o3ZYv4kCkJOfYpO3pt3W6jnPmxrgTLANUBNbbyaNatPnSQ12De9CeqSYQrqWzHTs",
|
"vapidPublic": "BK_EpP8NDm9waor2zn6_S28o3ZYv4kCkJOfYpO3pt3W6jnPmxrgTLANUBNbbyaNatPnSQ12De9CeqSYQrqWzHTs",
|
||||||
"main": {
|
"main": {
|
||||||
"apiKey": "testkey",
|
"apiKey": "testkey",
|
||||||
"applicationTitle": "Overseerr",
|
"applicationTitle": "Jellyseerr",
|
||||||
"applicationUrl": "",
|
"applicationUrl": "",
|
||||||
"csrfProtection": false,
|
"csrfProtection": false,
|
||||||
"cacheImages": false,
|
"cacheImages": false,
|
||||||
@@ -16,11 +16,14 @@
|
|||||||
"hideAvailable": false,
|
"hideAvailable": false,
|
||||||
"localLogin": true,
|
"localLogin": true,
|
||||||
"newPlexLogin": true,
|
"newPlexLogin": true,
|
||||||
"region": "",
|
"discoverRegion": "",
|
||||||
|
"streamingRegion": "",
|
||||||
"originalLanguage": "",
|
"originalLanguage": "",
|
||||||
"trustProxy": false,
|
"trustProxy": false,
|
||||||
"mediaServerType": 1,
|
"mediaServerType": 1,
|
||||||
"partialRequestsEnabled": true,
|
"partialRequestsEnabled": true,
|
||||||
|
"enableSpecialEpisodes": false,
|
||||||
|
"forceIpv4First": false,
|
||||||
"locale": "en"
|
"locale": "en"
|
||||||
},
|
},
|
||||||
"plex": {
|
"plex": {
|
||||||
@@ -67,7 +70,7 @@
|
|||||||
"ignoreTls": false,
|
"ignoreTls": false,
|
||||||
"requireTls": false,
|
"requireTls": false,
|
||||||
"allowSelfSigned": false,
|
"allowSelfSigned": false,
|
||||||
"senderName": "Overseerr"
|
"senderName": "Jellyseerr"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
@@ -75,6 +78,7 @@
|
|||||||
"types": 0,
|
"types": 0,
|
||||||
"options": {
|
"options": {
|
||||||
"webhookUrl": "",
|
"webhookUrl": "",
|
||||||
|
"webhookRoleId": "",
|
||||||
"enableMentions": true
|
"enableMentions": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -98,6 +102,7 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"botAPI": "",
|
"botAPI": "",
|
||||||
"chatId": "",
|
"chatId": "",
|
||||||
|
"messageThreadId": "",
|
||||||
"sendSilently": false
|
"sendSilently": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ describe('General Settings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('modifies setting that requires restart', () => {
|
it('modifies setting that requires restart', () => {
|
||||||
cy.visit('/settings');
|
cy.visit('/settings/network');
|
||||||
|
|
||||||
cy.get('#trustProxy').click();
|
cy.get('#trustProxy').click();
|
||||||
cy.get('[data-testid=settings-main-form]').submit();
|
cy.get('[data-testid=settings-network-form]').submit();
|
||||||
cy.get('[data-testid=modal-title]').should(
|
cy.get('[data-testid=modal-title]').should(
|
||||||
'contain',
|
'contain',
|
||||||
'Server Restart Required'
|
'Server Restart Required'
|
||||||
@@ -26,7 +26,7 @@ describe('General Settings', () => {
|
|||||||
cy.get('[data-testid=modal-title]').should('not.exist');
|
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||||
|
|
||||||
cy.get('[type=checkbox]#trustProxy').click();
|
cy.get('[type=checkbox]#trustProxy').click();
|
||||||
cy.get('[data-testid=settings-main-form]').submit();
|
cy.get('[data-testid=settings-network-form]').submit();
|
||||||
cy.get('[data-testid=modal-title]').should('not.exist');
|
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const testUser = {
|
const testUser = {
|
||||||
displayName: 'Test User',
|
username: 'Test User',
|
||||||
emailAddress: 'test@seeerr.dev',
|
emailAddress: 'test@seeerr.dev',
|
||||||
password: 'test1234',
|
password: 'test1234',
|
||||||
};
|
};
|
||||||
@@ -32,7 +32,7 @@ describe('User List', () => {
|
|||||||
|
|
||||||
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
cy.get('[data-testid=modal-title]').should('contain', 'Create Local User');
|
||||||
|
|
||||||
cy.get('#displayName').type(testUser.displayName);
|
cy.get('#username').type(testUser.username);
|
||||||
cy.get('#email').type(testUser.emailAddress);
|
cy.get('#email').type(testUser.emailAddress);
|
||||||
cy.get('#password').type(testUser.password);
|
cy.get('#password').type(testUser.password);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Cypress.Commands.add('login', (email, password) => {
|
|||||||
[email, password],
|
[email, password],
|
||||||
() => {
|
() => {
|
||||||
cy.visit('/login');
|
cy.visit('/login');
|
||||||
cy.contains('Use your Overseerr account').click();
|
|
||||||
|
|
||||||
cy.get('[data-testid=email]').type(email);
|
cy.get('[data-testid=email]').type(email);
|
||||||
cy.get('[data-testid=password]').type(password);
|
cy.get('[data-testid=password]').type(password);
|
||||||
|
|||||||
38
docker-compose.postgres.yaml
Normal file
38
docker-compose.postgres.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
jellyseerr:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.local
|
||||||
|
ports:
|
||||||
|
- '5055:5055'
|
||||||
|
environment:
|
||||||
|
DB_TYPE: 'postgres' # Which DB engine to use. The default is "sqlite". To use postgres, this needs to be set to "postgres"
|
||||||
|
DB_HOST: 'postgres' # The host (url) of the database
|
||||||
|
DB_PORT: '5432' # The port to connect to
|
||||||
|
DB_USER: 'jellyseerr' # Username used to connect to the database
|
||||||
|
DB_PASS: 'jellyseerr' # Password of the user used to connect to the database
|
||||||
|
DB_NAME: 'jellyseerr' # The name of the database to connect to
|
||||||
|
DB_LOG_QUERIES: 'false' # Whether to log the DB queries for debugging
|
||||||
|
DB_USE_SSL: 'false' # Whether to enable ssl for database connection
|
||||||
|
volumes:
|
||||||
|
- .:/app:rw,cached
|
||||||
|
- /app/node_modules
|
||||||
|
- /app/.next
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
links:
|
||||||
|
- postgres
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: jellyseerr
|
||||||
|
POSTGRES_PASSWORD: jellyseerr
|
||||||
|
POSTGRES_DB: jellyseerr
|
||||||
|
ports:
|
||||||
|
- '5432:5432'
|
||||||
|
volumes:
|
||||||
|
- postgres:/var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
postgres:
|
||||||
@@ -7,30 +7,34 @@ sidebar_position: 1
|
|||||||
|
|
||||||
Welcome to the Jellyseerr Documentation.
|
Welcome to the Jellyseerr Documentation.
|
||||||
|
|
||||||
|
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Full Jellyfin/Emby/Plex integration**. Login and manage user access with Jellyfin/Emby/Plex.
|
- **Full Jellyfin/Emby/Plex integration**. Login and manage user access with Jellyfin/Emby/Plex.
|
||||||
- **Syncs to your Jellyfin/Emby/Plex library** to show what titles you already have.
|
- **Syncs to your Jellyfin/Emby/Plex library** to show what titles you already have.
|
||||||
|
- Supports Movies, Shows and Mixed Libraries.
|
||||||
- **Integrates with Sonarr and Radarr**. With more services to come in the future.
|
- **Integrates with Sonarr and Radarr**. With more services to come in the future.
|
||||||
|
- Optionally set **Override rules** for requests to match with your defined conditions.
|
||||||
- **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI.
|
- **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI.
|
||||||
- **Simple request management UI**. Don't dig through the app to approve recent requests.
|
- **Simple request management UI**. Don't dig through the app to approve recent requests.
|
||||||
- **Mobile-friendly design**, for when you need to approve requests on the go.
|
- **Mobile-friendly design**, for when you need to approve requests on the go.
|
||||||
- Granular permission system.
|
- Granular permission system.
|
||||||
- Localization into other languages.
|
- Localization into other languages.
|
||||||
|
- Support for **PostgreSQL** and **SQLite** databases.
|
||||||
|
- Support for various notification agents.
|
||||||
|
- Easily **Watchlist** or **Blacklist** media.
|
||||||
- More features to come!
|
- More features to come!
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
The primary motivation for starting this project was to add support for Jellyfin and Emby to Overseerr. As Overseerr is an incredibly performant and easy-to-use application, we wanted to bring that same experience to Jellyfin and Emby users. Thus, **Jellyseerr** was born.
|
The primary motivation for starting Jellyseerr was to bring Jellyfin and Emby support to Overseerr. However, over time, **Jellyseerr** has evolved into its own distinct application with unique features. Designed as a one-stop shop for media requests, it offers a simple, easy-to-use experience for managing requests on Jellyfin, Emby, and Plex servers.
|
||||||
|
|
||||||
This application is designed to be a **one-stop-shop** for all your media requests. It is designed to be a **simple, easy-to-use** application that allows users to request media to be added to your Jellyfin/Emby/Plex server.
|
|
||||||
|
|
||||||
## We need your help!
|
## We need your help!
|
||||||
|
|
||||||
[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr, with a heavy focus on Jellyfin and Emby integration.
|
[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is an ambitious project where developers/contributors poured a lot of work into, and that builds on top of [Overseerr](https://github.com/sct/overseerr). And we have a lot more to do as well.
|
||||||
[Overseerr](https://github.com/sct/overseerr) is an ambitious project where the original developers/contributors have already poured a lot of work into, and we wanted to build on top of that.
|
|
||||||
|
|
||||||
We also have poured a lot of work into this project, and we have a lot more to do as well. We need your valuable feedback and help to find and fix bugs. Also, with Jellyseerr being an open-source project, anyone is welcome to contribute. We also encourage you to contribute to Overseerr as well.
|
We value your feedback and support in identifying and fixing bugs to make Jellyseerr even better. As an open-source project, we welcome contributions from everyone. While Jellyseerr has diverged from Overseerr and evolved into its own unique application, we still encourage contributions to Overseerr, as it played a crucial role in inspiring what Jellyseerr has become today.
|
||||||
|
|
||||||
Contribution includes building new features, patching bugs, translating the application, or even just writing documentation.
|
Contribution includes building new features, patching bugs, translating the application, or even just writing documentation.
|
||||||
|
|
||||||
|
|||||||
82
docs/extending-jellyseerr/database-config.mdx
Normal file
82
docs/extending-jellyseerr/database-config.mdx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: Configuring the Database (Advanced)
|
||||||
|
description: Configure the database for Jellyseerr
|
||||||
|
sidebar_position: 2
|
||||||
|
---
|
||||||
|
# Configuring the Database
|
||||||
|
|
||||||
|
Jellyseerr supports SQLite and PostgreSQL. The database connection can be configured using the following environment variables:
|
||||||
|
|
||||||
|
## SQLite Options
|
||||||
|
|
||||||
|
If you want to use SQLite, you can simply set the `DB_TYPE` environment variable to `sqlite`. This is the default configuration so even if you don't set any other options, SQLite will be used.
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
DB_TYPE="sqlite" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||||
|
CONFIG_DIRECTORY="config" # (optional) The path to the config directory where the db file is stored. The default is "config".
|
||||||
|
DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false".
|
||||||
|
```
|
||||||
|
|
||||||
|
## PostgreSQL Options
|
||||||
|
|
||||||
|
### TCP Connection
|
||||||
|
|
||||||
|
If your PostgreSQL server is configured to accept TCP connections, you can specify the host and port using the `DB_HOST` and `DB_PORT` environment variables. This is useful for remote connections where the server uses a network host and port.
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||||
|
DB_HOST="localhost" # (optional) The host (URL) of the database. The default is "localhost".
|
||||||
|
DB_PORT="5432" # (optional) The port to connect to. The default is "5432".
|
||||||
|
DB_USER= # (required) Username used to connect to the database.
|
||||||
|
DB_PASS= # (required) Password of the user used to connect to the database.
|
||||||
|
DB_NAME="jellyseerr" # (optional) The name of the database to connect to. The default is "jellyseerr".
|
||||||
|
DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false".
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unix Socket Connection
|
||||||
|
|
||||||
|
If your PostgreSQL server is configured to accept Unix socket connections, you can specify the path to the socket directory using the `DB_SOCKET_PATH` environment variable. This is useful for local connections where the server uses a Unix socket.
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||||
|
DB_SOCKET_PATH="/var/run/postgresql" # (required) The path to the PostgreSQL Unix socket directory.
|
||||||
|
DB_USER= # (required) Username used to connect to the database.
|
||||||
|
DB_PASS= # (optional) Password of the user used to connect to the database, depending on the server's authentication configuration.
|
||||||
|
DB_NAME="jellyseerr" # (optional) The name of the database to connect to. The default is "jellyseerr".
|
||||||
|
DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false".
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL configuration
|
||||||
|
|
||||||
|
The following options can be used to further configure ssl. Certificates can be provided as a string or a file path, with the string version taking precedence.
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
DB_USE_SSL="false" # (optional) Whether to enable ssl for database connection. This must be "true" to use the other ssl options. The default is "false".
|
||||||
|
DB_SSL_REJECT_UNAUTHORIZED="true" # (optional) Whether to reject ssl connections with unverifiable certificates i.e. self-signed certificates without providing the below settings. The default is "true".
|
||||||
|
DB_SSL_CA= # (optional) The CA certificate to verify the connection, provided as a string. The default is "".
|
||||||
|
DB_SSL_CA_FILE= # (optional) The path to a CA certificate to verify the connection. The default is "".
|
||||||
|
DB_SSL_KEY= # (optional) The private key for the connection in PEM format, provided as a string. The default is "".
|
||||||
|
DB_SSL_KEY_FILE= # (optinal) Path to the private key for the connection in PEM format. The default is "".
|
||||||
|
DB_SSL_CERT= # (optional) Certificate chain in pem format for the private key, provided as a string. The default is "".
|
||||||
|
DB_SSL_CERT_FILE= # (optional) Path to certificate chain in pem format for the private key. The default is "".
|
||||||
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
### Migrating from SQLite to PostgreSQL
|
||||||
|
|
||||||
|
1. Set up your PostgreSQL database and configure Jellyseerr to use it
|
||||||
|
2. Run Jellyseerr to create the tables in the PostgreSQL database
|
||||||
|
3. Stop Jellyseerr
|
||||||
|
4. Run the following command to export the data from the SQLite database and import it into the PostgreSQL database:
|
||||||
|
:::info
|
||||||
|
Edit the postgres connection string to match your setup.
|
||||||
|
|
||||||
|
If you don't have or don't want to use docker, you can build the working pgloader version [in this PR](https://github.com/dimitri/pgloader/pull/1531) from source and use the same options as below.
|
||||||
|
:::
|
||||||
|
:::caution
|
||||||
|
The most recent release of pgloader has an issue quoting the table columns. Use the version in the docker container to avoid this issue.
|
||||||
|
:::
|
||||||
|
```bash
|
||||||
|
docker run --rm -v config/db.sqlite3:/db.sqlite3:ro ghcr.io/ralgar/pgloader:pr-1531 pgloader --with "quote identifiers" --with "data only" /db.sqlite3 postgresql://{{DB_USER}}:{{DB_PASS}}@{{DB_HOST}}:{{DB_PORT}}/{{DB_NAME}}
|
||||||
|
```
|
||||||
|
5. Start Jellyseerr
|
||||||
@@ -17,7 +17,7 @@ A Nginx subfolder workaround configuration is provided below, but it is not offi
|
|||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
<Tabs groupId="nginx-reverse-proxy">
|
<Tabs groupId="nginx-reverse-proxy" queryString>
|
||||||
<TabItem value="subdomain" label="Subdomain">
|
<TabItem value="subdomain" label="Subdomain">
|
||||||
Add the following configuration to a new file `/etc/nginx/sites-available/jellyseerr.example.com.conf`:
|
Add the following configuration to a new file `/etc/nginx/sites-available/jellyseerr.example.com.conf`:
|
||||||
|
|
||||||
@@ -63,9 +63,9 @@ sudo ln -s /etc/nginx/sites-available/jellyseerr.example.com.conf /etc/nginx/sit
|
|||||||
<TabItem value="subfolder" label="Subfolder">
|
<TabItem value="subfolder" label="Subfolder">
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
This Nginx subfolder reverse proxy is an unsupported workaround, and only provided as an example. The filters may stop working when Overseerr is updated.
|
This Nginx subfolder reverse proxy is an unsupported workaround, and only provided as an example. The filters may stop working when Jellyseerr is updated.
|
||||||
|
|
||||||
If you encounter any issues with Overseerr while using this workaround, we may ask you to try to reproduce the problem without the Nginx proxy.
|
If you encounter any issues with Jellyseerr while using this workaround, we may ask you to try to reproduce the problem without the Nginx proxy.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Add the following location block to your existing `nginx.conf` file.
|
Add the following location block to your existing `nginx.conf` file.
|
||||||
@@ -95,6 +95,8 @@ location ^~ /jellyseerr {
|
|||||||
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';
|
||||||
sub_filter '/images/' '/$app/images/';
|
sub_filter '/images/' '/$app/images/';
|
||||||
|
sub_filter '/imageproxy/' '/$app/imageproxy/';
|
||||||
|
sub_filter '/avatarproxy/' '/$app/avatarproxy/';
|
||||||
sub_filter '/android-' '/$app/android-';
|
sub_filter '/android-' '/$app/android-';
|
||||||
sub_filter '/apple-' '/$app/apple-';
|
sub_filter '/apple-' '/$app/apple-';
|
||||||
sub_filter '/favicon' '/$app/favicon';
|
sub_filter '/favicon' '/$app/favicon';
|
||||||
@@ -146,9 +148,9 @@ Add a new proxy host with the following settings:
|
|||||||
|
|
||||||
### Details
|
### Details
|
||||||
|
|
||||||
- **Domain Names:** Your desired external Overseerr hostname; e.g., `overseerr.example.com`
|
- **Domain Names:** Your desired external Jellyseerr hostname; e.g., `jellyseerr.example.com`
|
||||||
- **Scheme:** `http`
|
- **Scheme:** `http`
|
||||||
- **Forward Hostname / IP:** Internal Overseerr hostname or IP
|
- **Forward Hostname / IP:** Internal Jellyseerr hostname or IP
|
||||||
- **Forward Port:** `5055`
|
- **Forward Port:** `5055`
|
||||||
- **Cache Assets:** yes
|
- **Cache Assets:** yes
|
||||||
- **Block Common Exploits:** yes
|
- **Block Common Exploits:** yes
|
||||||
@@ -190,18 +192,18 @@ Caddy will automatically obtain and renew SSL certificates for your domain.
|
|||||||
|
|
||||||
## Traefik (v2)
|
## Traefik (v2)
|
||||||
|
|
||||||
Add the following labels to the Overseerr service in your `docker-compose.yml` file:
|
Add the following labels to the Jellyseerr service in your `compose.yaml` file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
labels:
|
labels:
|
||||||
- 'traefik.enable=true'
|
- 'traefik.enable=true'
|
||||||
## HTTP Routers
|
## HTTP Routers
|
||||||
- 'traefik.http.routers.overseerr-rtr.entrypoints=https'
|
- 'traefik.http.routers.jellyseerr-rtr.entrypoints=https'
|
||||||
- 'traefik.http.routers.overseerr-rtr.rule=Host(`overseerr.domain.com`)'
|
- 'traefik.http.routers.jellyseerr-rtr.rule=Host(`jellyseerr.domain.com`)'
|
||||||
- 'traefik.http.routers.overseerr-rtr.tls=true'
|
- 'traefik.http.routers.jellyseerr-rtr.tls=true'
|
||||||
## HTTP Services
|
## HTTP Services
|
||||||
- 'traefik.http.routers.overseerr-rtr.service=overseerr-svc'
|
- 'traefik.http.routers.jellyseerr-rtr.service=jellyseerr-svc'
|
||||||
- 'traefik.http.services.overseerr-svc.loadbalancer.server.port=5055'
|
- 'traefik.http.services.jellyseerr-svc.loadbalancer.server.port=5055'
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information, please refer to the [Traefik documentation](https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/).
|
For more information, please refer to the [Traefik documentation](https://doc.traefik.io/traefik/user-guides/docker-compose/basic-example/).
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ sidebar_position: 4
|
|||||||
|
|
||||||
# AUR (Arch User Repository)
|
# AUR (Arch User Repository)
|
||||||
|
|
||||||
|
:::note Disclaimer
|
||||||
|
This AUR package is not maintained by us but by a third party. Please refer to the maintainer for any issues.
|
||||||
|
:::
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
|
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
|
||||||
:::
|
:::
|
||||||
@@ -17,7 +21,7 @@ To install Jellyseerr from the AUR, you can use an AUR helper like `yay` or `par
|
|||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
<Tabs groupId="aur-methods">
|
<Tabs groupId="aur-methods" queryString>
|
||||||
<TabItem value="yay" label="yay">
|
<TabItem value="yay" label="yay">
|
||||||
```bash
|
```bash
|
||||||
yay -S jellyseerr
|
yay -S jellyseerr
|
||||||
|
|||||||
@@ -6,55 +6,20 @@ sidebar_position: 2
|
|||||||
# Build from Source (Advanced)
|
# Build from Source (Advanced)
|
||||||
:::warning
|
:::warning
|
||||||
This method is not recommended for most users. It is intended for advanced users who are familiar with managing their own server infrastructure.
|
This method is not recommended for most users. It is intended for advanced users who are familiar with managing their own server infrastructure.
|
||||||
|
|
||||||
|
Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresql-options) for details on how to configure your database.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
<Tabs groupId="versions">
|
- [Node.js 22.x](https://nodejs.org/en/download/)
|
||||||
<TabItem value="latest" label="Latest">
|
|
||||||
- [Node.js 18.x](https://nodejs.org/en/download/)
|
|
||||||
- [Yarn 1.x](https://classic.yarnpkg.com/lang/en/docs/install)
|
|
||||||
- [Git](https://git-scm.com/downloads)
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="develop" label="Develop">
|
|
||||||
- [Node.js 20.x](https://nodejs.org/en/download/)
|
|
||||||
- [Pnpm 9.x](https://pnpm.io/installation)
|
- [Pnpm 9.x](https://pnpm.io/installation)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
|
|
||||||
## Unix (Linux, macOS)
|
## Unix (Linux, macOS)
|
||||||
### Installation
|
### Installation
|
||||||
<Tabs groupId="versions">
|
|
||||||
<TabItem value="latest" label="latest">
|
|
||||||
1. Assuming you want the working directory to be `/opt/jellyseerr`, create the directory and navigate to it:
|
|
||||||
```bash
|
|
||||||
sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
|
|
||||||
```
|
|
||||||
2. Clone the Jellyseerr repository and checkout the latest release:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/Fallenbagel/jellyseerr.git
|
|
||||||
cd jellyseerr
|
|
||||||
git checkout main
|
|
||||||
```
|
|
||||||
3. Install the dependencies:
|
|
||||||
```bash
|
|
||||||
CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile --network-timeout 1000000
|
|
||||||
```
|
|
||||||
4. Build the project:
|
|
||||||
```bash
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
5. Start Jellyseerr:
|
|
||||||
```bash
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="develop" label="develop">
|
|
||||||
1. Assuming you want the working directory to be `/opt/jellyseerr`, create the directory and navigate to it:
|
1. Assuming you want the working directory to be `/opt/jellyseerr`, create the directory and navigate to it:
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
|
sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
|
||||||
@@ -63,7 +28,7 @@ sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
|
|||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Fallenbagel/jellyseerr.git
|
git clone https://github.com/Fallenbagel/jellyseerr.git
|
||||||
cd jellyseerr
|
cd jellyseerr
|
||||||
git checkout develop # by default, you are on the develop branch so this step is not necessary
|
git checkout main
|
||||||
```
|
```
|
||||||
3. Install the dependencies:
|
3. Install the dependencies:
|
||||||
```bash
|
```bash
|
||||||
@@ -77,15 +42,13 @@ pnpm build
|
|||||||
```bash
|
```bash
|
||||||
pnpm start
|
pnpm start
|
||||||
```
|
```
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
#### Extending the installation
|
#### Extending the installation
|
||||||
<Tabs groupId="unix-extensions">
|
<Tabs groupId="unix-extensions" queryString>
|
||||||
<TabItem value="linux" label="Linux">
|
<TabItem value="linux" label="Linux">
|
||||||
To run jellyseerr as a systemd service:
|
To run jellyseerr as a systemd service:
|
||||||
1. create the environment file at `/etc/jellyseerr/jellyseerr.conf`:
|
1. create the environment file at `/etc/jellyseerr/jellyseerr.conf`:
|
||||||
@@ -97,8 +60,8 @@ PORT=5055
|
|||||||
## specify on which interface to listen, by default jellyseerr listens on all interfaces
|
## specify on which interface to listen, by default jellyseerr listens on all interfaces
|
||||||
#HOST=127.0.0.1
|
#HOST=127.0.0.1
|
||||||
|
|
||||||
## Uncomment if your media server is emby instead of jellyfin.
|
## Uncomment if you want to force Node.js to resolve IPv4 before IPv6 (advanced users only)
|
||||||
# JELLYFIN_TYPE=emby
|
# FORCE_IPV4_FIRST=true
|
||||||
```
|
```
|
||||||
2. Then run the following commands:
|
2. Then run the following commands:
|
||||||
```bash
|
```bash
|
||||||
@@ -231,32 +194,6 @@ pm2 status jellyseerr
|
|||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
### Installation
|
### Installation
|
||||||
<Tabs groupId="versions">
|
|
||||||
<TabItem value="latest" label="latest">
|
|
||||||
1. Assuming you want the working directory to be `C:\jellyseerr`, create the directory and navigate to it:
|
|
||||||
```powershell
|
|
||||||
mkdir C:\jellyseerr
|
|
||||||
cd C:\jellyseerr
|
|
||||||
```
|
|
||||||
2. Clone the Jellyseerr repository and checkout the latest release:
|
|
||||||
```powershell
|
|
||||||
git clone https://github.com/Fallenbagel/jellyseerr.git .
|
|
||||||
git checkout main
|
|
||||||
```
|
|
||||||
3. Install the dependencies:
|
|
||||||
```powershell
|
|
||||||
set CYPRESS_INSTALL_BINARY=0 && yarn install --frozen-lockfile --network-timeout 1000000
|
|
||||||
```
|
|
||||||
4. Build the project:
|
|
||||||
```powershell
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
5. Start Jellyseerr:
|
|
||||||
```powershell
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="develop" label="develop">
|
|
||||||
1. Assuming you want the working directory to be `C:\jellyseerr`, create the directory and navigate to it:
|
1. Assuming you want the working directory to be `C:\jellyseerr`, create the directory and navigate to it:
|
||||||
```powershell
|
```powershell
|
||||||
mkdir C:\jellyseerr
|
mkdir C:\jellyseerr
|
||||||
@@ -265,10 +202,11 @@ cd C:\jellyseerr
|
|||||||
2. Clone the Jellyseerr repository and checkout the develop branch:
|
2. Clone the Jellyseerr repository and checkout the develop branch:
|
||||||
```powershell
|
```powershell
|
||||||
git clone https://github.com/Fallenbagel/jellyseerr.git .
|
git clone https://github.com/Fallenbagel/jellyseerr.git .
|
||||||
git checkout develop # by default, you are on the develop branch so this step is not necessary
|
git checkout main
|
||||||
```
|
```
|
||||||
3. Install the dependencies:
|
3. Install the dependencies:
|
||||||
```powershell
|
```powershell
|
||||||
|
npm install -g win-node-env
|
||||||
set CYPRESS_INSTALL_BINARY=0 && pnpm install --frozen-lockfile
|
set CYPRESS_INSTALL_BINARY=0 && pnpm install --frozen-lockfile
|
||||||
```
|
```
|
||||||
4. Build the project:
|
4. Build the project:
|
||||||
@@ -279,8 +217,6 @@ pnpm build
|
|||||||
```powershell
|
```powershell
|
||||||
pnpm start
|
pnpm start
|
||||||
```
|
```
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
You can add the environment variables to a `.env` file in the Jellyseerr directory.
|
You can add the environment variables to a `.env` file in the Jellyseerr directory.
|
||||||
@@ -291,7 +227,7 @@ You can now access Jellyseerr by visiting `http://localhost:5055` in your web br
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
#### Extending the installation
|
#### Extending the installation
|
||||||
<Tabs groupId="windows-extensions">
|
<Tabs groupId="windows-extensions" queryString>
|
||||||
<TabItem value="task-scheduler" label="Task Scheduler">
|
<TabItem value="task-scheduler" label="Task Scheduler">
|
||||||
To run jellyseerr as a bat script:
|
To run jellyseerr as a bat script:
|
||||||
1. Create a file named `start-jellyseerr.bat` in the jellyseerr directory:
|
1. Create a file named `start-jellyseerr.bat` in the jellyseerr directory:
|
||||||
@@ -308,6 +244,7 @@ node dist/index.js
|
|||||||
- Set the trigger to "When the computer starts"
|
- Set the trigger to "When the computer starts"
|
||||||
- Set the action to "Start a program"
|
- Set the action to "Start a program"
|
||||||
- Set the program/script to the path of the `start-jellyseerr.bat` file
|
- Set the program/script to the path of the `start-jellyseerr.bat` file
|
||||||
|
- Set the "Start in" to the jellyseerr directory.
|
||||||
- Click "Finish"
|
- Click "Finish"
|
||||||
|
|
||||||
Now, Jellyseerr will start when the computer boots up in the background.
|
Now, Jellyseerr will start when the computer boots up in the background.
|
||||||
@@ -318,7 +255,8 @@ To run jellyseerr as a service:
|
|||||||
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)
|
1. Download the [Non-Sucking Service Manager](https://nssm.cc/download)
|
||||||
2. Install NSSM:
|
2. Install NSSM:
|
||||||
```powershell
|
```powershell
|
||||||
nssm install Jellyseerr "C:\Program Files\nodejs\node.exe" ["C:\jellyseerr\dist\index.js"]
|
nssm install Jellyseerr "C:\Program Files\nodejs\node.exe" "C:\jellyseerr\dist\index.js"
|
||||||
|
nssm set Jellyseerr AppDirectory "C:\jellyseerr"
|
||||||
nssm set Jellyseerr AppEnvironmentExtra NODE_ENV=production
|
nssm set Jellyseerr AppEnvironmentExtra NODE_ENV=production
|
||||||
```
|
```
|
||||||
3. Start the service:
|
3. Start the service:
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ sidebar_position: 1
|
|||||||
:::info
|
:::info
|
||||||
This is the recommended method for most users.
|
This is the recommended method for most users.
|
||||||
Details on how to install Docker can be found on the [official Docker website](https://docs.docker.com/get-docker/).
|
Details on how to install Docker can be found on the [official Docker website](https://docs.docker.com/get-docker/).
|
||||||
|
|
||||||
|
Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresql-options) for details on how to configure your database.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Unix (Linux, macOS)
|
## Unix (Linux, macOS)
|
||||||
@@ -20,7 +22,7 @@ Be sure to replace `/path/to/appdata/config` in the below examples with a valid
|
|||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
<Tabs groupId="docker-methods">
|
<Tabs groupId="docker-methods" queryString>
|
||||||
<TabItem value="docker-cli" label="Docker CLI">
|
<TabItem value="docker-cli" label="Docker CLI">
|
||||||
For details on the Docker CLI, please [review the official `docker run` documentation](https://docs.docker.com/engine/reference/run/).
|
For details on the Docker CLI, please [review the official `docker run` documentation](https://docs.docker.com/engine/reference/run/).
|
||||||
|
|
||||||
@@ -35,8 +37,13 @@ docker run -d \
|
|||||||
-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 \
|
||||||
fallenbagel/jellyseerr
|
ghcr.io/fallenbagel/jellyseerr
|
||||||
```
|
```
|
||||||
|
:::tip
|
||||||
|
If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable to `emby`.
|
||||||
|
|
||||||
|
`-e JELLYFIN_TYPE=emby`
|
||||||
|
:::
|
||||||
|
|
||||||
To run the container as a specific user/group, you may optionally add `--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ]` to the above command.
|
To run the container as a specific user/group, you may optionally add `--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ]` to the above command.
|
||||||
|
|
||||||
@@ -48,7 +55,7 @@ docker stop jellyseerr && docker rm Jellyseerr
|
|||||||
```
|
```
|
||||||
Pull the latest image:
|
Pull the latest image:
|
||||||
```bash
|
```bash
|
||||||
docker pull fallenbagel/jellyseerr
|
docker pull ghcr.io/fallenbagel/jellyseerr
|
||||||
```
|
```
|
||||||
Finally, run the container with the same parameters originally used to create the container:
|
Finally, run the container with the same parameters originally used to create the container:
|
||||||
```bash
|
```bash
|
||||||
@@ -66,12 +73,12 @@ You could also use [diun](https://github.com/crazy-max/diun) to receive notifica
|
|||||||
For details on how to use Docker Compose, please [review the official Compose documentation](https://docs.docker.com/compose/reference/).
|
For details on how to use Docker Compose, please [review the official Compose documentation](https://docs.docker.com/compose/reference/).
|
||||||
|
|
||||||
#### Installation:
|
#### Installation:
|
||||||
Define the `jellyseerr` service in your `docker-compose.yml` as follows:
|
Define the `jellyseerr` service in your `compose.yaml` as follows:
|
||||||
```yaml
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
jellyseerr:
|
jellyseerr:
|
||||||
image: fallenbagel/jellyseerr:latest
|
image: ghcr.io/fallenbagel/jellyseerr:latest
|
||||||
container_name: jellyseerr
|
container_name: jellyseerr
|
||||||
environment:
|
environment:
|
||||||
- LOG_LEVEL=debug
|
- LOG_LEVEL=debug
|
||||||
@@ -83,21 +90,24 @@ services:
|
|||||||
- /path/to/appdata/config:/app/config
|
- /path/to/appdata/config:/app/config
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
:::tip
|
||||||
|
If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable to `emby`.
|
||||||
|
:::
|
||||||
|
|
||||||
Then, start all services defined in the Compose file:
|
Then, start all services defined in the Compose file:
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Updating:
|
#### Updating:
|
||||||
Pull the latest image:
|
Pull the latest image:
|
||||||
```bash
|
```bash
|
||||||
docker-compose pull jellyseerr
|
docker compose pull jellyseerr
|
||||||
```
|
```
|
||||||
Then, restart all services defined in the Compose file:
|
Then, restart all services defined in the Compose file:
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
You may alternatively use a third-party mechanism like [dockge](https://github.com/louislam/dockge) to manage your docker compose files.
|
You may alternatively use a third-party mechanism like [dockge](https://github.com/louislam/dockge) to manage your docker compose files.
|
||||||
:::
|
:::
|
||||||
@@ -133,10 +143,20 @@ or the Docker Desktop app:
|
|||||||
4. Enter a name for the volume (example: `jellyseerr-data`) and hit "Create"
|
4. Enter a name for the volume (example: `jellyseerr-data`) and hit "Create"
|
||||||
|
|
||||||
Then, create and start the Jellyseerr container:
|
Then, create and start the Jellyseerr container:
|
||||||
<Tabs groupId="docker-methods">
|
<Tabs groupId="docker-methods" queryString>
|
||||||
<TabItem value="docker-cli" label="Docker CLI">
|
<TabItem value="docker-cli" label="Docker CLI">
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name jellyseerr -e LOG_LEVEL=debug -e TZ=Asia/Tashkent -p 5055:5055 -v "jellyseerr-data:/app/config" --restart unless-stopped fallenbagel/jellyseerr:latest
|
docker run -d --name jellyseerr -e LOG_LEVEL=debug -e TZ=Asia/Tashkent -p 5055:5055 -v "jellyseerr-data:/app/config" --restart unless-stopped ghcr.io/fallenbagel/jellyseerr:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Updating:
|
||||||
|
Pull the latest image:
|
||||||
|
```bash
|
||||||
|
docker compose pull jellyseerr
|
||||||
|
```
|
||||||
|
Then, restart all services defined in the Compose file:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
@@ -160,25 +180,25 @@ volumes:
|
|||||||
jellyseerr-data:
|
jellyseerr-data:
|
||||||
external: true
|
external: true
|
||||||
```
|
```
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="docker-desktop" label="Docker Desktop">
|
#### Updating:
|
||||||
1. Open the Docker Desktop app
|
Pull the latest image:
|
||||||
2. Head to the Containers/Apps tab
|
```bash
|
||||||
3. Click on the "Add Container/App" button near the top right
|
docker compose pull jellyseerr
|
||||||
4. Fill in the container details:
|
```
|
||||||
- **Name**: `jellyseerr`
|
Then, restart all services defined in the Compose file:
|
||||||
- **Image**: `fallenbagel/jellyseerr:latest`
|
```bash
|
||||||
- **Port**: `5055:5055`
|
docker compose up -d
|
||||||
- **Volume**: `jellyseerr-data:/app/config`
|
```
|
||||||
- **Environment Variables**:
|
|
||||||
- **LOG_LEVEL**: `debug`
|
|
||||||
- **TZ**: `Asia/Tashkent`
|
|
||||||
- **Restart Policy**: `unless-stopped`
|
|
||||||
5. Click on the "Run" button
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
If you are using a named volume, then you can safely **ignore** the warning about the `/app/config` folder being incorrectly mounted.
|
||||||
|
|
||||||
|
If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable to `emby`.
|
||||||
|
:::
|
||||||
|
|
||||||
To access the files inside the volume created above, navigate to `\\wsl$\docker-desktop-data\version-pack-data\community\docker\volumes\jellyseerr-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\jellyseerr-data\_data` using File Explorer.
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
@@ -187,3 +207,6 @@ 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. Therefore the warning on the setup about the `/app/config` folder being incorrectly mounted page should be ignored.
|
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.
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
21
docs/getting-started/kubernetes.mdx
Normal file
21
docs/getting-started/kubernetes.mdx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
title: Kubernetes
|
||||||
|
description: Install Jellyseerr in Kubernetes
|
||||||
|
sidebar_position: 5
|
||||||
|
---
|
||||||
|
# Kubernetes
|
||||||
|
:::info
|
||||||
|
This method is not recommended for most users. It is intended for advanced users who are using Kubernetes.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```console
|
||||||
|
helm install jellyseerr oci://ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart
|
||||||
|
```
|
||||||
|
Helm values can be found in the Jellyseerr repository under [charts/jellyseerr-chart/README.md](https://github.com/Fallenbagel/jellyseerr/tree/develop/charts/jellyseerr-chart).
|
||||||
|
|
||||||
|
Verify the signature with [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) (replace [tag], with the TAG you want to verify) :
|
||||||
|
```console
|
||||||
|
cosign verify ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart:[tag] --certificate-identity=https://github.com/Fallenbagel/jellyseerr/.github/workflows/helm.yml@refs/heads/main --certificate-oidc-issuer=https://token.ac
|
||||||
|
tions.githubusercontent.com
|
||||||
|
```
|
||||||
@@ -6,6 +6,8 @@ sidebar_position: 3
|
|||||||
|
|
||||||
import { JellyseerrVersion, NixpkgVersion } from '@site/src/components/JellyseerrVersion';
|
import { JellyseerrVersion, NixpkgVersion } from '@site/src/components/JellyseerrVersion';
|
||||||
import Admonition from '@theme/Admonition';
|
import Admonition from '@theme/Admonition';
|
||||||
|
import Tabs from '@theme/Tabs';
|
||||||
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
# Nix Package Manager (Advanced)
|
# Nix Package Manager (Advanced)
|
||||||
:::info
|
:::info
|
||||||
@@ -13,22 +15,55 @@ This method is not recommended for most users. It is intended for advanced users
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
export const VersionMismatchWarning = () => {
|
export const VersionMismatchWarning = () => {
|
||||||
const jellyseerrVersion = JellyseerrVersion();
|
let jellyseerrVersion = null;
|
||||||
const nixpkgVersion = NixpkgVersion();
|
let nixpkgVersions = null;
|
||||||
|
try {
|
||||||
|
jellyseerrVersion = JellyseerrVersion();
|
||||||
|
nixpkgVersions = NixpkgVersion();
|
||||||
|
} catch (err) {
|
||||||
|
return (
|
||||||
|
<Admonition type="error">
|
||||||
|
Failed to load version information. Error: {err.message || JSON.stringify(err)}
|
||||||
|
</Admonition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isUpToDate = jellyseerrVersion === nixpkgVersion;
|
if (!nixpkgVersions || nixpkgVersions.error) {
|
||||||
|
return (
|
||||||
|
<Admonition type="error">
|
||||||
|
Failed to fetch Nixpkg versions: {nixpkgVersions?.error || 'Unknown error'}
|
||||||
|
</Admonition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUnstableUpToDate = jellyseerrVersion === nixpkgVersions.unstable;
|
||||||
|
const isStableUpToDate = jellyseerrVersion === nixpkgVersions.stable;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isUpToDate ? (
|
{!isStableUpToDate ? (
|
||||||
<Admonition type="warning">
|
<Admonition type="warning">
|
||||||
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to <a href="#overriding-the-package">override the package derivation</a>.
|
The{' '}
|
||||||
</Admonition>
|
<a href="https://github.com/NixOS/nixpkgs/blob/nixos-24.11/pkgs/servers/jellyseerr/default.nix#L14">
|
||||||
|
upstream Jellyseerr Nix Package (v{nixpkgVersions.stable})
|
||||||
|
</a>{' '}
|
||||||
|
is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>,{' '}
|
||||||
|
{isUnstableUpToDate ? (
|
||||||
|
<>
|
||||||
|
consider using the{' '}
|
||||||
|
<a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/by-name/je/jellyseerr/package.nix">
|
||||||
|
unstable package
|
||||||
|
</a>{' '}
|
||||||
|
instead.
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Admonition type="success">
|
<>
|
||||||
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is <b>up-to-date</b> with <b>Jellyseerr v{jellyseerrVersion}</b>.
|
you will need to{' '}
|
||||||
</Admonition>
|
<a href="#overriding-the-package-derivation">override the package derivation</a>.
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
</Admonition>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -48,6 +83,8 @@ To get up and running with jellyseerr using Nix, you can add the following to yo
|
|||||||
|
|
||||||
If you want more advanced configuration options, you can use the following:
|
If you want more advanced configuration options, you can use the following:
|
||||||
|
|
||||||
|
<Tabs groupId="nixpkg-methods" queryString>
|
||||||
|
<TabItem value="default" label="Default Configurations">
|
||||||
```nix
|
```nix
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
@@ -56,53 +93,20 @@ If you want more advanced configuration options, you can use the following:
|
|||||||
enable = true;
|
enable = true;
|
||||||
port = 5055;
|
port = 5055;
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
|
package = pkgs.jellyseerr; # Use the unstable package if stable is not up-to-date
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
</TabItem>
|
||||||
After adding the configuration to your `configuration.nix`, you can run the following command to install jellyseerr:
|
<TabItem value="custom" label="Database Configurations">
|
||||||
|
In order to use postgres, you will need to add override the default module of jellyseerr with the following as the current default module is not compatible with postgres:
|
||||||
```bash
|
```nix
|
||||||
nixos-rebuild switch
|
|
||||||
```
|
|
||||||
After rebuild is complete jellyseerr should be running, verify that it is with the following command.
|
|
||||||
```bash
|
|
||||||
systemctl status jellyseerr
|
|
||||||
```
|
|
||||||
|
|
||||||
:::info
|
|
||||||
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
|
||||||
:::
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import CodeBlock from '@theme/CodeBlock';
|
|
||||||
|
|
||||||
## Overriding the package derivation
|
|
||||||
export const VersionMatch = () => {
|
|
||||||
const jellyseerrVersion = JellyseerrVersion();
|
|
||||||
const nixpkgVersion = NixpkgVersion();
|
|
||||||
|
|
||||||
const code = `{ config, pkgs, ... }:
|
|
||||||
{
|
{
|
||||||
nixpkgs.config.packageOverrides = pkgs: {
|
config,
|
||||||
jellyseerr = pkgs.jellyseerr.overrideAttrs (oldAttrs: rec {
|
pkgs,
|
||||||
version = "${jellyseerrVersion}";
|
lib,
|
||||||
|
...
|
||||||
src = pkgs.fetchFromGitHub {
|
}:
|
||||||
rev = "v\${version}";
|
|
||||||
sha256 = pkgs.lib.fakeSha256;
|
|
||||||
};
|
|
||||||
|
|
||||||
offlineCache = pkgs.fetchYarnDeps {
|
|
||||||
sha256 = pkgs.lib.fakeSha256;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const module = `{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
cfg = config.services.jellyseerr;
|
cfg = config.services.jellyseerr;
|
||||||
@@ -113,28 +117,65 @@ in
|
|||||||
disabledModules = [ "services/misc/jellyseerr.nix" ];
|
disabledModules = [ "services/misc/jellyseerr.nix" ];
|
||||||
|
|
||||||
options.services.jellyseerr = {
|
options.services.jellyseerr = {
|
||||||
enable = mkEnableOption (mdDoc ''Jellyseerr, a requests manager for Jellyfin'');
|
enable = mkEnableOption ''Jellyseerr, a requests manager for Jellyfin'';
|
||||||
|
|
||||||
openFirewall = mkOption {
|
openFirewall = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = mdDoc ''Open port in the firewall for the Jellyseerr web interface.'';
|
description = ''Open port in the firewall for the Jellyseerr web interface.'';
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = types.port;
|
type = types.port;
|
||||||
default = 5055;
|
default = 5055;
|
||||||
description = mdDoc ''The port which the Jellyseerr web UI should listen to.'';
|
description = ''The port which the Jellyseerr web UI should listen to.'';
|
||||||
};
|
};
|
||||||
|
|
||||||
package = mkOption {
|
package = mkOption {
|
||||||
type = types.package;
|
type = types.package;
|
||||||
default = pkgs.jellyseerr;
|
default = pkgs.jellyseerr;
|
||||||
defaultText = literalExpression "pkgs.jellyseerr";
|
defaultText = literalExpression "pkgs.jellyseerr";
|
||||||
description = lib.mdDoc ''
|
description = ''
|
||||||
Jellyseerr package to use.
|
Jellyseerr package to use.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
databaseConfig = mkOption {
|
||||||
|
type = types.attrsOf types.str;
|
||||||
|
default = {
|
||||||
|
type = "sqlite";
|
||||||
|
configDirectory = "config";
|
||||||
|
logQueries = "false";
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Database configuration. For "sqlite", only "type", "configDirectory", and "logQueries" are relevant.
|
||||||
|
For "postgres", include host, port, user, pass, name, and optionally socket.
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
type = "postgres";
|
||||||
|
socket = "/run/postgresql";
|
||||||
|
user = "jellyseerr";
|
||||||
|
name = "jellyseerr";
|
||||||
|
logQueries = "false";
|
||||||
|
}
|
||||||
|
or
|
||||||
|
{
|
||||||
|
type = "postgres";
|
||||||
|
host = "localhost";
|
||||||
|
port = "5432";
|
||||||
|
user = "dbuser";
|
||||||
|
pass = "password";
|
||||||
|
name = "jellyseerr";
|
||||||
|
logQueries = "false";
|
||||||
|
}
|
||||||
|
or
|
||||||
|
{
|
||||||
|
type = "sqlite";
|
||||||
|
configDirectory = "config";
|
||||||
|
logQueries = "false";
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
@@ -142,14 +183,29 @@ in
|
|||||||
description = "Jellyseerr, a requests manager for Jellyfin";
|
description = "Jellyseerr, a requests manager for Jellyfin";
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
environment.PORT = toString cfg.port;
|
environment =
|
||||||
|
let
|
||||||
|
dbConfig = cfg.databaseConfig;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
PORT = toString cfg.port;
|
||||||
|
DB_TYPE = toString dbConfig.type;
|
||||||
|
CONFIG_DIRECTORY = toString dbConfig.configDirectory or "";
|
||||||
|
DB_LOG_QUERIES = toString dbConfig.logQueries;
|
||||||
|
DB_HOST = if dbConfig.type == "postgres" && !(hasAttr "socket" dbConfig) then toString dbConfig.host or "" else "";
|
||||||
|
DB_PORT = if dbConfig.type == "postgres" && !(hasAttr "socket" dbConfig) then toString dbConfig.port or "" else "";
|
||||||
|
DB_SOCKET_PATH = if dbConfig.type == "postgres" && hasAttr "socket" dbConfig then toString dbConfig.socket or "" else "";
|
||||||
|
DB_USER = if dbConfig.type == "postgres" then toString dbConfig.user or "" else "";
|
||||||
|
DB_PASS = if dbConfig.type == "postgres" then toString dbConfig.pass or "" else "";
|
||||||
|
DB_NAME = if dbConfig.type == "postgres" then toString dbConfig.name or "" else "";
|
||||||
|
};
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "exec";
|
Type = "exec";
|
||||||
StateDirectory = "jellyseerr";
|
StateDirectory = "jellyseerr";
|
||||||
WorkingDirectory = "\${cfg.package}/libexec/jellyseerr/deps/jellyseerr";
|
WorkingDirectory = "${cfg.package}/libexec/jellyseerr";
|
||||||
DynamicUser = true;
|
DynamicUser = true;
|
||||||
ExecStart = "\${cfg.package}/bin/jellyseerr";
|
ExecStart = "${cfg.package}/bin/jellyseerr";
|
||||||
BindPaths = [ "/var/lib/jellyseerr/:\${cfg.package}/libexec/jellyseerr/deps/jellyseerr/config/" ];
|
BindPaths = [ "/var/lib/jellyseerr/:${cfg.package}/libexec/jellyseerr/config/" ];
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
ProtectHome = true;
|
ProtectHome = true;
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
@@ -169,57 +225,47 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall = mkIf cfg.openFirewall {
|
networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };
|
||||||
allowedTCPPorts = [ cfg.port ];
|
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
}`;
|
```
|
||||||
|
Then, import the module into your `configuration.nix`:
|
||||||
const configuration = `{ config, pkgs, ... }:
|
```nix
|
||||||
|
{ config, pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [ ./jellyseerr-module.nix ]
|
imports = [ ./modules/jellyseerr.nix ];
|
||||||
|
|
||||||
services.jellyseerr = {
|
services.jellyseerr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
port = 5055;
|
port = 5055;
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
package = (pkgs.callPackage (import ../../../pkgs/jellyseerr) { });
|
package = pkgs.unstable.jellyseerr; # use the unstable package if stable is not up-to-date
|
||||||
|
databaseConfig = {
|
||||||
|
type = "postgres";
|
||||||
|
host = "localhost"; # or socket: "/run/postgresql"
|
||||||
|
port = "5432"; # if using socket, this is not needed
|
||||||
|
user = "jellyseerr";
|
||||||
|
pass = "jellyseerr";
|
||||||
|
name = "jellyseerr";
|
||||||
|
logQueries = "false";
|
||||||
};
|
};
|
||||||
}`;
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
const isUpToDate = jellyseerrVersion === nixpkgVersion;
|
After adding the configuration to your `configuration.nix`, you can run the following command to install jellyseerr:
|
||||||
|
|
||||||
return (
|
```bash
|
||||||
<>
|
nixos-rebuild switch
|
||||||
{isUpToDate ? (
|
```
|
||||||
<>
|
After rebuild is complete jellyseerr should be running, verify that it is with the following command.
|
||||||
<p>The latest version of Jellyseerr <strong>({jellyseerrVersion})</strong> and the Jellyseerr nixpkg package version <strong>({nixpkgVersion})</strong> is <strong>up-to-date</strong>.</p>
|
```bash
|
||||||
<p>There is no need to override the package derivation.</p>
|
systemctl status jellyseerr
|
||||||
</>
|
```
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p>The latest version of Jellyseerr <strong>({jellyseerrVersion})</strong> and the Jellyseerr nixpkg version <strong>(v{nixpkgVersion})</strong> is <strong>out-of-date</strong>.
|
|
||||||
If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to override the package derivation.</p>
|
|
||||||
<p>In order to override the package derivation:</p>
|
|
||||||
<ol>
|
|
||||||
<li style={{ marginBottom: '1rem' }}>Grab the <a href="https://raw.githubusercontent.com/NixOS/nixpkgs/nixos-unstable/pkgs/servers/jellyseerr/default.nix">latest nixpkg derivation for Jellyseerr</a></li>
|
|
||||||
<li style={{ marginBottom: '1rem' }}>Grab the latest <a href="https://raw.githubusercontent.com/Fallenbagel/jellyseerr/main/package.json">package.json</a> for Jellyseerr</li>
|
|
||||||
<li style={{ marginBottom: '1rem' }}>Add it to the same directory as the nixpkg derivation</li>
|
|
||||||
<li style={{ marginBottom: '1rem' }}>Update the `src` and `offlineCache` attributes in the nixpkg derivation:</li>
|
|
||||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{code}</CodeBlock>
|
|
||||||
<Admonition type="tip" style={{ marginBottom: '1rem' }}>You can replace the <b>sha256</b> with the actual hash that <b>nixos-rebuild</b> outputs when you run the command.</Admonition>
|
|
||||||
<li style={{ marginBottom: '1rem' }}>Grab this module and import it in your `configuration.nix`</li>
|
|
||||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{module}</CodeBlock>
|
|
||||||
<Admonition type="tip" style={{ marginBottom: '1rem' }}>We are using a custom module because the upstream module does not have a package option.</Admonition>
|
|
||||||
<li style={{ marginBottom: '1rem' }}>Call the new package in your `configuration.nix`</li>
|
|
||||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{configuration}</CodeBlock>
|
|
||||||
</ol>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
:::info
|
||||||
|
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
||||||
<VersionMatch />
|
:::
|
||||||
|
|
||||||
|
|||||||
178
docs/troubleshooting.mdx
Normal file
178
docs/troubleshooting.mdx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
title: Troubleshooting
|
||||||
|
---
|
||||||
|
|
||||||
|
import Tabs from '@theme/Tabs';
|
||||||
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
|
## [TMDB] failed to retrieve/fetch XXX
|
||||||
|
|
||||||
|
### Option 1: Change your DNS servers
|
||||||
|
|
||||||
|
This error often comes from your Internet Service Provider (ISP) blocking TMDB API. The ISP may block the DNS resolution to the TMDB API hostname.
|
||||||
|
|
||||||
|
To fix this, you can change your DNS servers to a public DNS service like Google's DNS or Cloudflare's DNS:
|
||||||
|
|
||||||
|
<Tabs groupId="methods" queryString>
|
||||||
|
<TabItem value="docker-cli" label="Docker CLI">
|
||||||
|
|
||||||
|
Add the following to your `docker run` command to use Google's DNS:
|
||||||
|
```bash
|
||||||
|
--dns=8.8.8.8
|
||||||
|
```
|
||||||
|
or for Cloudflare's DNS:
|
||||||
|
```bash
|
||||||
|
--dns=1.1.1.1
|
||||||
|
```
|
||||||
|
or for Quad9 DNS:
|
||||||
|
```bash
|
||||||
|
--dns=9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
|
You can try them all and see which one works for your network.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="docker-compose" label="Docker Compose">
|
||||||
|
|
||||||
|
Add the following to your `compose.yaml` to use Google's DNS:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
jellyseerr:
|
||||||
|
dns:
|
||||||
|
- 8.8.8.8
|
||||||
|
```
|
||||||
|
or for Cloudflare's DNS:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
jellyseerr:
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
```
|
||||||
|
or for Quad9's DNS:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
jellyseerr:
|
||||||
|
dns:
|
||||||
|
- 9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
|
You can try them all and see which one works for your network.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="windows" label="Windows">
|
||||||
|
|
||||||
|
1. Open the Control Panel.
|
||||||
|
2. Click on Network and Internet.
|
||||||
|
3. Click on Network and Sharing Center.
|
||||||
|
4. Click on Change adapter settings.
|
||||||
|
5. Right-click the network interface connected to the internet and select Properties.
|
||||||
|
6. Select Internet Protocol Version 4 (TCP/IPv4) and click Properties.
|
||||||
|
7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS or `9.9.9.9` for Quad9's DNS.
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="linux" label="Linux">
|
||||||
|
|
||||||
|
1. Open a terminal.
|
||||||
|
2. Edit the `/etc/resolv.conf` file with your favorite text editor.
|
||||||
|
3. Add the following line to use Google's DNS:
|
||||||
|
```bash
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
```
|
||||||
|
or for Cloudflare's DNS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nameserver 1.1.1.1
|
||||||
|
```
|
||||||
|
or for Quad9's DNS:
|
||||||
|
```bash
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Option 2: Force IPV4 resolution first
|
||||||
|
|
||||||
|
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
|
||||||
|
|
||||||
|
You can try to force the resolution to use IPV4 first by going to `Settings > Networking > Advanced Networking` and enabling `Force IPv4 Resolution First` setting and restarting. You can also add the environment variable, `FORCE_IPV4_FIRST=true`:
|
||||||
|
|
||||||
|
<Tabs groupId="methods" queryString>
|
||||||
|
<TabItem value="docker-cli" label="Docker CLI">
|
||||||
|
|
||||||
|
Add the following to your `docker run` command:
|
||||||
|
```bash
|
||||||
|
-e "FORCE_IPV4_FIRST=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="docker-compose" label="Docker Compose">
|
||||||
|
|
||||||
|
Add the following to your `compose.yaml`:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
jellyseerr:
|
||||||
|
environment:
|
||||||
|
- FORCE_IPV4_FIRST=true
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Option 3: Use Jellyseerr through a proxy
|
||||||
|
|
||||||
|
If you can't change your DNS servers or force IPV4 resolution, you can use Jellyseerr through a proxy.
|
||||||
|
|
||||||
|
In some places (like China), the ISP blocks not only the DNS resolution but also the connection to the TMDB API.
|
||||||
|
|
||||||
|
You can configure Jellyseerr to use a proxy with the [HTTP(S) Proxy](/using-jellyseerr/settings/general#https-proxy) setting.
|
||||||
|
|
||||||
|
### Option 4: Check that your server can reach TMDB API
|
||||||
|
|
||||||
|
Make sure that your server can reach the TMDB API by running the following command:
|
||||||
|
|
||||||
|
<Tabs groupId="methods" queryString>
|
||||||
|
<TabItem value="docker-cli" label="Docker CLI">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem value="docker-compose" label="Docker Compose">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org"
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="linux" label="Linux">
|
||||||
|
|
||||||
|
In a terminal:
|
||||||
|
```bash
|
||||||
|
curl -L https://api.themoviedb.org
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="windows" label="Windows">
|
||||||
|
|
||||||
|
In a PowerShell window:
|
||||||
|
```powershell
|
||||||
|
(Invoke-WebRequest -Uri "https://api.themoviedb.org" -Method Get).Content
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
If you can't get a response, then your server can't reach the TMDB API.
|
||||||
|
This is usually due to a network configuration issue or a firewall blocking the connection.
|
||||||
93
docs/using-jellyseerr/backups.md
Normal file
93
docs/using-jellyseerr/backups.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: Backups
|
||||||
|
description: Understand which data you should back up.
|
||||||
|
sidebar_position: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# Which data does Jellyseerr save and where?
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
All configurations from the **Settings** panel in the Jellyseerr web UI are saved, including integrations with Radarr, Sonarr, Jellyfin, Plex, and notification settings.
|
||||||
|
These settings are stored in the `settings.json` file located in the Jellyseerr data folder.
|
||||||
|
|
||||||
|
## User Data
|
||||||
|
|
||||||
|
Apart from the settings, all other data—including user accounts, media requests, blacklist etc. are stored in the database (either SQLite or PostgreSQL).
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
|
||||||
|
### SQLite
|
||||||
|
|
||||||
|
If your backup system uses filesystem snapshots (such as Kubernetes with Volsync), you can directly back up the Jellyseerr data folder.
|
||||||
|
Otherwise, you need to stop the Jellyseerr application and back up the `config` folder.
|
||||||
|
|
||||||
|
For advanced users, it's possible to back up the database without stopping the application by using the [SQLite CLI](https://www.sqlite.org/download.html). Run the following command to create a backup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 db/db.sqlite3 ".backup '/tmp/jellyseerr_db.sqlite3.bak'"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, copy the `/tmp/jellyseerr_dump.sqlite3.bak` file to your desired backup location.
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
You can back up the `config` folder and dump the PostgreSQL database without stopping the Jellyseerr application.
|
||||||
|
|
||||||
|
Install [postgresql-client](https://www.postgresql.org/download/) and run the following command to create a backup (just replace the placeholders):
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below.
|
||||||
|
|
||||||
|
-h, --host=HOSTNAME database server host or socket directory
|
||||||
|
|
||||||
|
-p, --port=PORT database server port number
|
||||||
|
:::
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pg_dump -U <database_user> -d <database_name> -f /tmp/jellyseerr_db.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
|
||||||
|
### SQLite
|
||||||
|
|
||||||
|
After restoring your `db/db.sqlite3` file and, optionally, the `settings.json` file, the `config` folder structure should look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── cache <-- Optional
|
||||||
|
├── db
|
||||||
|
│ └── db.sqlite3
|
||||||
|
├── logs <-- Optional
|
||||||
|
└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr)
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the files are restored, start the Jellyseerr application.
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
Install the [PostgreSQL client](https://www.postgresql.org/download/) and restore the PostgreSQL database using the following command (replace the placeholders accordingly):
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below.
|
||||||
|
|
||||||
|
-h, --host=HOSTNAME database server host or socket directory
|
||||||
|
|
||||||
|
-p, --port=PORT database server port number
|
||||||
|
:::
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pg_restore -U <database_user> -d <database_name> /tmp/jellyseerr_db.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Optionally, restore the `settings.json` file. The `config` folder structure should look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── cache <-- Optional
|
||||||
|
├── logs <-- Optional
|
||||||
|
└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr)
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the database and files are restored, start the Jellyseerr application.
|
||||||
@@ -18,6 +18,10 @@ Users can optionally opt-in to being mentioned in Discord notifications by confi
|
|||||||
|
|
||||||
You can find the webhook URL in the Discord application, at **Server Settings → Integrations → Webhooks**.
|
You can find the webhook URL in the Discord application, at **Server Settings → Integrations → Webhooks**.
|
||||||
|
|
||||||
|
### Notification Role ID (optional)
|
||||||
|
|
||||||
|
If a role ID is specified, it will be included in the webhook message. See [Discord role ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID).
|
||||||
|
|
||||||
### Bot Username (optional)
|
### Bot Username (optional)
|
||||||
|
|
||||||
If you would like to override the name you configured for your bot in Discord, you may set this value to whatever you like!
|
If you would like to override the name you configured for your bot in Discord, you may set this value to whatever you like!
|
||||||
|
|||||||
@@ -12,19 +12,21 @@ This is your Jellyseerr API key, which can be used to integrate Jellyseerr with
|
|||||||
|
|
||||||
If you need to generate a new API key for any reason, simply click the button to the right of the text box.
|
If you need to generate a new API key for any reason, simply click the button to the right of the text box.
|
||||||
|
|
||||||
|
If you want to set the API key, rather than letting it be randomly generated, you can use the API_KEY environment variable. Whatever that variable is set to will be your API key.
|
||||||
|
|
||||||
## Application Title
|
## Application Title
|
||||||
|
|
||||||
If you aren't a huge fan of the name "Jellyseerr" and would like to display something different to your users, you can customize the application title!
|
If you aren't a huge fan of the name "Jellyseerr" and would like to display something different to your users, you can customize the application title!
|
||||||
|
|
||||||
## Application URL
|
## Application URL
|
||||||
|
|
||||||
Set this to the externally-accessible URL of your Overseerr instance.
|
Set this to the externally-accessible URL of your Jellyseerr instance.
|
||||||
|
|
||||||
You must configure this setting in order to enable password reset and generation emails.
|
You must configure this setting in order to enable password reset and generation emails.
|
||||||
|
|
||||||
## Enable Proxy Support
|
## Enable Proxy Support
|
||||||
|
|
||||||
If you have Overseerr behind a reverse proxy, enable this setting to allow Overseerr to correctly register client IP addresses. For details, please see the [Express Documentation](https://expressjs.com/en/guide/behind-proxies.html).
|
If you have Jellyseerr behind a reverse proxy, enable this setting to allow Jellyseerr to correctly register client IP addresses. For details, please see the [Express Documentation](https://expressjs.com/en/guide/behind-proxies.html).
|
||||||
|
|
||||||
This setting is **disabled** by default.
|
This setting is **disabled** by default.
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ If you do not use Jellyseerr integrations with third-party applications to add/m
|
|||||||
|
|
||||||
One caveat, however, is that HTTPS is required, meaning that once this setting is enabled, you will no longer be able to access your Jellyseerr instance over _HTTP_ (including using an IP address and port number).
|
One caveat, however, is that HTTPS is required, meaning that once this setting is enabled, you will no longer be able to access your Jellyseerr instance over _HTTP_ (including using an IP address and port number).
|
||||||
|
|
||||||
If you enable this setting and find yourself unable to access Overseerr, you can disable the setting by modifying `settings.json` in `/app/config`.
|
If you enable this setting and find yourself unable to access Jellyseerr, you can disable the setting by modifying `settings.json` in `/app/config`.
|
||||||
|
|
||||||
This setting is **disabled** by default.
|
This setting is **disabled** by default.
|
||||||
|
|
||||||
@@ -56,9 +58,9 @@ You should enable this if you are having issues with loading images directly fro
|
|||||||
|
|
||||||
Set the default display language for Jellyseerr. Users can override this setting in their user settings.
|
Set the default display language for Jellyseerr. Users can override this setting in their user settings.
|
||||||
|
|
||||||
## Discover Region & Discover Language
|
## Discover Region, Discover Language & Streaming Region
|
||||||
|
|
||||||
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. Users can override these global settings by configuring these same options in their user settings.
|
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. The Streaming Region filters the available streaming providers on the media page. Users can override these global settings by configuring these same options in their user settings.
|
||||||
|
|
||||||
## Hide Available Media
|
## Hide Available Media
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ sidebar_position: 3
|
|||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
<Tabs groupId="media-server-type">
|
<Tabs groupId="media-server-type" queryString>
|
||||||
<TabItem value="jellyfin" label="Jellyfin">
|
<TabItem value="jellyfin" label="Jellyfin">
|
||||||
:::info
|
:::info
|
||||||
To set up Jellyfin, make sure you log in using an account with administrative privileges.
|
To set up Jellyfin, make sure you log in using an account with administrative privileges.
|
||||||
@@ -32,7 +32,7 @@ Jellyseerr will perform a full scan of your Jellyfin libraries once every 24 hou
|
|||||||
|
|
||||||
This section is where you configure the connection to your Jellyfin server.
|
This section is where you configure the connection to your Jellyfin server.
|
||||||
|
|
||||||
<Tabs groupId="versions">
|
<Tabs groupId="versions" queryString>
|
||||||
<TabItem value="latest" label="Latest">
|
<TabItem value="latest" label="Latest">
|
||||||
|
|
||||||
#### Internal URL
|
#### Internal URL
|
||||||
@@ -126,7 +126,7 @@ Jellyseerr will perform a full scan of your Emby libraries once every 24 hours (
|
|||||||
|
|
||||||
This section is where you configure the connection to your Emby server.
|
This section is where you configure the connection to your Emby server.
|
||||||
|
|
||||||
<Tabs groupId="versions">
|
<Tabs groupId="versions" queryString>
|
||||||
<TabItem value="latest" label="Latest">
|
<TabItem value="latest" label="Latest">
|
||||||
|
|
||||||
#### Internal URL
|
#### Internal URL
|
||||||
|
|||||||
@@ -14,9 +14,17 @@ When disabled, your mediaserver OAuth becomes the only sign-in option, and any "
|
|||||||
|
|
||||||
This setting is **enabled** by default.
|
This setting is **enabled** by default.
|
||||||
|
|
||||||
|
## Enable Jellyfin/Emby/Plex Sign-In
|
||||||
|
|
||||||
|
When enabled, users will be able to sign in to Jellyseerr using their Jellyfin/Emby/Plex credentials, provided they have linked their media server accounts.
|
||||||
|
|
||||||
|
When disabled, users will only be able to sign in using their email address. Users without a password set will not be able to sign in to Jellyseerr.
|
||||||
|
|
||||||
|
This setting is **enabled** by default.
|
||||||
|
|
||||||
## Enable New Jellyfin/Emby/Plex Sign-In
|
## Enable New Jellyfin/Emby/Plex Sign-In
|
||||||
|
|
||||||
When enabled, users with access to your media server will be able to sign in to Overseerr even if they have not yet been imported. Users will be automatically assigned the permissions configured in the [Default Permissions](#default-permissions) setting upon first sign-in.
|
When enabled, users with access to your media server will be able to sign in to Jellyseerr even if they have not yet been imported. Users will be automatically assigned the permissions configured in the [Default Permissions](#default-permissions) setting upon first sign-in.
|
||||||
|
|
||||||
This setting is **enabled** by default.
|
This setting is **enabled** by default.
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ sidebar_position: 2
|
|||||||
|
|
||||||
# Adding Users
|
# Adding Users
|
||||||
|
|
||||||
There are currently two methods to add users to Overseerr: importing Plex users and creating "local users." All new users are created with the [default permissions](/using-jellyseerr/settings/users#default-permissions) defined in **Settings → Users**.
|
There are currently two methods to add users to Jellyseerr: importing Mediaserver users and creating "local users." All new users are created with the [default permissions](/using-jellyseerr/settings/users#default-permissions) defined in **Settings → Users**.
|
||||||
|
|
||||||
### Importing Mediaserver Users
|
### Importing Mediaserver Users
|
||||||
|
|
||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
<Tabs groupId="media-server-type">
|
<Tabs groupId="media-server-type" queryString>
|
||||||
<TabItem value="jellyfin" label="Jellyfin">
|
<TabItem value="jellyfin" label="Jellyfin">
|
||||||
Clicking the **Import Jellyfin Users** button on the **User List** page will fetch the list of users with access to the Jellyfin server and add them to Jellyseerr automatically.
|
Clicking the **Import Jellyfin Users** button on the **User List** page will fetch the list of users with access to the Jellyfin server and add them to Jellyseerr automatically.
|
||||||
|
|
||||||
@@ -25,9 +25,9 @@ To disable new Jellyfin sign-ins, navigate to **Settings → Users** and unch
|
|||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="emby" label="Emby">
|
<TabItem value="emby" label="Emby">
|
||||||
Clicking the **Import Emby Users** button on the **User List** page will fetch the list of users with access to the Emby server and add them to Overseerr automatically.
|
Clicking the **Import Emby Users** button on the **User List** page will fetch the list of users with access to the Emby server and add them to Jellyseerr automatically.
|
||||||
|
|
||||||
Importing Emby users is not required, however. Any user with access to the Emby server can log in to Overseerr even if they have not been imported, and will be assigned the configured [default permissions](/using-jellyseerr/settings/users#default-permissions) upon their first login.
|
Importing Emby users is not required, however. Any user with access to the Emby server can log in to Jellyseerr even if they have not been imported, and will be assigned the configured [default permissions](/using-jellyseerr/settings/users#default-permissions) upon their first login.
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
To disable new Emby sign-ins, navigate to **Settings → Users** and uncheck the [**Enable New Emby Sign-In**](/using-jellyseerr/settings/users#enable-new-jellyfinembyplex-sign-in) box.
|
To disable new Emby sign-ins, navigate to **Settings → Users** and uncheck the [**Enable New Emby Sign-In**](/using-jellyseerr/settings/users#enable-new-jellyfinembyplex-sign-in) box.
|
||||||
@@ -36,9 +36,9 @@ To disable new Emby sign-ins, navigate to **Settings → Users** and uncheck
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem value="plex" label="Plex">
|
<TabItem value="plex" label="Plex">
|
||||||
Clicking the **Import Plex Users** button on the **User List** page will fetch the list of users with access to the Plex server from [plex.tv](https://www.plex.tv/), and add them to Overseerr automatically.
|
Clicking the **Import Plex Users** button on the **User List** page will fetch the list of users with access to the Plex server from [plex.tv](https://www.plex.tv/), and add them to Jellyseerr automatically.
|
||||||
|
|
||||||
Importing Plex users is not required, however. Any user with access to the Plex server can log in to Overseerr even if they have not been imported, and will be assigned the configured [default permissions](/using-jellyseerr/settings/users#default-permissions) upon their first login.
|
Importing Plex users is not required, however. Any user with access to the Plex server can log in to Jellyseerr even if they have not been imported, and will be assigned the configured [default permissions](/using-jellyseerr/settings/users#default-permissions) upon their first login.
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
To disable new Plex sign-ins, navigate to **Settings → Users** and uncheck the [**Enable New Plex Sign-In**](/using-jellyseerr/settings/users#enable-new-jellyfinembyplex-sign-in) box.
|
To disable new Plex sign-ins, navigate to **Settings → Users** and uncheck the [**Enable New Plex Sign-In**](/using-jellyseerr/settings/users#enable-new-jellyfinembyplex-sign-in) box.
|
||||||
@@ -49,7 +49,7 @@ To disable new Plex sign-ins, navigate to **Settings → Users** and uncheck
|
|||||||
|
|
||||||
### Creating Local Users
|
### Creating Local Users
|
||||||
|
|
||||||
If you would like to grant Overseerr access to a user who doesn't have their own Plex account and/or access to the Plex server, you can manually add them by clicking the **Create Local User** button.
|
If you would like to grant Jellyseerr access to a user who doesn't have their own Plex account and/or access to the Plex server, you can manually add them by clicking the **Create Local User** button.
|
||||||
|
|
||||||
#### Email Address
|
#### Email Address
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ Enter a valid email address at which the user can receive messages pertaining to
|
|||||||
|
|
||||||
#### Automatically Generate Password
|
#### Automatically Generate Password
|
||||||
|
|
||||||
If an [application URL](/using-jellyseerr/settings/general#application-url) is set and [email notifications](/using-jellyseerr/notifications/email) have been configured and enabled, Overseerr can automatically generate a password for the new user.
|
If an [application URL](/using-jellyseerr/settings/general#application-url) is set and [email notifications](/using-jellyseerr/notifications/email) have been configured and enabled, Jellyseerr can automatically generate a password for the new user.
|
||||||
|
|
||||||
#### Password
|
#### Password
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Users can override the [global display language](/using-jellyseerr/settings/gene
|
|||||||
|
|
||||||
### Discover Region & Discover Language
|
### Discover Region & Discover Language
|
||||||
|
|
||||||
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-and-discover-language) to suit their own preferences.
|
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-discover-language--streaming-region) to suit their own preferences.
|
||||||
|
|
||||||
### Movie Request Limit & Series Request Limit
|
### Movie Request Limit & Series Request Limit
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Jellyseerr docs is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
Jellyseerr docs is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||||
|
|
||||||
Jellyseerr docs will be available at [docs.jellyseerr.com](https://docs.jellyseerr.dev).
|
Jellyseerr docs will be available at [docs.jellyseerr.dev](https://docs.jellyseerr.dev).
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ const config: Config = {
|
|||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
trailingSlash: false,
|
trailingSlash: false,
|
||||||
|
|
||||||
organizationName: 'Fallenbagel',
|
organizationName: 'fallenbagel',
|
||||||
projectName: 'Jellyseerr',
|
projectName: 'Jellyseerr',
|
||||||
|
deploymentBranch: 'gh-pages',
|
||||||
|
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
onBrokenMarkdownLinks: 'warn',
|
onBrokenMarkdownLinks: 'warn',
|
||||||
@@ -31,7 +32,7 @@ const config: Config = {
|
|||||||
routeBasePath: '/',
|
routeBasePath: '/',
|
||||||
path: '../docs',
|
path: '../docs',
|
||||||
editUrl:
|
editUrl:
|
||||||
'https://github.com/Fallenbagel/jellyseerr/edit/develop/docs/',
|
'https://github.com/fallenbagel/jellyseerr/edit/develop/docs/',
|
||||||
},
|
},
|
||||||
blog: false,
|
blog: false,
|
||||||
pages: false,
|
pages: false,
|
||||||
@@ -57,7 +58,6 @@ const config: Config = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
image: 'img/docusaurus-social-card.jpg',
|
|
||||||
colorMode: {
|
colorMode: {
|
||||||
defaultMode: 'dark',
|
defaultMode: 'dark',
|
||||||
disableSwitch: true,
|
disableSwitch: true,
|
||||||
@@ -70,7 +70,7 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
href: 'https://github.com/Fallenbagel/jellyseerr',
|
href: 'https://github.com/fallenbagel/jellyseerr',
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0"
|
"node": ">=22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,25 +26,37 @@ export const JellyseerrVersion = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NixpkgVersion = () => {
|
export const NixpkgVersion = () => {
|
||||||
const [version, setVersion] = useState(null);
|
const [versions, setVersions] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVersion = async () => {
|
const fetchVersion = async () => {
|
||||||
try {
|
try {
|
||||||
const url =
|
const unstableUrl =
|
||||||
'https://raw.githubusercontent.com/NixOS/nixpkgs/nixos-unstable/pkgs/servers/jellyseerr/default.nix';
|
'https://raw.githubusercontent.com/NixOS/nixpkgs/refs/heads/nixos-unstable/pkgs/by-name/je/jellyseerr/package.nix';
|
||||||
const response = await fetch(url);
|
const stableUrl =
|
||||||
const data = await response.text();
|
'https://raw.githubusercontent.com/NixOS/nixpkgs/refs/heads/nixos-24.11/pkgs/servers/jellyseerr/default.nix';
|
||||||
|
|
||||||
|
const [unstableResponse, stableResponse] = await Promise.all([
|
||||||
|
fetch(unstableUrl),
|
||||||
|
fetch(stableUrl),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const unstableData = await unstableResponse.text();
|
||||||
|
const stableData = await stableResponse.text();
|
||||||
|
|
||||||
const versionRegex = /version\s*=\s*"([^"]+)"/;
|
const versionRegex = /version\s*=\s*"([^"]+)"/;
|
||||||
const match = data.match(versionRegex);
|
|
||||||
if (match && match[1]) {
|
const unstableMatch = unstableData.match(versionRegex);
|
||||||
setVersion(match[1]);
|
const stableMatch = stableData.match(versionRegex);
|
||||||
} else {
|
|
||||||
setError('0.0.0');
|
const unstableVersion =
|
||||||
}
|
unstableMatch && unstableMatch[1] ? unstableMatch[1] : '0.0.0';
|
||||||
|
const stableVersion =
|
||||||
|
stableMatch && stableMatch[1] ? stableMatch[1] : '0.0.0';
|
||||||
|
|
||||||
|
setVersions({ unstable: unstableVersion, stable: stableVersion });
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
@@ -63,5 +75,5 @@ export const NixpkgVersion = () => {
|
|||||||
return { error };
|
return { error };
|
||||||
}
|
}
|
||||||
|
|
||||||
return version;
|
return versions;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
openapi: '3.0.2'
|
openapi: '3.0.2'
|
||||||
info:
|
info:
|
||||||
title: 'Overseerr API'
|
title: 'Jellyseerr API'
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
description: |
|
description: |
|
||||||
This is the documentation for the Overseerr API backend.
|
This is the documentation for the Jellyseerr API backend.
|
||||||
|
|
||||||
Two primary authentication methods are supported:
|
Two primary authentication methods are supported:
|
||||||
|
|
||||||
- **Cookie Authentication**: A valid sign-in to the `/auth/plex` or `/auth/local` will generate a valid authentication cookie.
|
- **Cookie Authentication**: A valid sign-in to the `/auth/plex` or `/auth/local` will generate a valid authentication cookie.
|
||||||
- **API Key Authentication**: Sign-in is also possible by passing an `X-Api-Key` header along with a valid API Key generated by Overseerr.
|
- **API Key Authentication**: Sign-in is also possible by passing an `X-Api-Key` header along with a valid API Key generated by Jellyseerr.
|
||||||
tags:
|
tags:
|
||||||
- name: public
|
- name: public
|
||||||
description: Public API endpoints requiring no authentication.
|
description: Public API endpoints requiring no authentication.
|
||||||
- name: settings
|
- name: settings
|
||||||
description: Endpoints related to Overseerr's settings and configuration.
|
description: Endpoints related to Jellyseerr's settings and configuration.
|
||||||
- name: auth
|
- name: auth
|
||||||
description: Endpoints related to logging in or out, and the currently authenticated user.
|
description: Endpoints related to logging in or out, and the currently authenticated user.
|
||||||
- name: users
|
- name: users
|
||||||
@@ -38,6 +38,8 @@ tags:
|
|||||||
description: Endpoints related to getting service (Radarr/Sonarr) details.
|
description: Endpoints related to getting service (Radarr/Sonarr) details.
|
||||||
- name: watchlist
|
- name: watchlist
|
||||||
description: Collection of media to watch later
|
description: Collection of media to watch later
|
||||||
|
- name: blacklist
|
||||||
|
description: Blacklisted media from discovery page.
|
||||||
servers:
|
servers:
|
||||||
- url: '{server}/api/v1'
|
- url: '{server}/api/v1'
|
||||||
variables:
|
variables:
|
||||||
@@ -46,6 +48,19 @@ servers:
|
|||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
Blacklist:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
tmdbId:
|
||||||
|
type: number
|
||||||
|
example: 1
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
media:
|
||||||
|
$ref: '#/components/schemas/MediaInfo'
|
||||||
|
userId:
|
||||||
|
type: number
|
||||||
|
example: 1
|
||||||
Watchlist:
|
Watchlist:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -128,10 +143,12 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
locale:
|
locale:
|
||||||
type: string
|
type: string
|
||||||
region:
|
discoverRegion:
|
||||||
type: string
|
type: string
|
||||||
originalLanguage:
|
originalLanguage:
|
||||||
type: string
|
type: string
|
||||||
|
streamingRegion:
|
||||||
|
type: string
|
||||||
MainSettings:
|
MainSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -143,16 +160,10 @@ components:
|
|||||||
example: en
|
example: en
|
||||||
applicationTitle:
|
applicationTitle:
|
||||||
type: string
|
type: string
|
||||||
example: Overseerr
|
example: Jellyseerr
|
||||||
applicationUrl:
|
applicationUrl:
|
||||||
type: string
|
type: string
|
||||||
example: https://os.example.com
|
example: https://os.example.com
|
||||||
trustProxy:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
csrfProtection:
|
|
||||||
type: boolean
|
|
||||||
example: false
|
|
||||||
hideAvailable:
|
hideAvailable:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
@@ -171,6 +182,21 @@ components:
|
|||||||
defaultPermissions:
|
defaultPermissions:
|
||||||
type: number
|
type: number
|
||||||
example: 32
|
example: 32
|
||||||
|
enableSpecialEpisodes:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
NetworkSettings:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
csrfProtection:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
forceIpv4First:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
trustProxy:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
PlexLibrary:
|
PlexLibrary:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1258,6 +1284,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
webhookUrl:
|
webhookUrl:
|
||||||
type: string
|
type: string
|
||||||
|
webhookRoleId:
|
||||||
|
type: string
|
||||||
enableMentions:
|
enableMentions:
|
||||||
type: boolean
|
type: boolean
|
||||||
SlackSettings:
|
SlackSettings:
|
||||||
@@ -1319,6 +1347,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
chatId:
|
chatId:
|
||||||
type: string
|
type: string
|
||||||
|
messageThreadId:
|
||||||
|
type: string
|
||||||
sendSilently:
|
sendSilently:
|
||||||
type: boolean
|
type: boolean
|
||||||
PushbulletSettings:
|
PushbulletSettings:
|
||||||
@@ -1405,7 +1435,7 @@ components:
|
|||||||
example: no-reply@example.com
|
example: no-reply@example.com
|
||||||
senderName:
|
senderName:
|
||||||
type: string
|
type: string
|
||||||
example: Overseerr
|
example: Jellyseerr
|
||||||
smtpHost:
|
smtpHost:
|
||||||
type: string
|
type: string
|
||||||
example: 127.0.0.1
|
example: 127.0.0.1
|
||||||
@@ -1802,6 +1832,9 @@ components:
|
|||||||
telegramChatId:
|
telegramChatId:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
|
telegramMessageThreadId:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
telegramSendSilently:
|
telegramSendSilently:
|
||||||
type: boolean
|
type: boolean
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -1915,6 +1948,11 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
native_name:
|
native_name:
|
||||||
type: string
|
type: string
|
||||||
|
OverrideRule:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
type: apiKey
|
type: apiKey
|
||||||
@@ -1928,8 +1966,8 @@ components:
|
|||||||
paths:
|
paths:
|
||||||
/status:
|
/status:
|
||||||
get:
|
get:
|
||||||
summary: Get Overseerr status
|
summary: Get Jellyseerr status
|
||||||
description: Returns the current Overseerr status in a JSON object.
|
description: Returns the current Jellyseerr status in a JSON object.
|
||||||
security: []
|
security: []
|
||||||
tags:
|
tags:
|
||||||
- public
|
- public
|
||||||
@@ -1973,6 +2011,9 @@ paths:
|
|||||||
appDataPath:
|
appDataPath:
|
||||||
type: string
|
type: string
|
||||||
example: /app/config
|
example: /app/config
|
||||||
|
appDataPermissions:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
/settings/main:
|
/settings/main:
|
||||||
get:
|
get:
|
||||||
summary: Get main settings
|
summary: Get main settings
|
||||||
@@ -2004,6 +2045,37 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/MainSettings'
|
$ref: '#/components/schemas/MainSettings'
|
||||||
|
/settings/network:
|
||||||
|
get:
|
||||||
|
summary: Get network settings
|
||||||
|
description: Retrieves all network settings in a JSON object.
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/MainSettings'
|
||||||
|
post:
|
||||||
|
summary: Update network settings
|
||||||
|
description: Updates network settings with the provided values.
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NetworkSettings'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Values were sucessfully updated'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/NetworkSettings'
|
||||||
/settings/main/regenerate:
|
/settings/main/regenerate:
|
||||||
post:
|
post:
|
||||||
summary: Get main settings with newly-generated API key
|
summary: Get main settings with newly-generated API key
|
||||||
@@ -2775,6 +2847,15 @@ paths:
|
|||||||
imageCount:
|
imageCount:
|
||||||
type: number
|
type: number
|
||||||
example: 123
|
example: 123
|
||||||
|
avatar:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
size:
|
||||||
|
type: number
|
||||||
|
example: 123456
|
||||||
|
imageCount:
|
||||||
|
type: number
|
||||||
|
example: 123
|
||||||
apiCaches:
|
apiCaches:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -3586,6 +3667,8 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
|
serverType:
|
||||||
|
type: number
|
||||||
required:
|
required:
|
||||||
- username
|
- username
|
||||||
- password
|
- password
|
||||||
@@ -3724,6 +3807,16 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
enum: [created, updated, requests, displayname]
|
enum: [created, updated, requests, displayname]
|
||||||
default: created
|
default: created
|
||||||
|
- in: query
|
||||||
|
name: q
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: includeIds
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: A JSON array of all users
|
description: A JSON array of all users
|
||||||
@@ -3840,7 +3933,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
jellyfinIds:
|
jellyfinUserIds:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -3872,6 +3965,8 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
p256dh:
|
p256dh:
|
||||||
type: string
|
type: string
|
||||||
|
userAgent:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- endpoint
|
- endpoint
|
||||||
- auth
|
- auth
|
||||||
@@ -3879,6 +3974,88 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: Successfully registered push subscription
|
description: Successfully registered push subscription
|
||||||
|
/user/{userId}/pushSubscriptions:
|
||||||
|
get:
|
||||||
|
summary: Get all web push notification settings for a user
|
||||||
|
description: |
|
||||||
|
Returns all web push notification settings for a user in a JSON object.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User web push notification settings in JSON
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
endpoint:
|
||||||
|
type: string
|
||||||
|
p256dh:
|
||||||
|
type: string
|
||||||
|
auth:
|
||||||
|
type: string
|
||||||
|
userAgent:
|
||||||
|
type: string
|
||||||
|
/user/{userId}/pushSubscription/{key}:
|
||||||
|
get:
|
||||||
|
summary: Get web push notification settings for a user
|
||||||
|
description: |
|
||||||
|
Returns web push notification settings for a user in a JSON object.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
- in: path
|
||||||
|
name: key
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User web push notification settings in JSON
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
endpoint:
|
||||||
|
type: string
|
||||||
|
p256dh:
|
||||||
|
type: string
|
||||||
|
auth:
|
||||||
|
type: string
|
||||||
|
userAgent:
|
||||||
|
type: string
|
||||||
|
delete:
|
||||||
|
summary: Delete user push subscription by key
|
||||||
|
description: Deletes the user push subscription with the provided key.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
- in: path
|
||||||
|
name: key
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Successfully removed user push subscription
|
||||||
/user/{userId}:
|
/user/{userId}:
|
||||||
get:
|
get:
|
||||||
summary: Get user by ID
|
summary: Get user by ID
|
||||||
@@ -4040,6 +4217,109 @@ paths:
|
|||||||
restricted:
|
restricted:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
|
/blacklist:
|
||||||
|
get:
|
||||||
|
summary: Returns blacklisted items
|
||||||
|
description: Returns list of all blacklisted media
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: take
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
example: 25
|
||||||
|
- in: query
|
||||||
|
name: skip
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
example: 0
|
||||||
|
- in: query
|
||||||
|
name: search
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
example: dune
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Blacklisted items returned
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pageInfo:
|
||||||
|
$ref: '#/components/schemas/PageInfo'
|
||||||
|
results:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
example: 2024-04-21T01:55:44.000Z
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
example: 1
|
||||||
|
mediaType:
|
||||||
|
type: string
|
||||||
|
example: movie
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
example: Dune
|
||||||
|
tmdbId:
|
||||||
|
type: number
|
||||||
|
example: 438631
|
||||||
|
post:
|
||||||
|
summary: Add media to blacklist
|
||||||
|
tags:
|
||||||
|
- blacklist
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Blacklist'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: Item succesfully blacklisted
|
||||||
|
'412':
|
||||||
|
description: Item has already been blacklisted
|
||||||
|
/blacklist/{tmdbId}:
|
||||||
|
get:
|
||||||
|
summary: Get media from blacklist
|
||||||
|
tags:
|
||||||
|
- blacklist
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: tmdbId
|
||||||
|
description: tmdbId ID
|
||||||
|
required: true
|
||||||
|
example: '1'
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Blacklist details in JSON
|
||||||
|
delete:
|
||||||
|
summary: Remove media from blacklist
|
||||||
|
tags:
|
||||||
|
- blacklist
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: tmdbId
|
||||||
|
description: tmdbId ID
|
||||||
|
required: true
|
||||||
|
example: '1'
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Succesfully removed media item
|
||||||
/watchlist:
|
/watchlist:
|
||||||
post:
|
post:
|
||||||
summary: Add media to watchlist
|
summary: Add media to watchlist
|
||||||
@@ -4229,6 +4509,104 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: User password updated
|
description: User password updated
|
||||||
|
/user/{userId}/settings/linked-accounts/plex:
|
||||||
|
post:
|
||||||
|
summary: Link the provided Plex account to the current user
|
||||||
|
description: Logs in to Plex with the provided auth token, then links the associated Plex account with the user's account. Users can only link external accounts to their own account.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
authToken:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- authToken
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Linking account succeeded
|
||||||
|
'403':
|
||||||
|
description: Invalid credentials
|
||||||
|
'422':
|
||||||
|
description: Account already linked to a user
|
||||||
|
delete:
|
||||||
|
summary: Remove the linked Plex account for a user
|
||||||
|
description: Removes the linked Plex account for a specific user. Requires `MANAGE_USERS` permission if editing other users.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Unlinking account succeeded
|
||||||
|
'400':
|
||||||
|
description: Unlink request invalid
|
||||||
|
'404':
|
||||||
|
description: User does not exist
|
||||||
|
/user/{userId}/settings/linked-accounts/jellyfin:
|
||||||
|
post:
|
||||||
|
summary: Link the provided Jellyfin account to the current user
|
||||||
|
description: Logs in to Jellyfin with the provided credentials, then links the associated Jellyfin account with the user's account. Users can only link external accounts to their own account.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
example: 'Mr User'
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
example: 'supersecret'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Linking account succeeded
|
||||||
|
'403':
|
||||||
|
description: Invalid credentials
|
||||||
|
'422':
|
||||||
|
description: Account already linked to a user
|
||||||
|
delete:
|
||||||
|
summary: Remove the linked Jellyfin account for a user
|
||||||
|
description: Removes the linked Jellyfin account for a specific user. Requires `MANAGE_USERS` permission if editing other users.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Unlinking account succeeded
|
||||||
|
'400':
|
||||||
|
description: Unlink request invalid
|
||||||
|
'404':
|
||||||
|
description: User does not exist
|
||||||
/user/{userId}/settings/notifications:
|
/user/{userId}/settings/notifications:
|
||||||
get:
|
get:
|
||||||
summary: Get notification settings for a user
|
summary: Get notification settings for a user
|
||||||
@@ -4862,6 +5240,11 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: 8|9
|
example: 8|9
|
||||||
|
- in: query
|
||||||
|
name: status
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: 3|4
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Results
|
description: Results
|
||||||
@@ -5297,6 +5680,13 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
enum: [added, modified]
|
enum: [added, modified]
|
||||||
default: added
|
default: added
|
||||||
|
- in: query
|
||||||
|
name: sortDirection
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [asc, desc]
|
||||||
|
nullable: true
|
||||||
|
default: desc
|
||||||
- in: query
|
- in: query
|
||||||
name: requestedBy
|
name: requestedBy
|
||||||
schema:
|
schema:
|
||||||
@@ -5347,7 +5737,7 @@ paths:
|
|||||||
- type: array
|
- type: array
|
||||||
items:
|
items:
|
||||||
type: number
|
type: number
|
||||||
minimum: 1
|
minimum: 0
|
||||||
- type: string
|
- type: string
|
||||||
enum: [all]
|
enum: [all]
|
||||||
is4k:
|
is4k:
|
||||||
@@ -5453,7 +5843,7 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: number
|
type: number
|
||||||
minimum: 1
|
minimum: 0
|
||||||
is4k:
|
is4k:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
@@ -6817,6 +7207,74 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/WatchProviderDetails'
|
$ref: '#/components/schemas/WatchProviderDetails'
|
||||||
|
/overrideRule:
|
||||||
|
get:
|
||||||
|
summary: Get override rules
|
||||||
|
description: Returns a list of all override rules with their conditions and settings
|
||||||
|
tags:
|
||||||
|
- overriderule
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Override rules returned
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/OverrideRule'
|
||||||
|
post:
|
||||||
|
summary: Create override rule
|
||||||
|
description: Creates a new Override Rule from the request body.
|
||||||
|
tags:
|
||||||
|
- overriderule
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Values were successfully created'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/OverrideRule'
|
||||||
|
/overrideRule/{ruleId}:
|
||||||
|
put:
|
||||||
|
summary: Update override rule
|
||||||
|
description: Updates an Override Rule from the request body.
|
||||||
|
tags:
|
||||||
|
- overriderule
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: ruleId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Values were successfully updated'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/OverrideRule'
|
||||||
|
delete:
|
||||||
|
summary: Delete override rule by ID
|
||||||
|
description: Deletes the override rule with the provided ruleId.
|
||||||
|
tags:
|
||||||
|
- overriderule
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: ruleId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Override rule successfully deleted
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/OverrideRule'
|
||||||
security:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
- apiKey: []
|
- apiKey: []
|
||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
|||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||||
|
|||||||
@@ -4,16 +4,14 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
commitTag: process.env.COMMIT_TAG || 'local',
|
commitTag: process.env.COMMIT_TAG || 'local',
|
||||||
},
|
forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false',
|
||||||
publicRuntimeConfig: {
|
|
||||||
// Will be available on both server and client
|
|
||||||
JELLYFIN_TYPE: process.env.JELLYFIN_TYPE,
|
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{ hostname: 'gravatar.com' },
|
{ hostname: 'gravatar.com' },
|
||||||
{ hostname: 'image.tmdb.org' },
|
{ hostname: 'image.tmdb.org' },
|
||||||
{ hostname: '*', protocol: 'https' },
|
{ hostname: 'artworks.thetvdb.com' },
|
||||||
|
{ hostname: 'plex.tv' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
|
|||||||
55
package.json
55
package.json
@@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"postinstall": "node postinstall-win.js",
|
"postinstall": "node postinstall-win.js",
|
||||||
"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",
|
"dev": "nodemon -e ts --watch server --watch jellyseerr-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 && tsc-alias -p server/tsconfig.json",
|
"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": "pnpm build:next && pnpm build:server",
|
"build": "pnpm build:next && pnpm build:server",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dr.pogodin/csurf": "^1.14.1",
|
||||||
"@formatjs/intl-displaynames": "6.2.6",
|
"@formatjs/intl-displaynames": "6.2.6",
|
||||||
"@formatjs/intl-locale": "3.1.1",
|
"@formatjs/intl-locale": "3.1.1",
|
||||||
"@formatjs/intl-pluralrules": "5.1.10",
|
"@formatjs/intl-pluralrules": "5.1.10",
|
||||||
@@ -42,36 +43,37 @@
|
|||||||
"@supercharge/request-ip": "1.2.0",
|
"@supercharge/request-ip": "1.2.0",
|
||||||
"@svgr/webpack": "6.5.1",
|
"@svgr/webpack": "6.5.1",
|
||||||
"@tanem/react-nprogress": "5.0.30",
|
"@tanem/react-nprogress": "5.0.30",
|
||||||
|
"@types/wink-jaro-distance": "^2.0.2",
|
||||||
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"ace-builds": "1.15.2",
|
"ace-builds": "1.15.2",
|
||||||
"axios": "1.3.4",
|
|
||||||
"axios-rate-limit": "1.3.0",
|
|
||||||
"bcrypt": "5.1.0",
|
"bcrypt": "5.1.0",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
"connect-typeorm": "1.1.4",
|
"connect-typeorm": "1.1.4",
|
||||||
"cookie-parser": "1.4.6",
|
"cookie-parser": "1.4.7",
|
||||||
"copy-to-clipboard": "3.3.3",
|
"copy-to-clipboard": "3.3.3",
|
||||||
"country-flag-icons": "1.5.5",
|
"country-flag-icons": "1.5.5",
|
||||||
"cronstrue": "2.23.0",
|
"cronstrue": "2.23.0",
|
||||||
"csurf": "1.11.0",
|
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"dayjs": "1.11.7",
|
"dayjs": "1.11.7",
|
||||||
"email-templates": "9.0.0",
|
"email-templates": "12.0.1",
|
||||||
"email-validator": "2.0.4",
|
"email-validator": "2.0.4",
|
||||||
"express": "4.18.2",
|
"express": "4.21.2",
|
||||||
"express-openapi-validator": "4.13.8",
|
"express-openapi-validator": "4.13.8",
|
||||||
"express-rate-limit": "6.7.0",
|
"express-rate-limit": "6.7.0",
|
||||||
"express-session": "1.17.3",
|
"express-session": "1.17.3",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"gravatar-url": "3.1.0",
|
"gravatar-url": "3.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"next": "^14.2.4",
|
"mime": "3",
|
||||||
|
"next": "^14.2.25",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-gyp": "9.3.1",
|
"node-gyp": "9.3.1",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
"nodemailer": "6.9.1",
|
"nodemailer": "6.10.0",
|
||||||
"openpgp": "5.7.0",
|
"openpgp": "5.11.2",
|
||||||
|
"pg": "8.11.0",
|
||||||
"plex-api": "5.3.2",
|
"plex-api": "5.3.2",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-ace": "10.1.0",
|
"react-ace": "10.1.0",
|
||||||
"react-animate-height": "2.1.2",
|
"react-animate-height": "2.1.2",
|
||||||
@@ -85,23 +87,28 @@
|
|||||||
"react-spring": "9.7.1",
|
"react-spring": "9.7.1",
|
||||||
"react-tailwindcss-datepicker-sct": "1.3.4",
|
"react-tailwindcss-datepicker-sct": "1.3.4",
|
||||||
"react-toast-notifications": "2.5.1",
|
"react-toast-notifications": "2.5.1",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"react-truncate-markup": "5.1.2",
|
"react-truncate-markup": "5.1.2",
|
||||||
"react-use-clipboard": "1.0.9",
|
"react-use-clipboard": "1.0.9",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"secure-random-password": "0.2.3",
|
"secure-random-password": "0.2.3",
|
||||||
"semver": "7.3.8",
|
"semver": "7.7.1",
|
||||||
"sharp": "^0.33.4",
|
"sharp": "^0.33.4",
|
||||||
"sqlite3": "5.1.4",
|
"sqlite3": "5.1.7",
|
||||||
"swagger-ui-express": "4.6.2",
|
"swagger-ui-express": "4.6.2",
|
||||||
"swr": "2.2.5",
|
"swr": "2.2.5",
|
||||||
"typeorm": "0.3.12",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"typeorm": "0.3.11",
|
||||||
|
"undici": "^7.3.0",
|
||||||
|
"ua-parser-js": "^1.0.35",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
|
"wink-jaro-distance": "^2.0.0",
|
||||||
"winston": "3.8.2",
|
"winston": "3.8.2",
|
||||||
"winston-daily-rotate-file": "4.7.1",
|
"winston-daily-rotate-file": "4.7.1",
|
||||||
"xml2js": "0.4.23",
|
"xml2js": "0.4.23",
|
||||||
"yamljs": "0.3.0",
|
"yamljs": "0.3.0",
|
||||||
"yup": "0.32.11",
|
"yup": "0.32.11",
|
||||||
"zod": "3.20.6"
|
"zod": "3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "17.4.4",
|
"@commitlint/cli": "17.4.4",
|
||||||
@@ -111,8 +118,8 @@
|
|||||||
"@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.2",
|
||||||
"@tailwindcss/forms": "0.5.3",
|
"@tailwindcss/forms": "0.5.10",
|
||||||
"@tailwindcss/typography": "0.5.9",
|
"@tailwindcss/typography": "0.5.16",
|
||||||
"@types/bcrypt": "5.0.0",
|
"@types/bcrypt": "5.0.0",
|
||||||
"@types/cookie-parser": "1.4.3",
|
"@types/cookie-parser": "1.4.3",
|
||||||
"@types/country-flag-icons": "1.2.0",
|
"@types/country-flag-icons": "1.2.0",
|
||||||
@@ -121,7 +128,8 @@
|
|||||||
"@types/express": "4.17.17",
|
"@types/express": "4.17.17",
|
||||||
"@types/express-session": "1.17.6",
|
"@types/express-session": "1.17.6",
|
||||||
"@types/lodash": "4.14.191",
|
"@types/lodash": "4.14.191",
|
||||||
"@types/node": "17.0.36",
|
"@types/mime": "3",
|
||||||
|
"@types/node": "22.10.5",
|
||||||
"@types/node-schedule": "2.1.0",
|
"@types/node-schedule": "2.1.0",
|
||||||
"@types/nodemailer": "6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
@@ -140,7 +148,7 @@
|
|||||||
"commitizen": "4.3.0",
|
"commitizen": "4.3.0",
|
||||||
"copyfiles": "2.4.1",
|
"copyfiles": "2.4.1",
|
||||||
"cy-mobile-commands": "0.3.0",
|
"cy-mobile-commands": "0.3.0",
|
||||||
"cypress": "12.7.0",
|
"cypress": "14.1.0",
|
||||||
"cz-conventional-changelog": "3.3.0",
|
"cz-conventional-changelog": "3.3.0",
|
||||||
"eslint": "8.35.0",
|
"eslint": "8.35.0",
|
||||||
"eslint-config-next": "^14.2.4",
|
"eslint-config-next": "^14.2.4",
|
||||||
@@ -153,8 +161,8 @@
|
|||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"husky": "8.0.3",
|
"husky": "8.0.3",
|
||||||
"lint-staged": "13.1.2",
|
"lint-staged": "13.1.2",
|
||||||
"nodemon": "2.0.20",
|
"nodemon": "3.1.9",
|
||||||
"postcss": "8.4.21",
|
"postcss": "8.4.31",
|
||||||
"prettier": "2.8.4",
|
"prettier": "2.8.4",
|
||||||
"prettier-plugin-organize-imports": "3.2.2",
|
"prettier-plugin-organize-imports": "3.2.2",
|
||||||
"prettier-plugin-tailwindcss": "0.2.3",
|
"prettier-plugin-tailwindcss": "0.2.3",
|
||||||
@@ -167,7 +175,7 @@
|
|||||||
"typescript": "4.9.5"
|
"typescript": "4.9.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.0.0",
|
"node": "^22.0.0",
|
||||||
"pnpm": "^9.0.0"
|
"pnpm": "^9.0.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
@@ -233,7 +241,8 @@
|
|||||||
"COMMIT_TAG": "$GIT_SHA"
|
"COMMIT_TAG": "$GIT_SHA"
|
||||||
},
|
},
|
||||||
"imageNames": [
|
"imageNames": [
|
||||||
"fallenbagel/jellyseerr"
|
"fallenbagel/jellyseerr",
|
||||||
|
"ghcr.io/fallenbagel/jellyseerr"
|
||||||
],
|
],
|
||||||
"platforms": [
|
"platforms": [
|
||||||
"linux/amd64",
|
"linux/amd64",
|
||||||
|
|||||||
17222
pnpm-lock.yaml
generated
17222
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
21
public/sw.js
21
public/sw.js
@@ -3,7 +3,7 @@
|
|||||||
// previously cached resources to be updated from the network.
|
// previously cached resources to be updated from the network.
|
||||||
// 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 = 4;
|
||||||
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';
|
||||||
@@ -107,6 +107,25 @@ self.addEventListener('push', (event) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the badge with the amount of pending requests
|
||||||
|
// Only update the badge if the payload confirms they are the admin
|
||||||
|
if (
|
||||||
|
(payload.notificationType === 'MEDIA_APPROVED' ||
|
||||||
|
payload.notificationType === 'MEDIA_DECLINED') &&
|
||||||
|
payload.isAdmin
|
||||||
|
) {
|
||||||
|
if ('setAppBadge' in navigator) {
|
||||||
|
navigator.setAppBadge(payload.pendingRequestsCount);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.notificationType === 'MEDIA_PENDING') {
|
||||||
|
if ('setAppBadge' in navigator) {
|
||||||
|
navigator.setAppBadge(payload.pendingRequestsCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
event.waitUntil(self.registration.showNotification(payload.subject, options));
|
event.waitUntil(self.registration.showNotification(payload.subject, options));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import axios from 'axios';
|
import fs, { promises as fsp } from 'node:fs';
|
||||||
import fs, { promises as fsp } from 'fs';
|
import path from 'node:path';
|
||||||
import path from 'path';
|
import { Readable } from 'node:stream';
|
||||||
|
import type { ReadableStream } from 'node:stream/web';
|
||||||
import xml2js from 'xml2js';
|
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
|
||||||
@@ -161,13 +162,18 @@ class AnimeListMapping {
|
|||||||
label: 'Anime-List Sync',
|
label: 'Anime-List Sync',
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(MAPPING_URL, {
|
const response = await fetch(MAPPING_URL);
|
||||||
responseType: 'stream',
|
if (!response.ok) {
|
||||||
});
|
throw new Error(`Failed to fetch: ${response.statusText}`);
|
||||||
await new Promise<void>((resolve) => {
|
}
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
const writer = fs.createWriteStream(LOCAL_PATH);
|
const writer = fs.createWriteStream(LOCAL_PATH);
|
||||||
writer.on('finish', resolve);
|
writer.on('finish', resolve);
|
||||||
response.data.pipe(writer);
|
writer.on('error', reject);
|
||||||
|
if (!response.body) return reject();
|
||||||
|
Readable.fromWeb(response.body as ReadableStream<Uint8Array>).pipe(
|
||||||
|
writer
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`Failed to download Anime-List mapping: ${e.message}`);
|
throw new Error(`Failed to download Anime-List mapping: ${e.message}`);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import axios from 'axios';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import rateLimit from 'axios-rate-limit';
|
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||||
|
import rateLimit from '@server/utils/rateLimit';
|
||||||
import type NodeCache from 'node-cache';
|
import type NodeCache from 'node-cache';
|
||||||
|
|
||||||
// 5 minute default TTL (in seconds)
|
// 5 minute default TTL (in seconds)
|
||||||
@@ -12,93 +13,226 @@ const DEFAULT_ROLLING_BUFFER = 10000;
|
|||||||
interface ExternalAPIOptions {
|
interface ExternalAPIOptions {
|
||||||
nodeCache?: NodeCache;
|
nodeCache?: NodeCache;
|
||||||
headers?: Record<string, unknown>;
|
headers?: Record<string, unknown>;
|
||||||
rateLimit?: {
|
rateLimit?: RateLimitOptions;
|
||||||
maxRPS: number;
|
|
||||||
maxRequests: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExternalAPI {
|
class ExternalAPI {
|
||||||
protected axios: AxiosInstance;
|
protected fetch: typeof fetch;
|
||||||
|
protected params: Record<string, string>;
|
||||||
|
protected defaultHeaders: { [key: string]: string };
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private cache?: NodeCache;
|
private cache?: NodeCache;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, string> = {},
|
||||||
options: ExternalAPIOptions = {}
|
options: ExternalAPIOptions = {}
|
||||||
) {
|
) {
|
||||||
this.axios = axios.create({
|
if (options.rateLimit) {
|
||||||
baseURL: baseUrl,
|
this.fetch = rateLimit(fetch, options.rateLimit);
|
||||||
params,
|
} else {
|
||||||
headers: {
|
this.fetch = fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
this.defaultHeaders = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
...((url.username || url.password) && {
|
||||||
|
Authorization: `Basic ${Buffer.from(
|
||||||
|
`${url.username}:${url.password}`
|
||||||
|
).toString('base64')}`,
|
||||||
|
}),
|
||||||
|
...(settings.main.mediaServerType === MediaServerType.EMBY && {
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
}),
|
||||||
...options.headers,
|
...options.headers,
|
||||||
},
|
};
|
||||||
});
|
|
||||||
|
|
||||||
if (options.rateLimit) {
|
if (url.username || url.password) {
|
||||||
this.axios = rateLimit(this.axios, {
|
url.username = '';
|
||||||
maxRequests: options.rateLimit.maxRequests,
|
url.password = '';
|
||||||
maxRPS: options.rateLimit.maxRPS,
|
baseUrl = url.toString();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
|
this.params = params;
|
||||||
this.cache = options.nodeCache;
|
this.cache = options.nodeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async get<T>(
|
protected async get<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
config?: AxiosRequestConfig,
|
params?: Record<string, string>,
|
||||||
ttl?: number
|
ttl?: number,
|
||||||
|
config?: RequestInit
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||||
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
|
...this.params,
|
||||||
|
...params,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
if (cachedItem) {
|
if (cachedItem) {
|
||||||
return cachedItem;
|
return cachedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.axios.get<T>(endpoint, config);
|
const url = this.formatUrl(endpoint, params);
|
||||||
|
const response = await this.fetch(url, {
|
||||||
|
...config,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||||
|
{
|
||||||
|
cause: response,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const data = await this.getDataFromResponse(response);
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache && ttl !== 0) {
|
||||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async post<T>(
|
protected async post<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: Record<string, unknown>,
|
data?: Record<string, unknown>,
|
||||||
config?: AxiosRequestConfig,
|
params?: Record<string, string>,
|
||||||
ttl?: number
|
ttl?: number,
|
||||||
|
config?: RequestInit
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
config: config?.params,
|
config: { ...this.params, ...params },
|
||||||
|
headers,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
if (cachedItem) {
|
if (cachedItem) {
|
||||||
return cachedItem;
|
return cachedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.axios.post<T>(endpoint, data, config);
|
const url = this.formatUrl(endpoint, params);
|
||||||
|
const response = await this.fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
...config,
|
||||||
|
headers,
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||||
|
{
|
||||||
|
cause: response,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const resData = await this.getDataFromResponse(response);
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache && ttl !== 0) {
|
||||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data;
|
return resData;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async put<T>(
|
||||||
|
endpoint: string,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
params?: Record<string, string>,
|
||||||
|
ttl?: number,
|
||||||
|
config?: RequestInit
|
||||||
|
): Promise<T> {
|
||||||
|
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||||
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
|
config: { ...this.params, ...params },
|
||||||
|
data,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
|
if (cachedItem) {
|
||||||
|
return cachedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this.formatUrl(endpoint, params);
|
||||||
|
const response = await this.fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
...config,
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||||
|
{
|
||||||
|
cause: response,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const resData = await this.getDataFromResponse(response);
|
||||||
|
|
||||||
|
if (this.cache && ttl !== 0) {
|
||||||
|
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resData;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async delete<T>(
|
||||||
|
endpoint: string,
|
||||||
|
params?: Record<string, string>,
|
||||||
|
config?: RequestInit
|
||||||
|
): Promise<T> {
|
||||||
|
const url = this.formatUrl(endpoint, params);
|
||||||
|
const response = await this.fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
...config,
|
||||||
|
headers: {
|
||||||
|
...this.defaultHeaders,
|
||||||
|
...config?.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||||
|
{
|
||||||
|
cause: response,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const data = await this.getDataFromResponse(response);
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getRolling<T>(
|
protected async getRolling<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
config?: AxiosRequestConfig,
|
params?: Record<string, string>,
|
||||||
ttl?: number
|
ttl?: number,
|
||||||
|
config?: RequestInit,
|
||||||
|
overwriteBaseUrl?: string
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, config?.params);
|
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||||
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
|
...this.params,
|
||||||
|
...params,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||||
|
|
||||||
if (cachedItem) {
|
if (cachedItem) {
|
||||||
@@ -109,31 +243,114 @@ class ExternalAPI {
|
|||||||
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
|
keyTtl - (ttl ?? DEFAULT_TTL) * 1000 <
|
||||||
Date.now() - DEFAULT_ROLLING_BUFFER
|
Date.now() - DEFAULT_ROLLING_BUFFER
|
||||||
) {
|
) {
|
||||||
this.axios.get<T>(endpoint, config).then((response) => {
|
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||||
this.cache?.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
this.fetch(url, {
|
||||||
|
...config,
|
||||||
|
headers,
|
||||||
|
}).then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`${response.status} ${response.statusText}${
|
||||||
|
text ? ': ' + text : ''
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
cause: response,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const data = await this.getDataFromResponse(response);
|
||||||
|
this.cache?.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return cachedItem;
|
return cachedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.axios.get<T>(endpoint, config);
|
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||||
|
const response = await this.fetch(url, {
|
||||||
|
...config,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`${response.status} ${response.statusText}${text ? ': ' + text : ''}`,
|
||||||
|
{
|
||||||
|
cause: response,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const data = await this.getDataFromResponse(response);
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected removeCache(endpoint: string, options?: Record<string, unknown>) {
|
||||||
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
|
...this.params,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
this.cache?.del(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatUrl(
|
||||||
|
endpoint: string,
|
||||||
|
params?: Record<string, string>,
|
||||||
|
overwriteBaseUrl?: string
|
||||||
|
): string {
|
||||||
|
const baseUrl = overwriteBaseUrl || this.baseUrl;
|
||||||
|
const href =
|
||||||
|
baseUrl +
|
||||||
|
(baseUrl.endsWith('/') ? '' : '/') +
|
||||||
|
(endpoint.startsWith('/') ? endpoint.slice(1) : endpoint);
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
...this.params,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
href +
|
||||||
|
(searchParams.toString().length
|
||||||
|
? '?' + searchParams.toString()
|
||||||
|
: searchParams.toString())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private serializeCacheKey(
|
private serializeCacheKey(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
params?: Record<string, unknown>
|
options?: Record<string, unknown>
|
||||||
) {
|
) {
|
||||||
if (!params) {
|
if (!options) {
|
||||||
return `${this.baseUrl}${endpoint}`;
|
return `${this.baseUrl}${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
return `${this.baseUrl}${endpoint}${JSON.stringify(options)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDataFromResponse(response: Response) {
|
||||||
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
if (contentType?.includes('application/json')) {
|
||||||
|
return await response.json();
|
||||||
|
} else if (
|
||||||
|
contentType?.includes('application/xml') ||
|
||||||
|
contentType?.includes('text/html') ||
|
||||||
|
contentType?.includes('text/plain')
|
||||||
|
) {
|
||||||
|
return await response.text();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return await response.json();
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
return await response.blob();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import ExternalAPI from './externalapi';
|
|
||||||
|
|
||||||
interface GitHubRelease {
|
interface GitHubRelease {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -67,16 +67,12 @@ class GithubAPI extends ExternalAPI {
|
|||||||
'https://api.github.com',
|
'https://api.github.com',
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
nodeCache: cacheManager.getCache('github').data,
|
nodeCache: cacheManager.getCache('github').data,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getOverseerrReleases({
|
public async getJellyseerrReleases({
|
||||||
take = 20,
|
take = 20,
|
||||||
}: {
|
}: {
|
||||||
take?: number;
|
take?: number;
|
||||||
@@ -85,23 +81,21 @@ class GithubAPI extends ExternalAPI {
|
|||||||
const data = await this.get<GitHubRelease[]>(
|
const data = await this.get<GitHubRelease[]>(
|
||||||
'/repos/fallenbagel/jellyseerr/releases',
|
'/repos/fallenbagel/jellyseerr/releases',
|
||||||
{
|
{
|
||||||
params: {
|
per_page: take.toString(),
|
||||||
per_page: take,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Failed to retrieve GitHub releases. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
|
"Failed to retrieve GitHub releases. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
|
||||||
{ label: 'GitHub API', errorMessage: e.message }
|
{ label: 'GitHub API', errorMessage: e.message }
|
||||||
);
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getOverseerrCommits({
|
public async getJellyseerrCommits({
|
||||||
take = 20,
|
take = 20,
|
||||||
branch = 'develop',
|
branch = 'develop',
|
||||||
}: {
|
}: {
|
||||||
@@ -112,17 +106,15 @@ class GithubAPI extends ExternalAPI {
|
|||||||
const data = await this.get<GithubCommit[]>(
|
const data = await this.get<GithubCommit[]>(
|
||||||
'/repos/fallenbagel/jellyseerr/commits',
|
'/repos/fallenbagel/jellyseerr/commits',
|
||||||
{
|
{
|
||||||
params: {
|
per_page: take.toString(),
|
||||||
per_page: take,
|
|
||||||
branch,
|
branch,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Overseerr can't check if it's on the latest version.",
|
"Failed to retrieve GitHub commits. This may be an issue on GitHub's end. Jellyseerr can't check if it's on the latest version.",
|
||||||
{ label: 'GitHub API', errorMessage: e.message }
|
{ label: 'GitHub API', errorMessage: e.message }
|
||||||
);
|
);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ExternalAPI from '@server/api/externalapi';
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
import { ApiErrorCode } from '@server/constants/error';
|
import { ApiErrorCode } from '@server/constants/error';
|
||||||
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import availabilitySync from '@server/lib/availabilitySync';
|
import availabilitySync from '@server/lib/availabilitySync';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { ApiError } from '@server/types/error';
|
import { ApiError } from '@server/types/error';
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
@@ -92,17 +94,34 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
|||||||
DateCreated?: string;
|
DateCreated?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class JellyfinAPI extends ExternalAPI {
|
export interface JellyfinItemsReponse {
|
||||||
private authToken?: string;
|
Items: JellyfinLibraryItemExtended[];
|
||||||
private userId?: string;
|
TotalRecordCount: number;
|
||||||
private jellyfinHost: string;
|
StartIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class JellyfinAPI extends ExternalAPI {
|
||||||
|
private userId?: string;
|
||||||
|
private mediaServerType: MediaServerType;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
jellyfinHost: string,
|
||||||
|
authToken?: string | null,
|
||||||
|
deviceId?: string | null
|
||||||
|
) {
|
||||||
|
const settings = getSettings();
|
||||||
|
const safeDeviceId =
|
||||||
|
deviceId && deviceId.length > 0
|
||||||
|
? deviceId
|
||||||
|
: Buffer.from(`BOT_jellyseerr_fallback_${Date.now()}`).toString(
|
||||||
|
'base64'
|
||||||
|
);
|
||||||
|
|
||||||
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
|
||||||
let authHeaderVal: string;
|
let authHeaderVal: string;
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
|
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${safeDeviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
|
||||||
} else {
|
} else {
|
||||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}"`;
|
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${safeDeviceId}", Version="${getAppVersion()}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
super(
|
super(
|
||||||
@@ -111,14 +130,11 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'X-Emby-Authorization': authHeaderVal,
|
'X-Emby-Authorization': authHeaderVal,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.jellyfinHost = jellyfinHost;
|
this.mediaServerType = settings.main.mediaServerType;
|
||||||
this.authToken = authToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(
|
public async login(
|
||||||
@@ -127,7 +143,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
ClientIP?: string
|
ClientIP?: string
|
||||||
): Promise<JellyfinLoginResponse> {
|
): Promise<JellyfinLoginResponse> {
|
||||||
const authenticate = async (useHeaders: boolean) => {
|
const authenticate = async (useHeaders: boolean) => {
|
||||||
const headers =
|
const headers: { [key: string]: string } =
|
||||||
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
|
useHeaders && ClientIP ? { 'X-Forwarded-For': ClientIP } : {};
|
||||||
|
|
||||||
return this.post<JellyfinLoginResponse>(
|
return this.post<JellyfinLoginResponse>(
|
||||||
@@ -136,6 +152,8 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
Username,
|
Username,
|
||||||
Pw: Password,
|
Pw: Password,
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
{ headers }
|
{ headers }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -143,39 +161,38 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
try {
|
try {
|
||||||
return await authenticate(true);
|
return await authenticate(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug(`Failed to authenticate with headers: ${e.message}`, {
|
logger.debug('Failed to authenticate with headers', {
|
||||||
label: 'Jellyfin API',
|
label: 'Jellyfin API',
|
||||||
|
error: e.cause.message ?? e.cause.statusText,
|
||||||
ip: ClientIP,
|
ip: ClientIP,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!e.cause.status) {
|
||||||
|
throw new ApiError(404, ApiErrorCode.InvalidUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.cause.status === 401) {
|
||||||
|
throw new ApiError(e.cause.status, ApiErrorCode.InvalidCredentials);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await authenticate(false);
|
return await authenticate(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const status = e.response?.status;
|
if (e.cause.status === 401) {
|
||||||
|
throw new ApiError(e.cause.status, ApiErrorCode.InvalidCredentials);
|
||||||
const networkErrorCodes = new Set([
|
|
||||||
'ECONNREFUSED',
|
|
||||||
'EHOSTUNREACH',
|
|
||||||
'ENOTFOUND',
|
|
||||||
'ETIMEDOUT',
|
|
||||||
'ECONNRESET',
|
|
||||||
'EADDRINUSE',
|
|
||||||
'ENETDOWN',
|
|
||||||
'ENETUNREACH',
|
|
||||||
'EPIPE',
|
|
||||||
'ECONNABORTED',
|
|
||||||
'EPROTO',
|
|
||||||
'EHOSTDOWN',
|
|
||||||
'EAI_AGAIN',
|
|
||||||
'ERR_INVALID_URL',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (networkErrorCodes.has(e.code) || status === 404) {
|
|
||||||
throw new ApiError(status, ApiErrorCode.InvalidUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ApiError(status, ApiErrorCode.InvalidCredentials);
|
logger.error(
|
||||||
|
'Something went wrong while authenticating with the Jellyfin server',
|
||||||
|
{
|
||||||
|
label: 'Jellyfin API',
|
||||||
|
error: e.cause.message ?? e.cause.statusText,
|
||||||
|
ip: ClientIP,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new ApiError(e.cause.status, ApiErrorCode.Unknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +207,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
|
|
||||||
return systemInfoResponse;
|
return systemInfoResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,11 +220,11 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
return serverResponse.ServerName;
|
return serverResponse.ServerName;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the server name from the Jellyfin server: ${e.message}`,
|
'Something went wrong while getting the server name from the Jellyfin server',
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.Unknown);
|
throw new ApiError(e.cause?.status, ApiErrorCode.Unknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,11 +235,11 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
return { users: userReponse };
|
return { users: userReponse };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
'Something went wrong while getting the account from the Jellyfin server',
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,11 +251,11 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
return userReponse;
|
return userReponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
'Something went wrong while getting the account from the Jellyfin server',
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,8 +275,11 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
return this.mapLibraries(mediaFolderResponse.Items);
|
return this.mapLibraries(mediaFolderResponse.Items);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting libraries from the Jellyfin server: ${e.message}`,
|
'Something went wrong while getting libraries from the Jellyfin server',
|
||||||
{ label: 'Jellyfin API' }
|
{
|
||||||
|
label: 'Jellyfin API',
|
||||||
|
error: e.cause.message ?? e.cause.statusText,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
@@ -295,37 +315,56 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
|
|
||||||
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const libraryItemsResponse = await this.get<any>(
|
const libraryItemsResponse = await this.get<any>(`/Items`, {
|
||||||
`/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&IncludeItemTypes=Series,Movie,Others&Recursive=true&StartIndex=0&ParentId=${id}&collapseBoxSetItems=false`
|
SortBy: 'SortName',
|
||||||
);
|
SortOrder: 'Ascending',
|
||||||
|
IncludeItemTypes: 'Series,Movie,Others',
|
||||||
|
Recursive: 'true',
|
||||||
|
StartIndex: '0',
|
||||||
|
ParentId: id,
|
||||||
|
collapseBoxSetItems: 'false',
|
||||||
|
});
|
||||||
|
|
||||||
return libraryItemsResponse.Items.filter(
|
return libraryItemsResponse.Items.filter(
|
||||||
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
(item: JellyfinLibraryItem) => item.LocationType !== 'Virtual'
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
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',
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const itemResponse = await this.get<any>(
|
const endpoint =
|
||||||
`/Users/${this.userId}/Items/Latest?Limit=12&ParentId=${id}`
|
this.mediaServerType === MediaServerType.JELLYFIN
|
||||||
);
|
? `/Items/Latest`
|
||||||
|
: `/Users/${this.userId}/Items/Latest`;
|
||||||
|
|
||||||
|
const baseParams = {
|
||||||
|
Limit: '12',
|
||||||
|
ParentId: id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const params =
|
||||||
|
this.mediaServerType === MediaServerType.JELLYFIN
|
||||||
|
? { ...baseParams, userId: this.userId ?? `Me` }
|
||||||
|
: baseParams;
|
||||||
|
|
||||||
|
const itemResponse = await this.get<any>(endpoint, params);
|
||||||
|
|
||||||
return itemResponse;
|
return itemResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
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',
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,23 +372,24 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
id: string
|
id: string
|
||||||
): Promise<JellyfinLibraryItemExtended | undefined> {
|
): Promise<JellyfinLibraryItemExtended | undefined> {
|
||||||
try {
|
try {
|
||||||
const itemResponse = await this.get<any>(
|
const itemResponse = await this.get<JellyfinItemsReponse>(`/Items`, {
|
||||||
`/Users/${this.userId}/Items/${id}`
|
ids: id,
|
||||||
);
|
fields: 'ProviderIds,MediaSources,Width,Height,IsHD,DateCreated',
|
||||||
|
});
|
||||||
|
|
||||||
return itemResponse;
|
return itemResponse.Items?.[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (availabilitySync.running) {
|
if (availabilitySync.running) {
|
||||||
if (e.response && e.response.status === 500) {
|
if (e.cause?.status === 500) {
|
||||||
return undefined;
|
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',
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
);
|
);
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,11 +400,11 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
return seasonResponse.Items;
|
return seasonResponse.Items;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the list of seasons from the Jellyfin server: ${e.message}`,
|
'Something went wrong while getting the list of seasons from the Jellyfin server',
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +414,10 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
): Promise<JellyfinLibraryItem[]> {
|
): Promise<JellyfinLibraryItem[]> {
|
||||||
try {
|
try {
|
||||||
const episodeResponse = await this.get<any>(
|
const episodeResponse = await this.get<any>(
|
||||||
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
`/Shows/${seriesID}/Episodes`,
|
||||||
|
{
|
||||||
|
seasonId: seasonID,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return episodeResponse.Items.filter(
|
return episodeResponse.Items.filter(
|
||||||
@@ -382,8 +425,25 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the list of episodes from the Jellyfin server: ${e.message}`,
|
'Something went wrong while getting the list of episodes from the Jellyfin server',
|
||||||
{ label: 'Jellyfin API' }
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createApiToken(appName: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
await this.post(`/Auth/Keys?App=${appName}`);
|
||||||
|
const apiKeys = await this.get<any>(`/Auth/Keys`);
|
||||||
|
return apiKeys.Items.reverse().find(
|
||||||
|
(item: any) => item.AppName === appName
|
||||||
|
).AccessToken;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
'Something went wrong while creating an API key from the Jellyfin server',
|
||||||
|
{ label: 'Jellyfin API', error: e.cause.message ?? e.cause.statusText }
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.response?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class PlexAPI {
|
|||||||
plexSettings,
|
plexSettings,
|
||||||
timeout,
|
timeout,
|
||||||
}: {
|
}: {
|
||||||
plexToken?: string;
|
plexToken?: string | null;
|
||||||
plexSettings?: PlexSettings;
|
plexSettings?: PlexSettings;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}) {
|
}) {
|
||||||
@@ -107,7 +107,7 @@ class PlexAPI {
|
|||||||
port: settingsPlex.port,
|
port: settingsPlex.port,
|
||||||
https: settingsPlex.useSsl,
|
https: settingsPlex.useSsl,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
token: plexToken,
|
token: plexToken ?? undefined,
|
||||||
authenticator: {
|
authenticator: {
|
||||||
authenticate: (
|
authenticate: (
|
||||||
_plexApi,
|
_plexApi,
|
||||||
@@ -124,9 +124,9 @@ class PlexAPI {
|
|||||||
// },
|
// },
|
||||||
options: {
|
options: {
|
||||||
identifier: settings.clientId,
|
identifier: settings.clientId,
|
||||||
product: 'Overseerr',
|
product: 'Jellyseerr',
|
||||||
deviceName: 'Overseerr',
|
deviceName: 'Jellyseerr',
|
||||||
platform: 'Overseerr',
|
platform: 'Jellyseerr',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ class PlexAPI {
|
|||||||
settings.plex.libraries = [];
|
settings.plex.libraries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.save();
|
await settings.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLibraryContents(
|
public async getLibraryContents(
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import xml2js from 'xml2js';
|
import xml2js from 'xml2js';
|
||||||
import ExternalAPI from './externalapi';
|
|
||||||
|
|
||||||
interface PlexAccountResponse {
|
interface PlexAccountResponse {
|
||||||
user: PlexUser;
|
user: PlexUser;
|
||||||
@@ -127,6 +128,11 @@ export interface PlexWatchlistItem {
|
|||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlexWatchlistCache {
|
||||||
|
etag: string;
|
||||||
|
response: WatchlistResponse;
|
||||||
|
}
|
||||||
|
|
||||||
class PlexTvAPI extends ExternalAPI {
|
class PlexTvAPI extends ExternalAPI {
|
||||||
private authToken: string;
|
private authToken: string;
|
||||||
|
|
||||||
@@ -137,8 +143,6 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'X-Plex-Token': authToken,
|
'X-Plex-Token': authToken,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
},
|
||||||
nodeCache: cacheManager.getCache('plextv').data,
|
nodeCache: cacheManager.getCache('plextv').data,
|
||||||
}
|
}
|
||||||
@@ -149,15 +153,11 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
|
|
||||||
public async getDevices(): Promise<PlexDevice[]> {
|
public async getDevices(): Promise<PlexDevice[]> {
|
||||||
try {
|
try {
|
||||||
const devicesResp = await this.axios.get(
|
const devicesResp = await this.get('/api/resources', {
|
||||||
'/api/resources?includeHttps=1',
|
includeHttps: '1',
|
||||||
{
|
});
|
||||||
transformResponse: [],
|
|
||||||
responseType: 'text',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const parsedXml = await xml2js.parseStringPromise(
|
const parsedXml = await xml2js.parseStringPromise(
|
||||||
devicesResp.data as DeviceResponse
|
devicesResp as DeviceResponse
|
||||||
);
|
);
|
||||||
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
|
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
|
||||||
name: pxml.$.name,
|
name: pxml.$.name,
|
||||||
@@ -205,11 +205,11 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
|
|
||||||
public async getUser(): Promise<PlexUser> {
|
public async getUser(): Promise<PlexUser> {
|
||||||
try {
|
try {
|
||||||
const account = await this.axios.get<PlexAccountResponse>(
|
const account = await this.get<PlexAccountResponse>(
|
||||||
'/users/account.json'
|
'/users/account.json'
|
||||||
);
|
);
|
||||||
|
|
||||||
return account.data.user;
|
return account.user;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the account from plex.tv: ${e.message}`,
|
`Something went wrong while getting the account from plex.tv: ${e.message}`,
|
||||||
@@ -249,13 +249,10 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getUsers(): Promise<UsersResponse> {
|
public async getUsers(): Promise<UsersResponse> {
|
||||||
const response = await this.axios.get('/api/users', {
|
const data = await this.get('/api/users');
|
||||||
transformResponse: [],
|
|
||||||
responseType: 'text',
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsedXml = (await xml2js.parseStringPromise(
|
const parsedXml = (await xml2js.parseStringPromise(
|
||||||
response.data
|
data as string
|
||||||
)) as UsersResponse;
|
)) as UsersResponse;
|
||||||
return parsedXml;
|
return parsedXml;
|
||||||
}
|
}
|
||||||
@@ -270,25 +267,50 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
items: PlexWatchlistItem[];
|
items: PlexWatchlistItem[];
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<WatchlistResponse>(
|
const watchlistCache = cacheManager.getCache('plexwatchlist');
|
||||||
'/library/sections/watchlist/all',
|
let cachedWatchlist = watchlistCache.data.get<PlexWatchlistCache>(
|
||||||
{
|
this.authToken
|
||||||
params: {
|
|
||||||
'X-Plex-Container-Start': offset,
|
|
||||||
'X-Plex-Container-Size': size,
|
|
||||||
},
|
|
||||||
baseURL: 'https://metadata.provider.plex.tv',
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
'X-Plex-Container-Start': offset.toString(),
|
||||||
|
'X-Plex-Container-Size': size.toString(),
|
||||||
|
});
|
||||||
|
const response = await this.fetch(
|
||||||
|
`https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...this.defaultHeaders,
|
||||||
|
...(cachedWatchlist?.etag
|
||||||
|
? { 'If-None-Match': cachedWatchlist.etag }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data = (await response.json()) as WatchlistResponse;
|
||||||
|
|
||||||
|
// If we don't recieve HTTP 304, the watchlist has been updated and we need to update the cache.
|
||||||
|
if (response.status >= 200 && response.status <= 299) {
|
||||||
|
cachedWatchlist = {
|
||||||
|
etag: response.headers.get('etag') ?? '',
|
||||||
|
response: data,
|
||||||
|
};
|
||||||
|
|
||||||
|
watchlistCache.data.set<PlexWatchlistCache>(
|
||||||
|
this.authToken,
|
||||||
|
cachedWatchlist
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const watchlistDetails = await Promise.all(
|
const watchlistDetails = await Promise.all(
|
||||||
(response.data.MediaContainer.Metadata ?? []).map(
|
(cachedWatchlist?.response.MediaContainer.Metadata ?? []).map(
|
||||||
async (watchlistItem) => {
|
async (watchlistItem) => {
|
||||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||||
{
|
{},
|
||||||
baseURL: 'https://metadata.provider.plex.tv',
|
undefined,
|
||||||
}
|
{},
|
||||||
|
'https://metadata.provider.plex.tv'
|
||||||
);
|
);
|
||||||
|
|
||||||
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||||
@@ -320,7 +342,7 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
return {
|
return {
|
||||||
offset,
|
offset,
|
||||||
size,
|
size,
|
||||||
totalSize: response.data.MediaContainer.totalSize,
|
totalSize: cachedWatchlist?.response.MediaContainer.totalSize ?? 0,
|
||||||
items: filteredList,
|
items: filteredList,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -336,6 +358,29 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async pingToken() {
|
||||||
|
try {
|
||||||
|
const data: { pong: unknown } = await this.get(
|
||||||
|
'/api/v2/ping',
|
||||||
|
{},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-Plex-Client-Identifier': randomUUID(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!data?.pong) {
|
||||||
|
throw new Error('No pong response');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to ping token', {
|
||||||
|
label: 'Plex Refresh Token',
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PlexTvAPI;
|
export default PlexTvAPI;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import ExternalAPI from './externalapi';
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
|
|
||||||
interface PushoverSoundsResponse {
|
interface PushoverSoundsResponse {
|
||||||
sounds: {
|
sounds: {
|
||||||
@@ -26,24 +26,13 @@ export const mapSounds = (sounds: {
|
|||||||
|
|
||||||
class PushoverAPI extends ExternalAPI {
|
class PushoverAPI extends ExternalAPI {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super('https://api.pushover.net/1');
|
||||||
'https://api.pushover.net/1',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSounds(appToken: string): Promise<PushoverSound[]> {
|
public async getSounds(appToken: string): Promise<PushoverSound[]> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
|
const data = await this.get<PushoverSoundsResponse>('/sounds.json', {
|
||||||
params: {
|
|
||||||
token: appToken,
|
token: appToken,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapSounds(data.sounds);
|
return mapSounds(data.sounds);
|
||||||
|
|||||||
@@ -155,13 +155,13 @@ export interface IMDBRating {
|
|||||||
*/
|
*/
|
||||||
class IMDBRadarrProxy extends ExternalAPI {
|
class IMDBRadarrProxy extends ExternalAPI {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('https://api.radarr.video/v1', {
|
super(
|
||||||
headers: {
|
'https://api.radarr.video/v1',
|
||||||
'Content-Type': 'application/json',
|
{},
|
||||||
Accept: 'application/json',
|
{
|
||||||
},
|
|
||||||
nodeCache: cacheManager.getCache('imdb').data,
|
nodeCache: cacheManager.getCache('imdb').data,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,7 +175,11 @@ class IMDBRadarrProxy extends ExternalAPI {
|
|||||||
`/movie/imdb/${IMDBid}`
|
`/movie/imdb/${IMDBid}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data?.length || data[0].ImdbId !== IMDBid) {
|
if (
|
||||||
|
!data?.length ||
|
||||||
|
data[0].ImdbId !== IMDBid ||
|
||||||
|
!data[0].MovieRatings.Imdb
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import ExternalAPI from '@server/api/externalapi';
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import jaro from 'wink-jaro-distance';
|
||||||
|
|
||||||
interface RTAlgoliaSearchResponse {
|
interface RTAlgoliaSearchResponse {
|
||||||
results: {
|
results: {
|
||||||
@@ -15,7 +16,7 @@ interface RTAlgoliaHit {
|
|||||||
tmsId: string;
|
tmsId: string;
|
||||||
type: string;
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
titles: string[];
|
titles?: string[];
|
||||||
description: string;
|
description: string;
|
||||||
releaseYear: number;
|
releaseYear: number;
|
||||||
rating: string;
|
rating: string;
|
||||||
@@ -24,9 +25,9 @@ interface RTAlgoliaHit {
|
|||||||
isEmsSearchable: boolean;
|
isEmsSearchable: boolean;
|
||||||
rtId: number;
|
rtId: number;
|
||||||
vanity: string;
|
vanity: string;
|
||||||
aka: string[];
|
aka?: string[];
|
||||||
posterImageUrl: string;
|
posterImageUrl: string;
|
||||||
rottenTomatoes: {
|
rottenTomatoes?: {
|
||||||
audienceScore: number;
|
audienceScore: number;
|
||||||
criticsIconUrl: string;
|
criticsIconUrl: string;
|
||||||
wantToSeeCount: number;
|
wantToSeeCount: number;
|
||||||
@@ -47,6 +48,47 @@ export interface RTRating {
|
|||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tunables
|
||||||
|
const INEXACT_TITLE_FACTOR = 0.25;
|
||||||
|
const ALTERNATE_TITLE_FACTOR = 0.8;
|
||||||
|
const PER_YEAR_PENALTY = 0.4;
|
||||||
|
const MINIMUM_SCORE = 0.175;
|
||||||
|
|
||||||
|
// Normalization for title comparisons.
|
||||||
|
// Lowercase and strip non-alphanumeric (unicode-aware).
|
||||||
|
const norm = (s: string): string =>
|
||||||
|
s.toLowerCase().replace(/[^\p{L}\p{N} ]/gu, '');
|
||||||
|
|
||||||
|
// Title similarity. 1 if exact, quarter-jaro otherwise.
|
||||||
|
const similarity = (a: string, b: string): number =>
|
||||||
|
a === b ? 1 : jaro(a, b).similarity * INEXACT_TITLE_FACTOR;
|
||||||
|
|
||||||
|
// Gets the best similarity score between the searched title and all alternate
|
||||||
|
// titles of the search result. Non-main titles are penalized.
|
||||||
|
const t_score = ({ title, titles, aka }: RTAlgoliaHit, s: string): number => {
|
||||||
|
const f = (t: string, i: number) =>
|
||||||
|
similarity(norm(t), norm(s)) * (i ? ALTERNATE_TITLE_FACTOR : 1);
|
||||||
|
return Math.max(...[title].concat(aka || [], titles || []).map(f));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Year difference to score: 0 -> 1.0, 1 -> 0.6, 2 -> 0.2, 3+ -> 0.0
|
||||||
|
const y_score = (r: RTAlgoliaHit, y?: number): number =>
|
||||||
|
y ? Math.max(0, 1 - Math.abs(r.releaseYear - y) * PER_YEAR_PENALTY) : 1;
|
||||||
|
|
||||||
|
// Cut score in half if result has no ratings.
|
||||||
|
const extra_score = (r: RTAlgoliaHit): number => (r.rottenTomatoes ? 1 : 0.5);
|
||||||
|
|
||||||
|
// Score search result as product of all subscores
|
||||||
|
const score = (r: RTAlgoliaHit, name: string, year?: number): number =>
|
||||||
|
t_score(r, name) * y_score(r, year) * extra_score(r);
|
||||||
|
|
||||||
|
// Score each search result and return the highest scoring result, if any
|
||||||
|
const best = (rs: RTAlgoliaHit[], name: string, year?: number): RTAlgoliaHit =>
|
||||||
|
rs
|
||||||
|
.map((r) => ({ score: score(r, name, year), result: r }))
|
||||||
|
.filter(({ score }) => score > MINIMUM_SCORE)
|
||||||
|
.sort(({ score: a }, { score: b }) => b - a)[0]?.result;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a best-effort API. The Rotten Tomatoes API is technically
|
* This is a best-effort API. The Rotten Tomatoes API is technically
|
||||||
* private and getting access costs money/requires approval.
|
* private and getting access costs money/requires approval.
|
||||||
@@ -63,15 +105,12 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
super(
|
super(
|
||||||
'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
|
'https://79frdp12pn-dsn.algolia.net/1/indexes/*',
|
||||||
{
|
{
|
||||||
'x-algolia-agent':
|
'x-algolia-agent': 'Algolia for JavaScript (4.14.3); Browser (lite)',
|
||||||
'Algolia%20for%20JavaScript%20(4.14.3)%3B%20Browser%20(lite)',
|
|
||||||
'x-algolia-api-key': '175588f6e5f8319b27702e4cc4013561',
|
'x-algolia-api-key': '175588f6e5f8319b27702e4cc4013561',
|
||||||
'x-algolia-application-id': '79FRDP12PN',
|
'x-algolia-application-id': '79FRDP12PN',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
'x-algolia-usertoken': settings.clientId,
|
'x-algolia-usertoken': settings.clientId,
|
||||||
},
|
},
|
||||||
nodeCache: cacheManager.getCache('rt').data,
|
nodeCache: cacheManager.getCache('rt').data,
|
||||||
@@ -93,47 +132,21 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
year: number
|
year: number
|
||||||
): Promise<RTRating | null> {
|
): Promise<RTRating | null> {
|
||||||
try {
|
try {
|
||||||
|
const filters = encodeURIComponent('isEmsSearchable=1 AND type:"movie"');
|
||||||
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
||||||
requests: [
|
requests: [
|
||||||
{
|
{
|
||||||
indexName: 'content_rt',
|
indexName: 'content_rt',
|
||||||
query: name,
|
query: name.replace(/\bthe\b ?/gi, ''),
|
||||||
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
|
params: `filters=${filters}&hitsPerPage=20`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
||||||
|
const movie = best(contentResults?.hits || [], name, year);
|
||||||
|
|
||||||
if (!contentResults) {
|
if (!movie?.rottenTomatoes) return null;
|
||||||
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 {
|
return {
|
||||||
title: movie.title,
|
title: movie.title,
|
||||||
@@ -161,33 +174,21 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
year?: number
|
year?: number
|
||||||
): Promise<RTRating | null> {
|
): Promise<RTRating | null> {
|
||||||
try {
|
try {
|
||||||
|
const filters = encodeURIComponent('isEmsSearchable=1 AND type:"tv"');
|
||||||
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
||||||
requests: [
|
requests: [
|
||||||
{
|
{
|
||||||
indexName: 'content_rt',
|
indexName: 'content_rt',
|
||||||
query: name,
|
query: name,
|
||||||
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
|
params: `filters=${filters}&hitsPerPage=20`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
||||||
|
const tvshow = best(contentResults?.hits || [], name, year);
|
||||||
|
|
||||||
if (!contentResults) {
|
if (!tvshow?.rottenTomatoes) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tvshow: RTAlgoliaHit | undefined = contentResults.hits[0];
|
|
||||||
|
|
||||||
if (year) {
|
|
||||||
tvshow = contentResults.hits.find(
|
|
||||||
(series) => series.releaseYear === year
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tvshow) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: tvshow.title,
|
title: tvshow.title,
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
|
|
||||||
public getSystemStatus = async (): Promise<SystemStatus> => {
|
public getSystemStatus = async (): Promise<SystemStatus> => {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<SystemStatus>('/system/status');
|
const data = await this.get<SystemStatus>('/system/status');
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${this.apiName}] Failed to retrieve system status: ${e.message}`
|
`[${this.apiName}] Failed to retrieve system status: ${e.message}`
|
||||||
@@ -157,16 +157,15 @@ 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 data = await this.get<QueueResponse<QueueItemAppendT>>(
|
||||||
`/queue`,
|
`/queue`,
|
||||||
{
|
{
|
||||||
params: {
|
includeEpisode: 'true',
|
||||||
includeEpisode: true,
|
|
||||||
},
|
},
|
||||||
}
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data.records;
|
return data.records;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${this.apiName}] Failed to retrieve queue: ${e.message}`
|
`[${this.apiName}] Failed to retrieve queue: ${e.message}`
|
||||||
@@ -176,9 +175,9 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
|
|
||||||
public getTags = async (): Promise<Tag[]> => {
|
public getTags = async (): Promise<Tag[]> => {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<Tag[]>(`/tag`);
|
const data = await this.get<Tag[]>(`/tag`);
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${this.apiName}] Failed to retrieve tags: ${e.message}`
|
`[${this.apiName}] Failed to retrieve tags: ${e.message}`
|
||||||
@@ -188,25 +187,34 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
|
|
||||||
public createTag = async ({ label }: { label: string }): Promise<Tag> => {
|
public createTag = async ({ label }: { label: string }): Promise<Tag> => {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.post<Tag>(`/tag`, {
|
const data = await this.post<Tag>(`/tag`, {
|
||||||
label,
|
label,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[${this.apiName}] Failed to create tag: ${e.message}`);
|
throw new Error(`[${this.apiName}] Failed to create tag: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async refreshMonitoredDownloads(): Promise<void> {
|
||||||
|
await this.runCommand('RefreshMonitoredDownloads', {});
|
||||||
|
}
|
||||||
|
|
||||||
protected async runCommand(
|
protected async runCommand(
|
||||||
commandName: string,
|
commandName: string,
|
||||||
options: Record<string, unknown>
|
options: Record<string, unknown>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.axios.post(`/command`, {
|
await this.post(
|
||||||
|
`/command`,
|
||||||
|
{
|
||||||
name: commandName,
|
name: commandName,
|
||||||
...options,
|
...options,
|
||||||
});
|
},
|
||||||
|
{},
|
||||||
|
0
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`);
|
throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface RadarrMovie {
|
|||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
added: string;
|
added: string;
|
||||||
hasFile: boolean;
|
hasFile: boolean;
|
||||||
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||||
@@ -37,9 +38,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
public getMovies = async (): Promise<RadarrMovie[]> => {
|
public getMovies = async (): Promise<RadarrMovie[]> => {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<RadarrMovie[]>('/movie');
|
const data = await this.get<RadarrMovie[]>('/movie');
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Radarr] Failed to retrieve movies: ${e.message}`);
|
throw new Error(`[Radarr] Failed to retrieve movies: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -47,9 +48,9 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
public getMovie = async ({ id }: { id: number }): Promise<RadarrMovie> => {
|
public getMovie = async ({ id }: { id: number }): Promise<RadarrMovie> => {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<RadarrMovie>(`/movie/${id}`);
|
const data = await this.get<RadarrMovie>(`/movie/${id}`);
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Radarr] Failed to retrieve movie: ${e.message}`);
|
throw new Error(`[Radarr] Failed to retrieve movie: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -57,17 +58,15 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
|
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<RadarrMovie[]>('/movie/lookup', {
|
const data = await this.get<RadarrMovie[]>('/movie/lookup', {
|
||||||
params: {
|
|
||||||
term: `tmdb:${id}`,
|
term: `tmdb:${id}`,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.data[0]) {
|
if (!data[0]) {
|
||||||
throw new Error('Movie not found');
|
throw new Error('Movie not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data[0];
|
return 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',
|
||||||
@@ -97,7 +96,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
|
|
||||||
// movie exists in Radarr but is neither downloaded nor monitored
|
// movie exists in Radarr but is neither downloaded nor monitored
|
||||||
if (movie.id && !movie.monitored) {
|
if (movie.id && !movie.monitored) {
|
||||||
const response = await this.axios.put<RadarrMovie>(`/movie`, {
|
const data = await this.put<RadarrMovie>(`/movie`, {
|
||||||
...movie,
|
...movie,
|
||||||
title: options.title,
|
title: options.title,
|
||||||
qualityProfileId: options.qualityProfileId,
|
qualityProfileId: options.qualityProfileId,
|
||||||
@@ -106,7 +105,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
minimumAvailability: options.minimumAvailability,
|
minimumAvailability: options.minimumAvailability,
|
||||||
tmdbId: options.tmdbId,
|
tmdbId: options.tmdbId,
|
||||||
year: options.year,
|
year: options.year,
|
||||||
tags: options.tags,
|
tags: Array.from(new Set([...movie.tags, ...options.tags])),
|
||||||
rootFolderPath: options.rootFolderPath,
|
rootFolderPath: options.rootFolderPath,
|
||||||
monitored: options.monitored,
|
monitored: options.monitored,
|
||||||
addOptions: {
|
addOptions: {
|
||||||
@@ -114,25 +113,25 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.monitored) {
|
if (data.monitored) {
|
||||||
logger.info(
|
logger.info(
|
||||||
'Found existing title in Radarr and set it to monitored.',
|
'Found existing title in Radarr and set it to monitored.',
|
||||||
{
|
{
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
movieId: response.data.id,
|
movieId: data.id,
|
||||||
movieTitle: response.data.title,
|
movieTitle: data.title,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
logger.debug('Radarr update details', {
|
logger.debug('Radarr update details', {
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
movie: response.data,
|
movie: data,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.searchNow) {
|
if (options.searchNow) {
|
||||||
this.searchMovie(response.data.id);
|
this.searchMovie(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to update existing movie in Radarr.', {
|
logger.error('Failed to update existing movie in Radarr.', {
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
@@ -150,7 +149,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
return movie;
|
return movie;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.axios.post<RadarrMovie>(`/movie`, {
|
const data = await this.post<RadarrMovie>(`/movie`, {
|
||||||
title: options.title,
|
title: options.title,
|
||||||
qualityProfileId: options.qualityProfileId,
|
qualityProfileId: options.qualityProfileId,
|
||||||
profileId: options.profileId,
|
profileId: options.profileId,
|
||||||
@@ -166,11 +165,11 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.id) {
|
if (data.id) {
|
||||||
logger.info('Radarr accepted request', { label: 'Radarr' });
|
logger.info('Radarr accepted request', { label: 'Radarr' });
|
||||||
logger.debug('Radarr add details', {
|
logger.debug('Radarr add details', {
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
movie: response.data,
|
movie: data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to add movie to Radarr', {
|
logger.error('Failed to add movie to Radarr', {
|
||||||
@@ -179,15 +178,22 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
});
|
});
|
||||||
throw new Error('Failed to add movie to Radarr');
|
throw new Error('Failed to add movie to Radarr');
|
||||||
}
|
}
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
let errorData;
|
||||||
|
try {
|
||||||
|
errorData = await e.cause?.text();
|
||||||
|
errorData = JSON.parse(errorData);
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
|
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
|
||||||
{
|
{
|
||||||
label: 'Radarr',
|
label: 'Radarr',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
options,
|
options,
|
||||||
response: e?.response?.data,
|
response: errorData,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
throw new Error('Failed to add movie to Radarr');
|
throw new Error('Failed to add movie to Radarr');
|
||||||
@@ -216,17 +222,35 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
public removeMovie = async (movieId: number): Promise<void> => {
|
public removeMovie = async (movieId: number): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id, title } = await this.getMovieByTmdbId(movieId);
|
const { id, title } = await this.getMovieByTmdbId(movieId);
|
||||||
await this.axios.delete(`/movie/${id}`, {
|
await this.delete(`/movie/${id}`, {
|
||||||
params: {
|
deleteFiles: 'true',
|
||||||
deleteFiles: true,
|
addImportExclusion: 'false',
|
||||||
addImportExclusion: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
logger.info(`[Radarr] Removed movie ${title}`);
|
logger.info(`[Radarr] Removed movie ${title}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
|
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public clearCache = ({
|
||||||
|
tmdbId,
|
||||||
|
externalId,
|
||||||
|
}: {
|
||||||
|
tmdbId?: number | null;
|
||||||
|
externalId?: number | null;
|
||||||
|
}) => {
|
||||||
|
if (tmdbId) {
|
||||||
|
this.removeCache('/movie/lookup', {
|
||||||
|
term: `tmdb:${tmdbId}`,
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (externalId) {
|
||||||
|
this.removeCache(`/movie/${externalId}`, {
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RadarrAPI;
|
export default RadarrAPI;
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
public async getSeries(): Promise<SonarrSeries[]> {
|
public async getSeries(): Promise<SonarrSeries[]> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<SonarrSeries[]>('/series');
|
const data = await this.get<SonarrSeries[]>('/series');
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Sonarr] Failed to retrieve series: ${e.message}`);
|
throw new Error(`[Sonarr] Failed to retrieve series: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -127,9 +127,9 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
public async getSeriesById(id: number): Promise<SonarrSeries> {
|
public async getSeriesById(id: number): Promise<SonarrSeries> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<SonarrSeries>(`/series/${id}`);
|
const data = await this.get<SonarrSeries>(`/series/${id}`);
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
|
throw new Error(`[Sonarr] Failed to retrieve series by ID: ${e.message}`);
|
||||||
}
|
}
|
||||||
@@ -137,17 +137,15 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
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 data = await this.get<SonarrSeries[]>('/series/lookup', {
|
||||||
params: {
|
|
||||||
term: title,
|
term: title,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.data[0]) {
|
if (!data[0]) {
|
||||||
throw new Error('No series found');
|
throw new Error('No series found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error retrieving series by series title', {
|
logger.error('Error retrieving series by series title', {
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
@@ -160,17 +158,15 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
|
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
|
const data = await this.get<SonarrSeries[]>('/series/lookup', {
|
||||||
params: {
|
|
||||||
term: `tvdb:${id}`,
|
term: `tvdb:${id}`,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.data[0]) {
|
if (!data[0]) {
|
||||||
throw new Error('Series not found');
|
throw new Error('Series not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data[0];
|
return data[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error retrieving series by tvdb ID', {
|
logger.error('Error retrieving series by tvdb ID', {
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
@@ -188,30 +184,32 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
// If the series already exists, we will simply just update it
|
// If the series already exists, we will simply just update it
|
||||||
if (series.id) {
|
if (series.id) {
|
||||||
series.monitored = options.monitored ?? series.monitored;
|
series.monitored = options.monitored ?? series.monitored;
|
||||||
series.tags = options.tags ?? series.tags;
|
series.tags = options.tags
|
||||||
|
? Array.from(new Set([...series.tags, ...options.tags]))
|
||||||
|
: series.tags;
|
||||||
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
||||||
|
|
||||||
const newSeriesResponse = await this.axios.put<SonarrSeries>(
|
const newSeriesData = await this.put<SonarrSeries>(
|
||||||
'/series',
|
'/series',
|
||||||
series
|
series as any
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newSeriesResponse.data.id) {
|
if (newSeriesData.id) {
|
||||||
logger.info('Updated existing series in Sonarr.', {
|
logger.info('Updated existing series in Sonarr.', {
|
||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
seriesId: newSeriesResponse.data.id,
|
seriesId: newSeriesData.id,
|
||||||
seriesTitle: newSeriesResponse.data.title,
|
seriesTitle: newSeriesData.title,
|
||||||
});
|
});
|
||||||
logger.debug('Sonarr update details', {
|
logger.debug('Sonarr update details', {
|
||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
movie: newSeriesResponse.data,
|
movie: newSeriesData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.searchNow) {
|
if (options.searchNow) {
|
||||||
this.searchSeries(newSeriesResponse.data.id);
|
this.searchSeries(newSeriesData.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newSeriesResponse.data;
|
return newSeriesData;
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to update series in Sonarr', {
|
logger.error('Failed to update series in Sonarr', {
|
||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
@@ -221,9 +219,7 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
|
const createdSeriesData = await this.post<SonarrSeries>('/series', {
|
||||||
'/series',
|
|
||||||
{
|
|
||||||
tvdbId: options.tvdbid,
|
tvdbId: options.tvdbid,
|
||||||
title: options.title,
|
title: options.title,
|
||||||
qualityProfileId: options.profileId,
|
qualityProfileId: options.profileId,
|
||||||
@@ -245,14 +241,13 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
ignoreEpisodesWithFiles: true,
|
ignoreEpisodesWithFiles: true,
|
||||||
searchForMissingEpisodes: options.searchNow,
|
searchForMissingEpisodes: options.searchNow,
|
||||||
},
|
},
|
||||||
} as Partial<SonarrSeries>
|
} as Partial<SonarrSeries>);
|
||||||
);
|
|
||||||
|
|
||||||
if (createdSeriesResponse.data.id) {
|
if (createdSeriesData.id) {
|
||||||
logger.info('Sonarr accepted request', { label: 'Sonarr' });
|
logger.info('Sonarr accepted request', { label: 'Sonarr' });
|
||||||
logger.debug('Sonarr add details', {
|
logger.debug('Sonarr add details', {
|
||||||
label: 'Sonarr',
|
label: 'Sonarr',
|
||||||
movie: createdSeriesResponse.data,
|
movie: createdSeriesData,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.error('Failed to add movie to Sonarr', {
|
logger.error('Failed to add movie to Sonarr', {
|
||||||
@@ -262,13 +257,20 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
throw new Error('Failed to add series to Sonarr');
|
throw new Error('Failed to add series to Sonarr');
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdSeriesResponse.data;
|
return createdSeriesData;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
let errorData;
|
||||||
|
try {
|
||||||
|
errorData = await e.cause?.text();
|
||||||
|
errorData = JSON.parse(errorData);
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
logger.error('Something went wrong while adding a series to Sonarr.', {
|
logger.error('Something went wrong while adding a series to Sonarr.', {
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
options,
|
options,
|
||||||
response: e?.response?.data,
|
response: errorData,
|
||||||
});
|
});
|
||||||
throw new Error('Failed to add series');
|
throw new Error('Failed to add series');
|
||||||
}
|
}
|
||||||
@@ -303,10 +305,10 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.runCommand('SeriesSearch', { seriesId });
|
await this.runCommand('MissingEpisodeSearch', { seriesId });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Something went wrong while executing Sonarr series search.',
|
'Something went wrong while executing Sonarr missing episode search.',
|
||||||
{
|
{
|
||||||
label: 'Sonarr API',
|
label: 'Sonarr API',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
@@ -340,20 +342,47 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
|
|
||||||
return newSeasons;
|
return newSeasons;
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeSerie = async (serieId: number): Promise<void> => {
|
public removeSerie = async (serieId: number): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id, title } = await this.getSeriesByTvdbId(serieId);
|
const { id, title } = await this.getSeriesByTvdbId(serieId);
|
||||||
await this.axios.delete(`/series/${id}`, {
|
await this.delete(`/series/${id}`, {
|
||||||
params: {
|
deleteFiles: 'true',
|
||||||
deleteFiles: true,
|
addImportExclusion: 'false',
|
||||||
addImportExclusion: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
logger.info(`[Radarr] Removed serie ${title}`);
|
logger.info(`[Radarr] Removed serie ${title}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
|
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public clearCache = ({
|
||||||
|
tvdbId,
|
||||||
|
externalId,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
tvdbId?: number | null;
|
||||||
|
externalId?: number | null;
|
||||||
|
title?: string | null;
|
||||||
|
}) => {
|
||||||
|
if (tvdbId) {
|
||||||
|
this.removeCache('/series/lookup', {
|
||||||
|
term: `tvdb:${tvdbId}`,
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (externalId) {
|
||||||
|
this.removeCache(`/series/${externalId}`, {
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (title) {
|
||||||
|
this.removeCache('/series/lookup', {
|
||||||
|
term: title,
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SonarrAPI;
|
export default SonarrAPI;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
import type { User } from '@server/entity/User';
|
import type { User } from '@server/entity/User';
|
||||||
import type { TautulliSettings } from '@server/lib/settings';
|
import type { TautulliSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import type { AxiosInstance } from 'axios';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { uniqWith } from 'lodash';
|
import { uniqWith } from 'lodash';
|
||||||
|
|
||||||
export interface TautulliHistoryRecord {
|
export interface TautulliHistoryRecord {
|
||||||
@@ -113,25 +112,25 @@ interface TautulliInfoResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class TautulliAPI {
|
class TautulliAPI extends ExternalAPI {
|
||||||
private axios: AxiosInstance;
|
|
||||||
|
|
||||||
constructor(settings: TautulliSettings) {
|
constructor(settings: TautulliSettings) {
|
||||||
this.axios = axios.create({
|
super(
|
||||||
baseURL: `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
`${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
|
||||||
settings.port
|
settings.port
|
||||||
}${settings.urlBase ?? ''}`,
|
}${settings.urlBase ?? ''}`,
|
||||||
params: { apikey: settings.apiKey },
|
{
|
||||||
});
|
apikey: settings.apiKey || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getInfo(): Promise<TautulliInfo> {
|
public async getInfo(): Promise<TautulliInfo> {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await this.axios.get<TautulliInfoResponse>('/api/v2', {
|
await this.get<TautulliInfoResponse>('/api/v2', {
|
||||||
params: { cmd: 'get_tautulli_info' },
|
cmd: 'get_tautulli_info',
|
||||||
})
|
})
|
||||||
).data.response.data;
|
).response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Something went wrong fetching Tautulli server info', {
|
logger.error('Something went wrong fetching Tautulli server info', {
|
||||||
label: 'Tautulli API',
|
label: 'Tautulli API',
|
||||||
@@ -148,14 +147,12 @@ class TautulliAPI {
|
|||||||
): Promise<TautulliWatchStats[]> {
|
): Promise<TautulliWatchStats[]> {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
await this.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||||
params: {
|
|
||||||
cmd: 'get_item_watch_time_stats',
|
cmd: 'get_item_watch_time_stats',
|
||||||
rating_key: ratingKey,
|
rating_key: ratingKey,
|
||||||
grouping: 1,
|
grouping: '1',
|
||||||
},
|
|
||||||
})
|
})
|
||||||
).data.response.data;
|
).response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Something went wrong fetching media watch stats from Tautulli',
|
'Something went wrong fetching media watch stats from Tautulli',
|
||||||
@@ -176,14 +173,12 @@ class TautulliAPI {
|
|||||||
): Promise<TautulliWatchUser[]> {
|
): Promise<TautulliWatchUser[]> {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await this.axios.get<TautulliWatchUsersResponse>('/api/v2', {
|
await this.get<TautulliWatchUsersResponse>('/api/v2', {
|
||||||
params: {
|
|
||||||
cmd: 'get_item_user_stats',
|
cmd: 'get_item_user_stats',
|
||||||
rating_key: ratingKey,
|
rating_key: ratingKey,
|
||||||
grouping: 1,
|
grouping: '1',
|
||||||
},
|
|
||||||
})
|
})
|
||||||
).data.response.data;
|
).response.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Something went wrong fetching media watch users from Tautulli',
|
'Something went wrong fetching media watch users from Tautulli',
|
||||||
@@ -206,15 +201,13 @@ class TautulliAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await this.axios.get<TautulliWatchStatsResponse>('/api/v2', {
|
await this.get<TautulliWatchStatsResponse>('/api/v2', {
|
||||||
params: {
|
|
||||||
cmd: 'get_user_watch_time_stats',
|
cmd: 'get_user_watch_time_stats',
|
||||||
user_id: user.plexId,
|
user_id: user.plexId.toString(),
|
||||||
query_days: 0,
|
query_days: '0',
|
||||||
grouping: 1,
|
grouping: '1',
|
||||||
},
|
|
||||||
})
|
})
|
||||||
).data.response.data[0];
|
).response.data[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'Something went wrong fetching user watch stats from Tautulli',
|
'Something went wrong fetching user watch stats from Tautulli',
|
||||||
@@ -245,19 +238,17 @@ class TautulliAPI {
|
|||||||
|
|
||||||
while (results.length < 20) {
|
while (results.length < 20) {
|
||||||
const tautulliData = (
|
const tautulliData = (
|
||||||
await this.axios.get<TautulliHistoryResponse>('/api/v2', {
|
await this.get<TautulliHistoryResponse>('/api/v2', {
|
||||||
params: {
|
|
||||||
cmd: 'get_history',
|
cmd: 'get_history',
|
||||||
grouping: 1,
|
grouping: '1',
|
||||||
order_column: 'date',
|
order_column: 'date',
|
||||||
order_dir: 'desc',
|
order_dir: 'desc',
|
||||||
user_id: user.plexId,
|
user_id: user.plexId.toString(),
|
||||||
media_type: 'movie,episode',
|
media_type: 'movie,episode',
|
||||||
length: take,
|
length: take.toString(),
|
||||||
start,
|
start: start.toString(),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
).data.response.data.data;
|
).response.data.data;
|
||||||
|
|
||||||
if (!tautulliData.length) {
|
if (!tautulliData.length) {
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -95,29 +95,30 @@ interface DiscoverTvOptions {
|
|||||||
sortBy?: SortOptions;
|
sortBy?: SortOptions;
|
||||||
watchRegion?: string;
|
watchRegion?: string;
|
||||||
watchProviders?: string;
|
watchProviders?: string;
|
||||||
|
withStatus?: string; // Returning Series: 0 Planned: 1 In Production: 2 Ended: 3 Cancelled: 4 Pilot: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
class TheMovieDb extends ExternalAPI {
|
class TheMovieDb extends ExternalAPI {
|
||||||
private region?: string;
|
private discoverRegion?: string;
|
||||||
private originalLanguage?: string;
|
private originalLanguage?: string;
|
||||||
constructor({
|
constructor({
|
||||||
region,
|
discoverRegion,
|
||||||
originalLanguage,
|
originalLanguage,
|
||||||
}: { region?: string; originalLanguage?: string } = {}) {
|
}: { discoverRegion?: string; originalLanguage?: string } = {}) {
|
||||||
super(
|
super(
|
||||||
'https://api.themoviedb.org/3',
|
'https://api.themoviedb.org/3',
|
||||||
{
|
{
|
||||||
api_key: 'db55323b8d3e4154498498a75642b381',
|
api_key: '431a8708161bcd1f1fbe7536137e61ed',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nodeCache: cacheManager.getCache('tmdb').data,
|
nodeCache: cacheManager.getCache('tmdb').data,
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
maxRequests: 20,
|
|
||||||
maxRPS: 50,
|
maxRPS: 50,
|
||||||
|
id: 'tmdb',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.region = region;
|
this.discoverRegion = discoverRegion;
|
||||||
this.originalLanguage = originalLanguage;
|
this.originalLanguage = originalLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +130,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
}: SearchOptions): Promise<TmdbSearchMultiResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
const data = await this.get<TmdbSearchMultiResponse>('/search/multi', {
|
||||||
params: { query, page, include_adult: includeAdult, language },
|
query,
|
||||||
|
page: page.toString(),
|
||||||
|
include_adult: includeAdult ? 'true' : 'false',
|
||||||
|
language,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -152,13 +156,11 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
}: SingleSearchOptions): Promise<TmdbSearchMovieResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
const data = await this.get<TmdbSearchMovieResponse>('/search/movie', {
|
||||||
params: {
|
|
||||||
query,
|
query,
|
||||||
page,
|
page: page.toString(),
|
||||||
include_adult: includeAdult,
|
include_adult: includeAdult ? 'true' : 'false',
|
||||||
language,
|
language,
|
||||||
primary_release_year: year,
|
primary_release_year: year?.toString() || '',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -181,13 +183,11 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
|
}: SingleSearchOptions): Promise<TmdbSearchTvResponse> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
|
const data = await this.get<TmdbSearchTvResponse>('/search/tv', {
|
||||||
params: {
|
|
||||||
query,
|
query,
|
||||||
page,
|
page: page.toString(),
|
||||||
include_adult: includeAdult,
|
include_adult: includeAdult ? 'true' : 'false',
|
||||||
language,
|
language,
|
||||||
first_air_date_year: year,
|
first_air_date_year: year?.toString() || '',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -210,7 +210,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}): Promise<TmdbPersonDetails> => {
|
}): Promise<TmdbPersonDetails> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
|
const data = await this.get<TmdbPersonDetails>(`/person/${personId}`, {
|
||||||
params: { language },
|
language,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -230,7 +230,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbPersonCombinedCredits>(
|
const data = await this.get<TmdbPersonCombinedCredits>(
|
||||||
`/person/${personId}/combined_credits`,
|
`/person/${personId}/combined_credits`,
|
||||||
{
|
{
|
||||||
params: { language },
|
language,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -253,11 +253,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbMovieDetails>(
|
const data = await this.get<TmdbMovieDetails>(
|
||||||
`/movie/${movieId}`,
|
`/movie/${movieId}`,
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
language,
|
language,
|
||||||
append_to_response:
|
append_to_response:
|
||||||
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
'credits,external_ids,videos,keywords,release_dates,watch/providers',
|
||||||
},
|
include_video_language: language + ', en',
|
||||||
},
|
},
|
||||||
43200
|
43200
|
||||||
);
|
);
|
||||||
@@ -279,11 +278,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbTvDetails>(
|
const data = await this.get<TmdbTvDetails>(
|
||||||
`/tv/${tvId}`,
|
`/tv/${tvId}`,
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
language,
|
language,
|
||||||
append_to_response:
|
append_to_response:
|
||||||
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
'aggregate_credits,credits,external_ids,keywords,videos,content_ratings,watch/providers',
|
||||||
},
|
include_video_language: language + ', en',
|
||||||
},
|
},
|
||||||
43200
|
43200
|
||||||
);
|
);
|
||||||
@@ -307,10 +305,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSeasonWithEpisodes>(
|
const data = await this.get<TmdbSeasonWithEpisodes>(
|
||||||
`/tv/${tvId}/season/${seasonNumber}`,
|
`/tv/${tvId}/season/${seasonNumber}`,
|
||||||
{
|
{
|
||||||
params: {
|
language: language || '',
|
||||||
language,
|
|
||||||
append_to_response: 'external_ids',
|
append_to_response: 'external_ids',
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -333,10 +329,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
`/movie/${movieId}/recommendations`,
|
`/movie/${movieId}/recommendations`,
|
||||||
{
|
{
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
language,
|
language,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -359,10 +353,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
`/movie/${movieId}/similar`,
|
`/movie/${movieId}/similar`,
|
||||||
{
|
{
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
language,
|
language,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -385,10 +377,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
`/keyword/${keywordId}/movies`,
|
`/keyword/${keywordId}/movies`,
|
||||||
{
|
{
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
language,
|
language,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -411,10 +401,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchTvResponse>(
|
const data = await this.get<TmdbSearchTvResponse>(
|
||||||
`/tv/${tvId}/recommendations`,
|
`/tv/${tvId}/recommendations`,
|
||||||
{
|
{
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
language,
|
language,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -437,10 +425,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
}): Promise<TmdbSearchTvResponse> {
|
}): Promise<TmdbSearchTvResponse> {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
|
const data = await this.get<TmdbSearchTvResponse>(`/tv/${tvId}/similar`, {
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
language,
|
language,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -481,40 +467,38 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
.split('T')[0];
|
.split('T')[0];
|
||||||
|
|
||||||
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
const data = await this.get<TmdbSearchMovieResponse>('/discover/movie', {
|
||||||
params: {
|
|
||||||
sort_by: sortBy,
|
sort_by: sortBy,
|
||||||
page,
|
page: page.toString(),
|
||||||
include_adult: includeAdult,
|
include_adult: includeAdult ? 'true' : 'false',
|
||||||
language,
|
language,
|
||||||
region: this.region,
|
region: this.discoverRegion || '',
|
||||||
with_original_language:
|
with_original_language:
|
||||||
originalLanguage && originalLanguage !== 'all'
|
originalLanguage && originalLanguage !== 'all'
|
||||||
? originalLanguage
|
? originalLanguage
|
||||||
: originalLanguage === 'all'
|
: originalLanguage === 'all'
|
||||||
? undefined
|
? ''
|
||||||
: this.originalLanguage,
|
: this.originalLanguage || '',
|
||||||
// Set our release date values, but check if one is set and not the other,
|
// 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!
|
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||||
'primary_release_date.gte':
|
'primary_release_date.gte':
|
||||||
!primaryReleaseDateGte && primaryReleaseDateLte
|
!primaryReleaseDateGte && primaryReleaseDateLte
|
||||||
? defaultPastDate
|
? defaultPastDate
|
||||||
: primaryReleaseDateGte,
|
: primaryReleaseDateGte || '',
|
||||||
'primary_release_date.lte':
|
'primary_release_date.lte':
|
||||||
!primaryReleaseDateLte && primaryReleaseDateGte
|
!primaryReleaseDateLte && primaryReleaseDateGte
|
||||||
? defaultFutureDate
|
? defaultFutureDate
|
||||||
: primaryReleaseDateLte,
|
: primaryReleaseDateLte || '',
|
||||||
with_genres: genre,
|
with_genres: genre || '',
|
||||||
with_companies: studio,
|
with_companies: studio || '',
|
||||||
with_keywords: keywords,
|
with_keywords: keywords || '',
|
||||||
'with_runtime.gte': withRuntimeGte,
|
'with_runtime.gte': withRuntimeGte || '',
|
||||||
'with_runtime.lte': withRuntimeLte,
|
'with_runtime.lte': withRuntimeLte || '',
|
||||||
'vote_average.gte': voteAverageGte,
|
'vote_average.gte': voteAverageGte || '',
|
||||||
'vote_average.lte': voteAverageLte,
|
'vote_average.lte': voteAverageLte || '',
|
||||||
'vote_count.gte': voteCountGte,
|
'vote_count.gte': voteCountGte || '',
|
||||||
'vote_count.lte': voteCountLte,
|
'vote_count.lte': voteCountLte || '',
|
||||||
watch_region: watchRegion,
|
watch_region: watchRegion || '',
|
||||||
with_watch_providers: watchProviders,
|
with_watch_providers: watchProviders || '',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -542,6 +526,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
voteCountLte,
|
voteCountLte,
|
||||||
watchProviders,
|
watchProviders,
|
||||||
watchRegion,
|
watchRegion,
|
||||||
|
withStatus,
|
||||||
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
}: DiscoverTvOptions = {}): Promise<TmdbSearchTvResponse> => {
|
||||||
try {
|
try {
|
||||||
const defaultFutureDate = new Date(
|
const defaultFutureDate = new Date(
|
||||||
@@ -555,40 +540,41 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
.split('T')[0];
|
.split('T')[0];
|
||||||
|
|
||||||
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
const data = await this.get<TmdbSearchTvResponse>('/discover/tv', {
|
||||||
params: {
|
|
||||||
sort_by: sortBy,
|
sort_by: sortBy,
|
||||||
page,
|
page: page.toString(),
|
||||||
language,
|
language,
|
||||||
region: this.region,
|
region: this.discoverRegion || '',
|
||||||
// Set our release date values, but check if one is set and not the other,
|
// 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!
|
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||||
'first_air_date.gte':
|
'first_air_date.gte':
|
||||||
!firstAirDateGte && firstAirDateLte
|
!firstAirDateGte && firstAirDateLte
|
||||||
? defaultPastDate
|
? defaultPastDate
|
||||||
: firstAirDateGte,
|
: firstAirDateGte || '',
|
||||||
'first_air_date.lte':
|
'first_air_date.lte':
|
||||||
!firstAirDateLte && firstAirDateGte
|
!firstAirDateLte && firstAirDateGte
|
||||||
? defaultFutureDate
|
? defaultFutureDate
|
||||||
: firstAirDateLte,
|
: firstAirDateLte || '',
|
||||||
with_original_language:
|
with_original_language:
|
||||||
originalLanguage && originalLanguage !== 'all'
|
originalLanguage && originalLanguage !== 'all'
|
||||||
? originalLanguage
|
? originalLanguage
|
||||||
: originalLanguage === 'all'
|
: originalLanguage === 'all'
|
||||||
? undefined
|
? ''
|
||||||
: this.originalLanguage,
|
: this.originalLanguage || '',
|
||||||
include_null_first_air_dates: includeEmptyReleaseDate,
|
include_null_first_air_dates: includeEmptyReleaseDate
|
||||||
with_genres: genre,
|
? 'true'
|
||||||
with_networks: network,
|
: 'false',
|
||||||
with_keywords: keywords,
|
with_genres: genre || '',
|
||||||
'with_runtime.gte': withRuntimeGte,
|
with_networks: network?.toString() || '',
|
||||||
'with_runtime.lte': withRuntimeLte,
|
with_keywords: keywords || '',
|
||||||
'vote_average.gte': voteAverageGte,
|
'with_runtime.gte': withRuntimeGte || '',
|
||||||
'vote_average.lte': voteAverageLte,
|
'with_runtime.lte': withRuntimeLte || '',
|
||||||
'vote_count.gte': voteCountGte,
|
'vote_average.gte': voteAverageGte || '',
|
||||||
'vote_count.lte': voteCountLte,
|
'vote_average.lte': voteAverageLte || '',
|
||||||
with_watch_providers: watchProviders,
|
'vote_count.gte': voteCountGte || '',
|
||||||
watch_region: watchRegion,
|
'vote_count.lte': voteCountLte || '',
|
||||||
},
|
with_watch_providers: watchProviders || '',
|
||||||
|
watch_region: watchRegion || '',
|
||||||
|
with_status: withStatus || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@@ -608,12 +594,10 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbUpcomingMoviesResponse>(
|
const data = await this.get<TmdbUpcomingMoviesResponse>(
|
||||||
'/movie/upcoming',
|
'/movie/upcoming',
|
||||||
{
|
{
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
language,
|
language,
|
||||||
region: this.region,
|
region: this.discoverRegion || '',
|
||||||
originalLanguage: this.originalLanguage,
|
originalLanguage: this.originalLanguage || '',
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -636,11 +620,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMultiResponse>(
|
const data = await this.get<TmdbSearchMultiResponse>(
|
||||||
`/trending/all/${timeWindow}`,
|
`/trending/all/${timeWindow}`,
|
||||||
{
|
{
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
language,
|
language,
|
||||||
region: this.region,
|
region: this.discoverRegion || '',
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -661,9 +643,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchMovieResponse>(
|
const data = await this.get<TmdbSearchMovieResponse>(
|
||||||
`/trending/movie/${timeWindow}`,
|
`/trending/movie/${timeWindow}`,
|
||||||
{
|
{
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -684,9 +664,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbSearchTvResponse>(
|
const data = await this.get<TmdbSearchTvResponse>(
|
||||||
`/trending/tv/${timeWindow}`,
|
`/trending/tv/${timeWindow}`,
|
||||||
{
|
{
|
||||||
params: {
|
page: page.toString(),
|
||||||
page,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -715,10 +693,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbExternalIdResponse>(
|
const data = await this.get<TmdbExternalIdResponse>(
|
||||||
`/find/${externalId}`,
|
`/find/${externalId}`,
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
external_source: type === 'imdb' ? 'imdb_id' : 'tvdb_id',
|
||||||
language,
|
language,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -808,9 +784,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbCollection>(
|
const data = await this.get<TmdbCollection>(
|
||||||
`/collection/${collectionId}`,
|
`/collection/${collectionId}`,
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
language,
|
language,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -883,10 +857,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbGenresResult>(
|
const data = await this.get<TmdbGenresResult>(
|
||||||
'/genre/movie/list',
|
'/genre/movie/list',
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -897,10 +869,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const englishData = await this.get<TmdbGenresResult>(
|
const englishData = await this.get<TmdbGenresResult>(
|
||||||
'/genre/movie/list',
|
'/genre/movie/list',
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
language: 'en',
|
language: 'en',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -934,10 +904,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbGenresResult>(
|
const data = await this.get<TmdbGenresResult>(
|
||||||
'/genre/tv/list',
|
'/genre/tv/list',
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -948,10 +916,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const englishData = await this.get<TmdbGenresResult>(
|
const englishData = await this.get<TmdbGenresResult>(
|
||||||
'/genre/tv/list',
|
'/genre/tv/list',
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
language: 'en',
|
language: 'en',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1005,10 +971,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbKeywordSearchResponse>(
|
const data = await this.get<TmdbKeywordSearchResponse>(
|
||||||
'/search/keyword',
|
'/search/keyword',
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
query,
|
query,
|
||||||
page,
|
page: page.toString(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -1030,10 +994,8 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<TmdbCompanySearchResponse>(
|
const data = await this.get<TmdbCompanySearchResponse>(
|
||||||
'/search/company',
|
'/search/company',
|
||||||
{
|
{
|
||||||
params: {
|
|
||||||
query,
|
query,
|
||||||
page,
|
page: page.toString(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -1053,9 +1015,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
|
const data = await this.get<{ results: TmdbWatchProviderRegion[] }>(
|
||||||
'/watch/providers/regions',
|
'/watch/providers/regions',
|
||||||
{
|
{
|
||||||
params: {
|
language: language ? this.originalLanguage || '' : '',
|
||||||
language: language ?? this.originalLanguage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
@@ -1079,11 +1039,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||||
'/watch/providers/movie',
|
'/watch/providers/movie',
|
||||||
{
|
{
|
||||||
params: {
|
language: language ? this.originalLanguage || '' : '',
|
||||||
language: language ?? this.originalLanguage,
|
|
||||||
watch_region: watchRegion,
|
watch_region: watchRegion,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1106,11 +1064,9 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
const data = await this.get<{ results: TmdbWatchProviderDetails[] }>(
|
||||||
'/watch/providers/tv',
|
'/watch/providers/tv',
|
||||||
{
|
{
|
||||||
params: {
|
language: language ? this.originalLanguage || '' : '',
|
||||||
language: language ?? this.originalLanguage,
|
|
||||||
watch_region: watchRegion,
|
watch_region: watchRegion,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
86400 // 24 hours
|
86400 // 24 hours
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ export enum ApiErrorCode {
|
|||||||
InvalidUrl = 'INVALID_URL',
|
InvalidUrl = 'INVALID_URL',
|
||||||
InvalidCredentials = 'INVALID_CREDENTIALS',
|
InvalidCredentials = 'INVALID_CREDENTIALS',
|
||||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
||||||
|
InvalidEmail = 'INVALID_EMAIL',
|
||||||
NotAdmin = 'NOT_ADMIN',
|
NotAdmin = 'NOT_ADMIN',
|
||||||
|
NoAdminUser = 'NO_ADMIN_USER',
|
||||||
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
||||||
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
||||||
|
Unauthorized = 'UNAUTHORIZED',
|
||||||
Unknown = 'UNKNOWN',
|
Unknown = 'UNKNOWN',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ export enum MediaStatus {
|
|||||||
PROCESSING,
|
PROCESSING,
|
||||||
PARTIALLY_AVAILABLE,
|
PARTIALLY_AVAILABLE,
|
||||||
AVAILABLE,
|
AVAILABLE,
|
||||||
|
BLACKLISTED,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,8 @@ export enum MediaServerType {
|
|||||||
EMBY,
|
EMBY,
|
||||||
NOT_CONFIGURED,
|
NOT_CONFIGURED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ServerType {
|
||||||
|
JELLYFIN = 'Jellyfin',
|
||||||
|
EMBY = 'Emby',
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export enum UserType {
|
|||||||
PLEX = 1,
|
PLEX = 1,
|
||||||
LOCAL = 2,
|
LOCAL = 2,
|
||||||
JELLYFIN = 3,
|
JELLYFIN = 3,
|
||||||
|
EMBY = 4,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,43 @@
|
|||||||
import 'reflect-metadata';
|
import fs from 'fs';
|
||||||
|
import type { TlsOptions } from 'tls';
|
||||||
import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm';
|
import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
const DB_SSL_PREFIX = 'DB_SSL_';
|
||||||
|
|
||||||
|
function boolFromEnv(envVar: string, defaultVal = false) {
|
||||||
|
if (process.env[envVar]) {
|
||||||
|
return process.env[envVar]?.toLowerCase() === 'true';
|
||||||
|
}
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringOrReadFileFromEnv(envVar: string): Buffer | string | undefined {
|
||||||
|
if (process.env[envVar]) {
|
||||||
|
return process.env[envVar];
|
||||||
|
}
|
||||||
|
const filePath = process.env[`${envVar}_FILE`];
|
||||||
|
if (filePath) {
|
||||||
|
return fs.readFileSync(filePath);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSslConfig(): TlsOptions | undefined {
|
||||||
|
if (process.env.DB_USE_SSL?.toLowerCase() !== 'true') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rejectUnauthorized: boolFromEnv(
|
||||||
|
`${DB_SSL_PREFIX}REJECT_UNAUTHORIZED`,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
ca: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}CA`),
|
||||||
|
key: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}KEY`),
|
||||||
|
cert: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}CERT`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const devConfig: DataSourceOptions = {
|
const devConfig: DataSourceOptions = {
|
||||||
type: 'sqlite',
|
type: 'sqlite',
|
||||||
database: process.env.CONFIG_DIRECTORY
|
database: process.env.CONFIG_DIRECTORY
|
||||||
@@ -9,10 +45,10 @@ const devConfig: DataSourceOptions = {
|
|||||||
: 'config/db/db.sqlite3',
|
: 'config/db/db.sqlite3',
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
migrationsRun: false,
|
migrationsRun: false,
|
||||||
logging: false,
|
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||||
enableWAL: true,
|
enableWAL: true,
|
||||||
entities: ['server/entity/**/*.ts'],
|
entities: ['server/entity/**/*.ts'],
|
||||||
migrations: ['server/migration/**/*.ts'],
|
migrations: ['server/migration/sqlite/**/*.ts'],
|
||||||
subscribers: ['server/subscriber/**/*.ts'],
|
subscribers: ['server/subscriber/**/*.ts'],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,16 +59,60 @@ const prodConfig: DataSourceOptions = {
|
|||||||
: 'config/db/db.sqlite3',
|
: 'config/db/db.sqlite3',
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
migrationsRun: false,
|
migrationsRun: false,
|
||||||
logging: false,
|
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||||
enableWAL: true,
|
enableWAL: true,
|
||||||
entities: ['dist/entity/**/*.js'],
|
entities: ['dist/entity/**/*.js'],
|
||||||
migrations: ['dist/migration/**/*.js'],
|
migrations: ['dist/migration/sqlite/**/*.js'],
|
||||||
subscribers: ['dist/subscriber/**/*.js'],
|
subscribers: ['dist/subscriber/**/*.js'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const dataSource = new DataSource(
|
const postgresDevConfig: DataSourceOptions = {
|
||||||
process.env.NODE_ENV !== 'production' ? devConfig : prodConfig
|
type: 'postgres',
|
||||||
);
|
host: process.env.DB_SOCKET_PATH || process.env.DB_HOST,
|
||||||
|
port: process.env.DB_SOCKET_PATH
|
||||||
|
? undefined
|
||||||
|
: parseInt(process.env.DB_PORT ?? '5432'),
|
||||||
|
username: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASS,
|
||||||
|
database: process.env.DB_NAME ?? 'jellyseerr',
|
||||||
|
ssl: buildSslConfig(),
|
||||||
|
synchronize: false,
|
||||||
|
migrationsRun: true,
|
||||||
|
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||||
|
entities: ['server/entity/**/*.ts'],
|
||||||
|
migrations: ['server/migration/postgres/**/*.ts'],
|
||||||
|
subscribers: ['server/subscriber/**/*.ts'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const postgresProdConfig: DataSourceOptions = {
|
||||||
|
type: 'postgres',
|
||||||
|
host: process.env.DB_SOCKET_PATH || process.env.DB_HOST,
|
||||||
|
port: process.env.DB_SOCKET_PATH
|
||||||
|
? undefined
|
||||||
|
: parseInt(process.env.DB_PORT ?? '5432'),
|
||||||
|
username: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASS,
|
||||||
|
database: process.env.DB_NAME ?? 'jellyseerr',
|
||||||
|
ssl: buildSslConfig(),
|
||||||
|
synchronize: false,
|
||||||
|
migrationsRun: false,
|
||||||
|
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||||
|
entities: ['dist/entity/**/*.js'],
|
||||||
|
migrations: ['dist/migration/postgres/**/*.js'],
|
||||||
|
subscribers: ['dist/subscriber/**/*.js'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPgsql = process.env.DB_TYPE === 'postgres';
|
||||||
|
|
||||||
|
function getDataSource(): DataSourceOptions {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
return isPgsql ? postgresProdConfig : prodConfig;
|
||||||
|
} else {
|
||||||
|
return isPgsql ? postgresDevConfig : devConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSource = new DataSource(getDataSource());
|
||||||
|
|
||||||
export const getRepository = <Entity extends object>(
|
export const getRepository = <Entity extends object>(
|
||||||
target: EntityTarget<Entity>
|
target: EntityTarget<Entity>
|
||||||
|
|||||||
95
server/entity/Blacklist.ts
Normal file
95
server/entity/Blacklist.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { MediaStatus, type MediaType } from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import Media from '@server/entity/Media';
|
||||||
|
import { User } from '@server/entity/User';
|
||||||
|
import type { BlacklistItem } from '@server/interfaces/api/blacklistInterfaces';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Unique,
|
||||||
|
} from 'typeorm';
|
||||||
|
import type { ZodNumber, ZodOptional, ZodString } from 'zod';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Unique(['tmdbId'])
|
||||||
|
export class Blacklist implements BlacklistItem {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
public id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar' })
|
||||||
|
public mediaType: MediaType;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'varchar' })
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@Index()
|
||||||
|
public tmdbId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.id, {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@OneToOne(() => Media, (media) => media.blacklist, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public media: Media;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
constructor(init?: Partial<Blacklist>) {
|
||||||
|
Object.assign(this, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async addToBlacklist({
|
||||||
|
blacklistRequest,
|
||||||
|
}: {
|
||||||
|
blacklistRequest: {
|
||||||
|
mediaType: MediaType;
|
||||||
|
title?: ZodOptional<ZodString>['_output'];
|
||||||
|
tmdbId: ZodNumber['_output'];
|
||||||
|
};
|
||||||
|
}): Promise<void> {
|
||||||
|
const blacklist = new this({
|
||||||
|
...blacklistRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mediaRepository = getRepository(Media);
|
||||||
|
let media = await mediaRepository.findOne({
|
||||||
|
where: {
|
||||||
|
tmdbId: blacklistRequest.tmdbId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const blacklistRepository = getRepository(this);
|
||||||
|
|
||||||
|
await blacklistRepository.save(blacklist);
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
media = new Media({
|
||||||
|
tmdbId: blacklistRequest.tmdbId,
|
||||||
|
status: MediaStatus.BLACKLISTED,
|
||||||
|
status4k: MediaStatus.BLACKLISTED,
|
||||||
|
mediaType: blacklistRequest.mediaType,
|
||||||
|
blacklist: Promise.resolve(blacklist),
|
||||||
|
});
|
||||||
|
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
} else {
|
||||||
|
media.blacklist = Promise.resolve(blacklist);
|
||||||
|
media.status = MediaStatus.BLACKLISTED;
|
||||||
|
media.status4k = MediaStatus.BLACKLISTED;
|
||||||
|
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@ import SonarrAPI from '@server/api/servarr/sonarr';
|
|||||||
import { MediaStatus, MediaType } from '@server/constants/media';
|
import { MediaStatus, MediaType } from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
|
import { Blacklist } from '@server/entity/Blacklist';
|
||||||
import type { User } from '@server/entity/User';
|
import type { User } from '@server/entity/User';
|
||||||
import { Watchlist } from '@server/entity/Watchlist';
|
import { Watchlist } from '@server/entity/Watchlist';
|
||||||
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||||
import downloadTracker from '@server/lib/downloadtracker';
|
import downloadTracker from '@server/lib/downloadtracker';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import { DbAwareColumn } from '@server/utils/DbColumnHelper';
|
||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import {
|
import {
|
||||||
AfterLoad,
|
AfterLoad,
|
||||||
@@ -17,6 +19,7 @@ import {
|
|||||||
Entity,
|
Entity,
|
||||||
Index,
|
Index,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
@@ -40,6 +43,10 @@ class Media {
|
|||||||
finalIds = tmdbIds;
|
finalIds = tmdbIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (finalIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const media = await mediaRepository
|
const media = await mediaRepository
|
||||||
.createQueryBuilder('media')
|
.createQueryBuilder('media')
|
||||||
.leftJoinAndSelect(
|
.leftJoinAndSelect(
|
||||||
@@ -66,7 +73,7 @@ class Media {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const media = await mediaRepository.findOne({
|
const media = await mediaRepository.findOne({
|
||||||
where: { tmdbId: id, mediaType },
|
where: { tmdbId: id, mediaType: mediaType },
|
||||||
relations: { requests: true, issues: true },
|
relations: { requests: true, issues: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,16 +123,32 @@ class Media {
|
|||||||
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
|
@OneToMany(() => Issue, (issue) => issue.media, { cascade: true })
|
||||||
public issues: Issue[];
|
public issues: Issue[];
|
||||||
|
|
||||||
|
@OneToOne(() => Blacklist, (blacklist) => blacklist.media)
|
||||||
|
public blacklist: Promise<Blacklist>;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
public updatedAt: Date;
|
public updatedAt: Date;
|
||||||
|
|
||||||
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
/**
|
||||||
|
* The `lastSeasonChange` column stores the date and time when the media was added to the library.
|
||||||
|
* It needs to be database-aware because SQLite supports `datetime` while PostgreSQL supports `timestamp with timezone (timestampz)`.
|
||||||
|
*/
|
||||||
|
@DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||||
public lastSeasonChange: Date;
|
public lastSeasonChange: Date;
|
||||||
|
|
||||||
@Column({ type: 'datetime', nullable: true })
|
/**
|
||||||
|
* The `mediaAddedAt` column stores the date and time when the media was added to the library.
|
||||||
|
* It needs to be database-aware because SQLite supports `datetime` while PostgreSQL supports `timestamp with timezone (timestampz)`.
|
||||||
|
* This column is nullable because it can be null when the media is not yet synced to the library.
|
||||||
|
*/
|
||||||
|
@DbAwareColumn({
|
||||||
|
type: 'datetime',
|
||||||
|
default: () => 'CURRENT_TIMESTAMP',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
public mediaAddedAt: Date;
|
public mediaAddedAt: Date;
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'int' })
|
@Column({ nullable: true, type: 'int' })
|
||||||
@@ -211,9 +234,10 @@ class Media {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pageName =
|
const pageName =
|
||||||
process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details';
|
getSettings().main.mediaServerType == MediaServerType.EMBY
|
||||||
|
? 'item'
|
||||||
|
: 'details';
|
||||||
const { serverId, externalHostname } = getSettings().jellyfin;
|
const { serverId, externalHostname } = getSettings().jellyfin;
|
||||||
|
|
||||||
const jellyfinHost =
|
const jellyfinHost =
|
||||||
externalHostname && externalHostname.length > 0
|
externalHostname && externalHostname.length > 0
|
||||||
? externalHostname
|
? externalHostname
|
||||||
@@ -223,7 +247,7 @@ class Media {
|
|||||||
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.jellyfinMediaId}&context=home&serverId=${serverId}`;
|
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import type {
|
|||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||||
|
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
MediaType,
|
MediaType,
|
||||||
} from '@server/constants/media';
|
} from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
|
import OverrideRule from '@server/entity/OverrideRule';
|
||||||
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
@@ -40,6 +42,7 @@ export class RequestPermissionError extends Error {}
|
|||||||
export class QuotaRestrictedError extends Error {}
|
export class QuotaRestrictedError extends Error {}
|
||||||
export class DuplicateMediaRequestError extends Error {}
|
export class DuplicateMediaRequestError extends Error {}
|
||||||
export class NoSeasonsAvailableError extends Error {}
|
export class NoSeasonsAvailableError extends Error {}
|
||||||
|
export class BlacklistedMediaError extends Error {}
|
||||||
|
|
||||||
type MediaRequestOptions = {
|
type MediaRequestOptions = {
|
||||||
isAutoRequest?: boolean;
|
isAutoRequest?: boolean;
|
||||||
@@ -56,6 +59,7 @@ export class MediaRequest {
|
|||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
let requestUser = user;
|
let requestUser = user;
|
||||||
|
|
||||||
@@ -143,6 +147,16 @@ export class MediaRequest {
|
|||||||
mediaType: requestBody.mediaType,
|
mediaType: requestBody.mediaType,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (media.status === MediaStatus.BLACKLISTED) {
|
||||||
|
logger.warn('Request for media blocked due to being blacklisted', {
|
||||||
|
tmdbId: tmdbMedia.id,
|
||||||
|
mediaType: requestBody.mediaType,
|
||||||
|
label: 'Media Request',
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new BlacklistedMediaError('This media is blacklisted.');
|
||||||
|
}
|
||||||
|
|
||||||
if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
|
if (media.status === MediaStatus.UNKNOWN && !requestBody.is4k) {
|
||||||
media.status = MediaStatus.PENDING;
|
media.status = MediaStatus.PENDING;
|
||||||
}
|
}
|
||||||
@@ -194,6 +208,134 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply overrides if the user is not an admin or has the "advanced request" permission
|
||||||
|
const useOverrides = !user.hasPermission([Permission.MANAGE_REQUESTS], {
|
||||||
|
type: 'or',
|
||||||
|
});
|
||||||
|
|
||||||
|
let rootFolder = requestBody.rootFolder;
|
||||||
|
let profileId = requestBody.profileId;
|
||||||
|
let tags = requestBody.tags;
|
||||||
|
|
||||||
|
if (useOverrides) {
|
||||||
|
const defaultRadarrId = requestBody.is4k
|
||||||
|
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
||||||
|
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
||||||
|
const defaultSonarrId = requestBody.is4k
|
||||||
|
? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
|
||||||
|
: settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
|
||||||
|
|
||||||
|
const overrideRuleRepository = getRepository(OverrideRule);
|
||||||
|
const overrideRules = await overrideRuleRepository.find({
|
||||||
|
where:
|
||||||
|
requestBody.mediaType === MediaType.MOVIE
|
||||||
|
? { radarrServiceId: defaultRadarrId }
|
||||||
|
: { sonarrServiceId: defaultSonarrId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const appliedOverrideRules = overrideRules.filter((rule) => {
|
||||||
|
const hasAnimeKeyword =
|
||||||
|
'results' in tmdbMedia.keywords &&
|
||||||
|
tmdbMedia.keywords.results.some(
|
||||||
|
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip override rules if the media is an anime TV show as anime TV
|
||||||
|
// is handled by default and override rules do not explicitly include
|
||||||
|
// the anime keyword
|
||||||
|
if (
|
||||||
|
requestBody.mediaType === MediaType.TV &&
|
||||||
|
hasAnimeKeyword &&
|
||||||
|
(!rule.keywords ||
|
||||||
|
!rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
rule.users &&
|
||||||
|
!rule.users
|
||||||
|
.split(',')
|
||||||
|
.some((userId) => Number(userId) === requestUser.id)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rule.genre &&
|
||||||
|
!rule.genre
|
||||||
|
.split(',')
|
||||||
|
.some((genreId) =>
|
||||||
|
tmdbMedia.genres
|
||||||
|
.map((genre) => genre.id)
|
||||||
|
.includes(Number(genreId))
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rule.language &&
|
||||||
|
!rule.language
|
||||||
|
.split('|')
|
||||||
|
.some((languageId) => languageId === tmdbMedia.original_language)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
rule.keywords &&
|
||||||
|
!rule.keywords.split(',').some((keywordId) => {
|
||||||
|
let keywordList: TmdbKeyword[] = [];
|
||||||
|
|
||||||
|
if ('keywords' in tmdbMedia.keywords) {
|
||||||
|
keywordList = tmdbMedia.keywords.keywords;
|
||||||
|
} else if ('results' in tmdbMedia.keywords) {
|
||||||
|
keywordList = tmdbMedia.keywords.results;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keywordList
|
||||||
|
.map((keyword: TmdbKeyword) => keyword.id)
|
||||||
|
.includes(Number(keywordId));
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// hacky way to prioritize rules
|
||||||
|
// TODO: make this better
|
||||||
|
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
|
||||||
|
const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
|
||||||
|
|
||||||
|
const aSpecificity = keys.filter((key) => a[key] !== null).length;
|
||||||
|
const bSpecificity = keys.filter((key) => b[key] !== null).length;
|
||||||
|
|
||||||
|
// Take the rule with the most specific condition first
|
||||||
|
return bSpecificity - aSpecificity;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (prioritizedRule) {
|
||||||
|
if (prioritizedRule.rootFolder) {
|
||||||
|
rootFolder = prioritizedRule.rootFolder;
|
||||||
|
}
|
||||||
|
if (prioritizedRule.profileId) {
|
||||||
|
profileId = prioritizedRule.profileId;
|
||||||
|
}
|
||||||
|
if (prioritizedRule.tags) {
|
||||||
|
tags = [
|
||||||
|
...new Set([
|
||||||
|
...(tags || []),
|
||||||
|
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Override rule applied.', {
|
||||||
|
label: 'Media Request',
|
||||||
|
overrides: prioritizedRule,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (requestBody.mediaType === MediaType.MOVIE) {
|
if (requestBody.mediaType === MediaType.MOVIE) {
|
||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
|
|
||||||
@@ -232,9 +374,9 @@ export class MediaRequest {
|
|||||||
: undefined,
|
: undefined,
|
||||||
is4k: requestBody.is4k,
|
is4k: requestBody.is4k,
|
||||||
serverId: requestBody.serverId,
|
serverId: requestBody.serverId,
|
||||||
profileId: requestBody.profileId,
|
profileId: profileId,
|
||||||
rootFolder: requestBody.rootFolder,
|
rootFolder: rootFolder,
|
||||||
tags: requestBody.tags,
|
tags: tags,
|
||||||
isAutoRequest: options.isAutoRequest ?? false,
|
isAutoRequest: options.isAutoRequest ?? false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,12 +386,14 @@ export class MediaRequest {
|
|||||||
const tmdbMediaShow = tmdbMedia as Awaited<
|
const tmdbMediaShow = tmdbMedia as Awaited<
|
||||||
ReturnType<typeof tmdb.getTvShow>
|
ReturnType<typeof tmdb.getTvShow>
|
||||||
>;
|
>;
|
||||||
const requestedSeasons =
|
let requestedSeasons =
|
||||||
requestBody.seasons === 'all'
|
requestBody.seasons === 'all'
|
||||||
? tmdbMediaShow.seasons
|
? tmdbMediaShow.seasons.map((season) => season.season_number)
|
||||||
.map((season) => season.season_number)
|
|
||||||
.filter((sn) => sn > 0)
|
|
||||||
: (requestBody.seasons as number[]);
|
: (requestBody.seasons as number[]);
|
||||||
|
if (!settings.main.enableSpecialEpisodes) {
|
||||||
|
requestedSeasons = requestedSeasons.filter((sn) => sn > 0);
|
||||||
|
}
|
||||||
|
|
||||||
let existingSeasons: 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
|
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||||
@@ -335,10 +479,10 @@ export class MediaRequest {
|
|||||||
: undefined,
|
: undefined,
|
||||||
is4k: requestBody.is4k,
|
is4k: requestBody.is4k,
|
||||||
serverId: requestBody.serverId,
|
serverId: requestBody.serverId,
|
||||||
profileId: requestBody.profileId,
|
profileId: profileId,
|
||||||
rootFolder: requestBody.rootFolder,
|
rootFolder: rootFolder,
|
||||||
languageProfileId: requestBody.languageProfileId,
|
languageProfileId: requestBody.languageProfileId,
|
||||||
tags: requestBody.tags,
|
tags: tags,
|
||||||
seasons: finalSeasons.map(
|
seasons: finalSeasons.map(
|
||||||
(sn) =>
|
(sn) =>
|
||||||
new SeasonRequest({
|
new SeasonRequest({
|
||||||
@@ -575,18 +719,26 @@ export class MediaRequest {
|
|||||||
// Do not update the status if the item is already partially available or available
|
// Do not update the status if the item is already partially available or available
|
||||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
|
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
|
||||||
media[this.is4k ? 'status4k' : 'status'] !==
|
media[this.is4k ? 'status4k' : 'status'] !==
|
||||||
MediaStatus.PARTIALLY_AVAILABLE
|
MediaStatus.PARTIALLY_AVAILABLE &&
|
||||||
|
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
|
||||||
) {
|
) {
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
|
const statusField = this.is4k ? 'status4k' : 'status';
|
||||||
mediaRepository.save(media);
|
|
||||||
|
await mediaRepository.update(
|
||||||
|
{ id: this.media.id },
|
||||||
|
{ [statusField]: MediaStatus.PROCESSING }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
media.mediaType === MediaType.MOVIE &&
|
media.mediaType === MediaType.MOVIE &&
|
||||||
this.status === MediaRequestStatus.DECLINED
|
this.status === MediaRequestStatus.DECLINED
|
||||||
) {
|
) {
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
const statusField = this.is4k ? 'status4k' : 'status';
|
||||||
mediaRepository.save(media);
|
await mediaRepository.update(
|
||||||
|
{ id: this.media.id },
|
||||||
|
{ [statusField]: MediaStatus.UNKNOWN }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -603,8 +755,11 @@ export class MediaRequest {
|
|||||||
).length === 0 &&
|
).length === 0 &&
|
||||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
|
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
|
||||||
) {
|
) {
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
const statusField = this.is4k ? 'status4k' : 'status';
|
||||||
mediaRepository.save(media);
|
mediaRepository.update(
|
||||||
|
{ id: this.media.id },
|
||||||
|
{ [statusField]: MediaStatus.UNKNOWN }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approve child seasons if parent is approved
|
// Approve child seasons if parent is approved
|
||||||
@@ -806,8 +961,10 @@ export class MediaRequest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
this.status = MediaRequestStatus.APPROVED;
|
|
||||||
await requestRepository.save(this);
|
await requestRepository.update(this.id, {
|
||||||
|
status: MediaRequestStatus.APPROVED,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,18 +994,22 @@ export class MediaRequest {
|
|||||||
throw new Error('Media data not found');
|
throw new Error('Media data not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
const updateFields = {
|
||||||
radarrMovie.id;
|
[this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
|
||||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
radarrMovie.id,
|
||||||
radarrMovie.titleSlug;
|
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
|
||||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = radarrSettings?.id;
|
radarrMovie.titleSlug,
|
||||||
await mediaRepository.save(media);
|
[this.is4k ? 'serviceId4k' : 'serviceId']: radarrSettings?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await mediaRepository.update({ id: this.media.id }, updateFields);
|
||||||
})
|
})
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
this.status = MediaRequestStatus.FAILED;
|
await requestRepository.update(this.id, {
|
||||||
requestRepository.save(this);
|
status: MediaRequestStatus.FAILED,
|
||||||
|
});
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'Something went wrong sending movie request to Radarr, marking status as FAILED',
|
'Something went wrong sending movie request to Radarr, marking status as FAILED',
|
||||||
@@ -861,6 +1022,14 @@ export class MediaRequest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
this.sendNotification(media, Notification.MEDIA_FAILED);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
radarr.clearCache({
|
||||||
|
tmdbId: movie.id,
|
||||||
|
externalId: this.is4k
|
||||||
|
? media.externalServiceId4k
|
||||||
|
: media.externalServiceId,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
logger.info('Sent request to Radarr', {
|
logger.info('Sent request to Radarr', {
|
||||||
label: 'Media Request',
|
label: 'Media Request',
|
||||||
@@ -956,8 +1125,9 @@ export class MediaRequest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
this.status = MediaRequestStatus.APPROVED;
|
await requestRepository.update(this.id, {
|
||||||
await requestRepository.save(this);
|
status: MediaRequestStatus.APPROVED,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,18 +1288,23 @@ export class MediaRequest {
|
|||||||
throw new Error('Media data not found');
|
throw new Error('Media data not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
const updateFields = {
|
||||||
sonarrSeries.id;
|
[this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
|
||||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
sonarrSeries.id,
|
||||||
sonarrSeries.titleSlug;
|
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
|
||||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
|
sonarrSeries.titleSlug,
|
||||||
await mediaRepository.save(media);
|
[this.is4k ? 'serviceId4k' : 'serviceId']: sonarrSettings?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await mediaRepository.update({ id: this.media.id }, updateFields);
|
||||||
})
|
})
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
this.status = MediaRequestStatus.FAILED;
|
await requestRepository.update(
|
||||||
requestRepository.save(this);
|
{ id: this.id },
|
||||||
|
{ status: MediaRequestStatus.FAILED }
|
||||||
|
);
|
||||||
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
||||||
@@ -1142,6 +1317,15 @@ export class MediaRequest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
this.sendNotification(media, Notification.MEDIA_FAILED);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
sonarr.clearCache({
|
||||||
|
tvdbId,
|
||||||
|
externalId: this.is4k
|
||||||
|
? media.externalServiceId4k
|
||||||
|
: media.externalServiceId,
|
||||||
|
title: series.name,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
logger.info('Sent request to Sonarr', {
|
logger.info('Sent request to Sonarr', {
|
||||||
label: 'Media Request',
|
label: 'Media Request',
|
||||||
|
|||||||
52
server/entity/OverrideRule.ts
Normal file
52
server/entity/OverrideRule.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class OverrideRule {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
public id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
public radarrServiceId?: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
public sonarrServiceId?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public users?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public genre?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public language?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public keywords?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
public profileId?: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public rootFolder?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public tags?: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
public updatedAt: Date;
|
||||||
|
|
||||||
|
constructor(init?: Partial<OverrideRule>) {
|
||||||
|
Object.assign(this, init);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverrideRule;
|
||||||
@@ -23,7 +23,9 @@ class Season {
|
|||||||
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
||||||
public status4k: MediaStatus;
|
public status4k: MediaStatus;
|
||||||
|
|
||||||
@ManyToOne(() => Media, (media) => media.seasons, { onDelete: 'CASCADE' })
|
@ManyToOne(() => Media, (media) => media.seasons, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
public media: Promise<Media>;
|
public media: Promise<Media>;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ export class User {
|
|||||||
})
|
})
|
||||||
public email: string;
|
public email: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
public plexUsername?: string;
|
public plexUsername?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
public jellyfinUsername?: string;
|
public jellyfinUsername?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public username?: string;
|
public username?: string;
|
||||||
@@ -77,20 +77,20 @@ export class User {
|
|||||||
@Column({ type: 'integer', default: UserType.PLEX })
|
@Column({ type: 'integer', default: UserType.PLEX })
|
||||||
public userType: UserType;
|
public userType: UserType;
|
||||||
|
|
||||||
@Column({ nullable: true, select: true })
|
@Column({ type: 'integer', nullable: true, select: true })
|
||||||
public plexId?: number;
|
public plexId?: number | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
public jellyfinUserId?: string;
|
public jellyfinUserId?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ type: 'varchar', nullable: true, select: false })
|
||||||
public jellyfinDeviceId?: string;
|
public jellyfinDeviceId?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ type: 'varchar', nullable: true, select: false })
|
||||||
public jellyfinAuthToken?: string;
|
public jellyfinAuthToken?: string | null;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ type: 'varchar', nullable: true, select: false })
|
||||||
public plexToken?: string;
|
public plexToken?: string | null;
|
||||||
|
|
||||||
@Column({ type: 'integer', default: 0 })
|
@Column({ type: 'integer', default: 0 })
|
||||||
public permissions = 0;
|
public permissions = 0;
|
||||||
@@ -98,6 +98,12 @@ export class User {
|
|||||||
@Column()
|
@Column()
|
||||||
public avatar: string;
|
public avatar: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
public avatarETag?: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
public avatarVersion?: string | null;
|
||||||
|
|
||||||
@RelationCount((user: User) => user.requests)
|
@RelationCount((user: User) => user.requests)
|
||||||
public requestCount: number;
|
public requestCount: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -18,9 +24,15 @@ export class UserPushSubscription {
|
|||||||
@Column()
|
@Column()
|
||||||
public p256dh: string;
|
public p256dh: string;
|
||||||
|
|
||||||
@Column({ unique: true })
|
@Column()
|
||||||
public auth: string;
|
public auth: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public userAgent: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ nullable: true })
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
constructor(init?: Partial<UserPushSubscription>) {
|
constructor(init?: Partial<UserPushSubscription>) {
|
||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ export class UserSettings {
|
|||||||
public locale?: string;
|
public locale?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public region?: string;
|
public discoverRegion?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public streamingRegion?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public originalLanguage?: string;
|
public originalLanguage?: string;
|
||||||
@@ -57,6 +60,9 @@ export class UserSettings {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public telegramChatId?: string;
|
public telegramChatId?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public telegramMessageThreadId?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
public telegramSendSilently?: boolean;
|
public telegramSendSilently?: boolean;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user