Compare commits
127 Commits
preview-je
...
preview-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e18a272a | ||
|
|
6957b7606e | ||
|
|
0154e6b538 | ||
|
|
3d0166aaef | ||
|
|
2fc8996606 | ||
|
|
ef5e954db1 | ||
|
|
39a5ccb7f3 | ||
|
|
9b151feb4f | ||
|
|
fe5d016929 | ||
|
|
14f316a9a6 | ||
|
|
5c24e79b1d | ||
|
|
ba84212e68 | ||
|
|
f25b32aec8 | ||
|
|
5a13226877 | ||
|
|
694913c767 | ||
|
|
a2d2fd3c2a | ||
|
|
cb94ad5a2e | ||
|
|
2829c2548a | ||
|
|
64f4610b9f | ||
|
|
830f431e01 | ||
|
|
94efdf7a18 | ||
|
|
ab5cdf5464 | ||
|
|
d3805d99e8 | ||
|
|
7a5ee18e2c | ||
|
|
96591a9ddd | ||
|
|
46c3af115a | ||
|
|
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 | ||
|
|
4e63cee12b | ||
|
|
5ee29823c6 | ||
|
|
2add7af5ec | ||
|
|
3b12c98242 | ||
|
|
0c7e652672 | ||
|
|
bd4da6d5fc | ||
|
|
12f908de7f | ||
|
|
61dcd8e487 | ||
|
|
9aee8887d3 | ||
|
|
2348f23f43 | ||
|
|
bd7339a105 | ||
|
|
66e308ba1d | ||
|
|
357b927ab3 | ||
|
|
f8926fa86c | ||
|
|
e2992fea9c | ||
|
|
2b0d497370 | ||
|
|
028012185c | ||
|
|
94a9806089 | ||
|
|
caaed7c8b8 | ||
|
|
29452648e6 | ||
|
|
f6b5a6fe9f | ||
|
|
cf4e3fd579 | ||
|
|
326d2cb4ca | ||
|
|
ff7bb884ae | ||
|
|
ef78fdd534 | ||
|
|
637e7dbd8e | ||
|
|
3ebf47fe61 | ||
|
|
88b67686ff | ||
|
|
86444b80b9 | ||
|
|
5f7679982a | ||
|
|
610c372498 | ||
|
|
7aca0be41c | ||
|
|
0bab6887b0 | ||
|
|
0d6a1f12fb | ||
|
|
87c8444ec6 | ||
|
|
f08c537cba | ||
|
|
b1b4dd9cfc | ||
|
|
eb111ac1db | ||
|
|
106cd195d4 | ||
|
|
42ad4e0ae3 | ||
|
|
501859207a | ||
|
|
8c7004c50d | ||
|
|
b594dec992 | ||
|
|
e93ab06504 | ||
|
|
0581d7b6ad | ||
|
|
00c811d10d | ||
|
|
14d3ec22b0 | ||
|
|
ed57911c7c | ||
|
|
e6cc2c55a2 | ||
|
|
4d85f29843 | ||
|
|
44aaca0fb2 | ||
|
|
abd80c1fa8 | ||
|
|
b6592bf9f7 | ||
|
|
573b64f901 | ||
|
|
b39a5a7d82 | ||
|
|
325e2ed6d3 | ||
|
|
e7c11da52b | ||
|
|
5712e19804 | ||
|
|
4b549763e5 | ||
|
|
24151d27f7 | ||
|
|
f3cc8cba0a | ||
|
|
57e7d68092 | ||
|
|
d3622f7bb3 | ||
|
|
20c821e2eb | ||
|
|
7b82ced5e6 |
@@ -439,6 +439,78 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
10
.github/ISSUE_TEMPLATE/bug.yml
vendored
10
.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,14 @@ 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?
|
||||||
- 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:
|
||||||
|
|||||||
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*
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ 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
|
||||||
pnpm
|
pnpm install
|
||||||
pnpm 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.
|
||||||
|
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -8,10 +8,10 @@
|
|||||||
<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-47-orange.svg"/></a>
|
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-48-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.
|
||||||
@@ -146,6 +146,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/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/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/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/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>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -290,6 +291,12 @@ 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>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
services:
|
||||||
jellyseerr:
|
jellyseerr:
|
||||||
build:
|
build:
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
"types": 0,
|
"types": 0,
|
||||||
"options": {
|
"options": {
|
||||||
"webhookUrl": "",
|
"webhookUrl": "",
|
||||||
|
"webhookRoleId": "",
|
||||||
"enableMentions": true
|
"enableMentions": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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:
|
||||||
@@ -17,6 +17,7 @@ Welcome to the Jellyseerr Documentation.
|
|||||||
- **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.
|
||||||
- More features to come!
|
- More features to come!
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|||||||
56
docs/extending-jellyseerr/database-config.mdx
Normal file
56
docs/extending-jellyseerr/database-config.mdx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite". To use postgres, this needs to be set to "postgres"
|
||||||
|
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".
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
- Edit the postgres connection string to match your setup
|
||||||
|
- WARNING: The most recent release of pgloader has an issue quoting the table columns. Use the version in the docker container to avoid this issue.
|
||||||
|
- "I 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.
|
||||||
|
```bash
|
||||||
|
docker run --rm -v config/db.sqlite3:/db.sqlite3:ro -v pgloader/pgloader.load:/pgloader.load 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
|
||||||
@@ -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';
|
||||||
@@ -190,7 +192,7 @@ Caddy will automatically obtain and renew SSL certificates for your domain.
|
|||||||
|
|
||||||
## Traefik (v2)
|
## Traefik (v2)
|
||||||
|
|
||||||
Add the following labels to the Jellyseerr service in your `docker-compose.yml` file:
|
Add the following labels to the Jellyseerr service in your `compose.yaml` file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -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.
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -12,49 +12,12 @@ import Tabs from '@theme/Tabs';
|
|||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
<Tabs groupId="versions" queryString>
|
|
||||||
<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/)
|
- [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" queryString>
|
|
||||||
<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
|
||||||
@@ -77,8 +40,6 @@ 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.
|
||||||
@@ -234,33 +195,6 @@ pm2 status jellyseerr
|
|||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
### Installation
|
### Installation
|
||||||
<Tabs groupId="versions" queryString>
|
|
||||||
<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
|
|
||||||
npm install -g win-node-env
|
|
||||||
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
|
||||||
@@ -284,8 +218,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.
|
||||||
@@ -313,6 +245,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.
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ 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:
|
||||||
@@ -94,17 +94,17 @@ If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable
|
|||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const VersionMismatchWarning = () => {
|
|||||||
<>
|
<>
|
||||||
{!isUpToDate ? (
|
{!isUpToDate ? (
|
||||||
<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 <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-derivation">override the package derivation</a>.
|
||||||
</Admonition>
|
</Admonition>
|
||||||
) : (
|
) : (
|
||||||
<Admonition type="success">
|
<Admonition type="success">
|
||||||
@@ -95,12 +95,12 @@ export const VersionMatch = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
offlineCache = pkgs.fetchYarnDeps {
|
offlineCache = pkgs.fetchYarnDeps {
|
||||||
sha256 = pkgs.lib.fakeSha256;
|
sha256 = pkgs.lib.fakeSha256;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const module = `{ config, pkgs, lib, ... }:
|
const module = `{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
|
|||||||
@@ -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,6 +12,8 @@ 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!
|
||||||
|
|||||||
@@ -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) to suit their own preferences.
|
||||||
|
|
||||||
### Movie Request Limit & Series Request Limit
|
### Movie Request Limit & Series Request Limit
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,11 @@ module.exports = {
|
|||||||
commitTag: process.env.COMMIT_TAG || 'local',
|
commitTag: process.env.COMMIT_TAG || 'local',
|
||||||
forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false',
|
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' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -1258,6 +1273,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
webhookUrl:
|
webhookUrl:
|
||||||
type: string
|
type: string
|
||||||
|
webhookRoleId:
|
||||||
|
type: string
|
||||||
enableMentions:
|
enableMentions:
|
||||||
type: boolean
|
type: boolean
|
||||||
SlackSettings:
|
SlackSettings:
|
||||||
@@ -1973,6 +1990,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
|
||||||
@@ -2775,6 +2795,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 +3615,8 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
|
serverType:
|
||||||
|
type: number
|
||||||
required:
|
required:
|
||||||
- username
|
- username
|
||||||
- password
|
- password
|
||||||
@@ -4040,6 +4071,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
|
||||||
@@ -4862,6 +4996,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
|
||||||
@@ -5347,7 +5486,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 +5592,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
|
||||||
|
|||||||
@@ -62,12 +62,14 @@
|
|||||||
"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",
|
||||||
|
"mime": "3",
|
||||||
"next": "^14.2.4",
|
"next": "^14.2.4",
|
||||||
"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.9.1",
|
||||||
"openpgp": "5.7.0",
|
"openpgp": "5.7.0",
|
||||||
|
"pg": "8.11.0",
|
||||||
"plex-api": "5.3.2",
|
"plex-api": "5.3.2",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -92,7 +94,8 @@
|
|||||||
"sqlite3": "5.1.4",
|
"sqlite3": "5.1.4",
|
||||||
"swagger-ui-express": "4.6.2",
|
"swagger-ui-express": "4.6.2",
|
||||||
"swr": "2.2.5",
|
"swr": "2.2.5",
|
||||||
"typeorm": "0.3.12",
|
"typeorm": "0.3.11",
|
||||||
|
"undici": "^6.20.1",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"winston": "3.8.2",
|
"winston": "3.8.2",
|
||||||
"winston-daily-rotate-file": "4.7.1",
|
"winston-daily-rotate-file": "4.7.1",
|
||||||
@@ -119,6 +122,7 @@
|
|||||||
"@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/mime": "3",
|
||||||
"@types/node": "20.14.8",
|
"@types/node": "20.14.8",
|
||||||
"@types/node-schedule": "2.1.0",
|
"@types/node-schedule": "2.1.0",
|
||||||
"@types/nodemailer": "6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
|
|||||||
208
pnpm-lock.yaml
generated
208
pnpm-lock.yaml
generated
@@ -49,7 +49,7 @@ importers:
|
|||||||
version: 2.11.0
|
version: 2.11.0
|
||||||
connect-typeorm:
|
connect-typeorm:
|
||||||
specifier: 1.1.4
|
specifier: 1.1.4
|
||||||
version: 1.1.4(typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))
|
version: 1.1.4(typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)))
|
||||||
cookie-parser:
|
cookie-parser:
|
||||||
specifier: 1.4.6
|
specifier: 1.4.6
|
||||||
version: 1.4.6
|
version: 1.4.6
|
||||||
@@ -98,6 +98,9 @@ importers:
|
|||||||
lodash:
|
lodash:
|
||||||
specifier: 4.17.21
|
specifier: 4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
mime:
|
||||||
|
specifier: '3'
|
||||||
|
version: 3.0.0
|
||||||
next:
|
next:
|
||||||
specifier: ^14.2.4
|
specifier: ^14.2.4
|
||||||
version: 14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
@@ -116,6 +119,9 @@ importers:
|
|||||||
openpgp:
|
openpgp:
|
||||||
specifier: 5.7.0
|
specifier: 5.7.0
|
||||||
version: 5.7.0
|
version: 5.7.0
|
||||||
|
pg:
|
||||||
|
specifier: 8.11.0
|
||||||
|
version: 8.11.0
|
||||||
plex-api:
|
plex-api:
|
||||||
specifier: 5.3.2
|
specifier: 5.3.2
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
@@ -189,8 +195,11 @@ importers:
|
|||||||
specifier: 2.2.5
|
specifier: 2.2.5
|
||||||
version: 2.2.5(react@18.3.1)
|
version: 2.2.5(react@18.3.1)
|
||||||
typeorm:
|
typeorm:
|
||||||
specifier: 0.3.12
|
specifier: 0.3.11
|
||||||
version: 0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
|
version: 0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
|
||||||
|
undici:
|
||||||
|
specifier: ^6.20.1
|
||||||
|
version: 6.20.1
|
||||||
web-push:
|
web-push:
|
||||||
specifier: 3.5.0
|
specifier: 3.5.0
|
||||||
version: 3.5.0
|
version: 3.5.0
|
||||||
@@ -264,6 +273,9 @@ importers:
|
|||||||
'@types/lodash':
|
'@types/lodash':
|
||||||
specifier: 4.14.191
|
specifier: 4.14.191
|
||||||
version: 4.14.191
|
version: 4.14.191
|
||||||
|
'@types/mime':
|
||||||
|
specifier: '3'
|
||||||
|
version: 3.0.4
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 20.14.8
|
specifier: 20.14.8
|
||||||
version: 20.14.8
|
version: 20.14.8
|
||||||
@@ -2848,6 +2860,9 @@ packages:
|
|||||||
'@types/mime@1.3.5':
|
'@types/mime@1.3.5':
|
||||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||||
|
|
||||||
|
'@types/mime@3.0.4':
|
||||||
|
resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==}
|
||||||
|
|
||||||
'@types/minimatch@3.0.5':
|
'@types/minimatch@3.0.5':
|
||||||
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
|
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
|
||||||
|
|
||||||
@@ -3518,6 +3533,10 @@ packages:
|
|||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
|
buffer-writer@2.0.0:
|
||||||
|
resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
@@ -4255,10 +4274,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
||||||
engines: {node: '>=0.11'}
|
engines: {node: '>=0.11'}
|
||||||
|
|
||||||
date-fns@2.30.0:
|
|
||||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
|
||||||
engines: {node: '>=0.11'}
|
|
||||||
|
|
||||||
dateformat@3.0.3:
|
dateformat@3.0.3:
|
||||||
resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
|
resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==}
|
||||||
|
|
||||||
@@ -5380,8 +5395,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
https-proxy-agent@7.0.4:
|
https-proxy-agent@7.0.5:
|
||||||
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
|
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
human-signals@1.1.1:
|
human-signals@1.1.1:
|
||||||
@@ -6545,11 +6560,6 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
mkdirp@2.1.6:
|
|
||||||
resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
modify-values@1.0.1:
|
modify-values@1.0.1:
|
||||||
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
|
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -7047,6 +7057,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==}
|
resolution: {integrity: sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
packet-reader@1.0.0:
|
||||||
|
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -7138,6 +7151,40 @@ packages:
|
|||||||
performance-now@2.1.0:
|
performance-now@2.1.0:
|
||||||
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||||
|
|
||||||
|
pg-cloudflare@1.1.1:
|
||||||
|
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
|
||||||
|
|
||||||
|
pg-connection-string@2.7.0:
|
||||||
|
resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==}
|
||||||
|
|
||||||
|
pg-int8@1.0.1:
|
||||||
|
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
pg-pool@3.7.0:
|
||||||
|
resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==}
|
||||||
|
peerDependencies:
|
||||||
|
pg: '>=8.0'
|
||||||
|
|
||||||
|
pg-protocol@1.7.0:
|
||||||
|
resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==}
|
||||||
|
|
||||||
|
pg-types@2.2.0:
|
||||||
|
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
pg@8.11.0:
|
||||||
|
resolution: {integrity: sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==}
|
||||||
|
engines: {node: '>= 8.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
pg-native: '>=3.0.1'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
pg-native:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pgpass@1.0.5:
|
||||||
|
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||||
|
|
||||||
picocolors@1.0.1:
|
picocolors@1.0.1:
|
||||||
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
|
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
|
||||||
|
|
||||||
@@ -7243,6 +7290,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
postgres-array@2.0.0:
|
||||||
|
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
postgres-bytea@1.0.0:
|
||||||
|
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-date@1.0.7:
|
||||||
|
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-interval@1.2.0:
|
||||||
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
prelude-ls@1.2.1:
|
prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -7721,9 +7784,6 @@ packages:
|
|||||||
reflect-metadata@0.1.13:
|
reflect-metadata@0.1.13:
|
||||||
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
||||||
|
|
||||||
reflect-metadata@0.1.14:
|
|
||||||
resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
|
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.6:
|
reflect.getprototypeof@1.0.6:
|
||||||
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
|
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -8156,6 +8216,10 @@ packages:
|
|||||||
split2@3.2.2:
|
split2@3.2.2:
|
||||||
resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
|
resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
|
||||||
|
|
||||||
|
split2@4.2.0:
|
||||||
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
split@1.0.1:
|
split@1.0.1:
|
||||||
resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==}
|
resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==}
|
||||||
|
|
||||||
@@ -8661,8 +8725,8 @@ packages:
|
|||||||
typedarray@0.0.6:
|
typedarray@0.0.6:
|
||||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||||
|
|
||||||
typeorm@0.3.12:
|
typeorm@0.3.11:
|
||||||
resolution: {integrity: sha512-sYSxBmCf1nJLLTcYtwqZ+lQIRtLPyUoO93rHTOKk9vJCyT4UfRtU7oRsJvfvKP3nnZTD1hzz2SEy2zwPEN6OyA==}
|
resolution: {integrity: sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==}
|
||||||
engines: {node: '>= 12.9.0'}
|
engines: {node: '>= 12.9.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -8673,7 +8737,7 @@ packages:
|
|||||||
ioredis: ^5.0.4
|
ioredis: ^5.0.4
|
||||||
mongodb: ^3.6.0
|
mongodb: ^3.6.0
|
||||||
mssql: ^7.3.0
|
mssql: ^7.3.0
|
||||||
mysql2: ^2.2.5 || ^3.0.1
|
mysql2: ^2.2.5
|
||||||
oracledb: ^5.1.0
|
oracledb: ^5.1.0
|
||||||
pg: ^8.5.1
|
pg: ^8.5.1
|
||||||
pg-native: ^3.0.0
|
pg-native: ^3.0.0
|
||||||
@@ -8759,6 +8823,10 @@ packages:
|
|||||||
undici-types@5.26.5:
|
undici-types@5.26.5:
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
|
undici@6.20.1:
|
||||||
|
resolution: {integrity: sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==}
|
||||||
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.0:
|
unicode-canonical-property-names-ecmascript@2.0.0:
|
||||||
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -10836,7 +10904,7 @@ snapshots:
|
|||||||
nopt: 5.0.0
|
nopt: 5.0.0
|
||||||
npmlog: 5.0.1
|
npmlog: 5.0.1
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
semver: 7.6.2
|
semver: 7.3.8
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
@@ -10911,13 +10979,13 @@ snapshots:
|
|||||||
'@npmcli/fs@1.1.1':
|
'@npmcli/fs@1.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@gar/promisify': 1.1.3
|
'@gar/promisify': 1.1.3
|
||||||
semver: 7.6.2
|
semver: 7.3.8
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@npmcli/fs@2.1.2':
|
'@npmcli/fs@2.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@gar/promisify': 1.1.3
|
'@gar/promisify': 1.1.3
|
||||||
semver: 7.6.2
|
semver: 7.3.8
|
||||||
|
|
||||||
'@npmcli/move-file@1.1.2':
|
'@npmcli/move-file@1.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -12301,7 +12369,7 @@ snapshots:
|
|||||||
fs-extra: 11.2.0
|
fs-extra: 11.2.0
|
||||||
globby: 11.1.0
|
globby: 11.1.0
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.4
|
https-proxy-agent: 7.0.5
|
||||||
issue-parser: 6.0.0
|
issue-parser: 6.0.0
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
@@ -12326,7 +12394,7 @@ snapshots:
|
|||||||
read-pkg: 5.2.0
|
read-pkg: 5.2.0
|
||||||
registry-auth-token: 5.0.2
|
registry-auth-token: 5.0.2
|
||||||
semantic-release: 19.0.5(encoding@0.1.13)
|
semantic-release: 19.0.5(encoding@0.1.13)
|
||||||
semver: 7.6.2
|
semver: 7.3.8
|
||||||
tempy: 1.0.1
|
tempy: 1.0.1
|
||||||
|
|
||||||
'@semantic-release/release-notes-generator@10.0.3(semantic-release@19.0.5(encoding@0.1.13))':
|
'@semantic-release/release-notes-generator@10.0.3(semantic-release@19.0.5(encoding@0.1.13))':
|
||||||
@@ -12670,6 +12738,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/mime@1.3.5': {}
|
'@types/mime@1.3.5': {}
|
||||||
|
|
||||||
|
'@types/mime@3.0.4': {}
|
||||||
|
|
||||||
'@types/minimatch@3.0.5': {}
|
'@types/minimatch@3.0.5': {}
|
||||||
|
|
||||||
'@types/minimist@1.2.5': {}
|
'@types/minimist@1.2.5': {}
|
||||||
@@ -12887,7 +12957,7 @@ snapshots:
|
|||||||
debug: 4.3.5(supports-color@8.1.1)
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
globby: 11.1.0
|
globby: 11.1.0
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
semver: 7.6.2
|
semver: 7.3.8
|
||||||
tsutils: 3.21.0(typescript@4.9.5)
|
tsutils: 3.21.0(typescript@4.9.5)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 4.9.5
|
typescript: 4.9.5
|
||||||
@@ -13423,6 +13493,8 @@ snapshots:
|
|||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
|
buffer-writer@2.0.0: {}
|
||||||
|
|
||||||
buffer@5.7.1:
|
buffer@5.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@@ -13813,13 +13885,13 @@ snapshots:
|
|||||||
ini: 1.3.8
|
ini: 1.3.8
|
||||||
proto-list: 1.2.4
|
proto-list: 1.2.4
|
||||||
|
|
||||||
connect-typeorm@1.1.4(typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))):
|
connect-typeorm@1.1.4(typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/debug': 0.0.31
|
'@types/debug': 0.0.31
|
||||||
'@types/express-session': 1.17.6
|
'@types/express-session': 1.17.6
|
||||||
debug: 4.3.5(supports-color@8.1.1)
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
express-session: 1.18.0
|
express-session: 1.18.0
|
||||||
typeorm: 0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
|
typeorm: 0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -14170,10 +14242,6 @@ snapshots:
|
|||||||
|
|
||||||
date-fns@2.29.3: {}
|
date-fns@2.29.3: {}
|
||||||
|
|
||||||
date-fns@2.30.0:
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.24.7
|
|
||||||
|
|
||||||
dateformat@3.0.3: {}
|
dateformat@3.0.3: {}
|
||||||
|
|
||||||
dayjs@1.11.11: {}
|
dayjs@1.11.11: {}
|
||||||
@@ -15728,7 +15796,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
https-proxy-agent@7.0.4:
|
https-proxy-agent@7.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.1
|
agent-base: 7.1.1
|
||||||
debug: 4.3.5(supports-color@8.1.1)
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
@@ -17138,8 +17206,6 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
mkdirp@2.1.6: {}
|
|
||||||
|
|
||||||
modify-values@1.0.1: {}
|
modify-values@1.0.1: {}
|
||||||
|
|
||||||
moment@2.30.1: {}
|
moment@2.30.1: {}
|
||||||
@@ -17269,7 +17335,7 @@ snapshots:
|
|||||||
nopt: 5.0.0
|
nopt: 5.0.0
|
||||||
npmlog: 6.0.2
|
npmlog: 6.0.2
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
semver: 7.6.2
|
semver: 7.3.8
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -17348,7 +17414,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hosted-git-info: 4.1.0
|
hosted-git-info: 4.1.0
|
||||||
is-core-module: 2.14.0
|
is-core-module: 2.14.0
|
||||||
semver: 7.6.2
|
semver: 7.3.8
|
||||||
validate-npm-package-license: 3.0.4
|
validate-npm-package-license: 3.0.4
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
@@ -17578,6 +17644,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-timeout: 3.2.0
|
p-timeout: 3.2.0
|
||||||
|
|
||||||
|
packet-reader@1.0.0: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@@ -17656,6 +17724,43 @@ snapshots:
|
|||||||
|
|
||||||
performance-now@2.1.0: {}
|
performance-now@2.1.0: {}
|
||||||
|
|
||||||
|
pg-cloudflare@1.1.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg-connection-string@2.7.0: {}
|
||||||
|
|
||||||
|
pg-int8@1.0.1: {}
|
||||||
|
|
||||||
|
pg-pool@3.7.0(pg@8.11.0):
|
||||||
|
dependencies:
|
||||||
|
pg: 8.11.0
|
||||||
|
|
||||||
|
pg-protocol@1.7.0: {}
|
||||||
|
|
||||||
|
pg-types@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
pg-int8: 1.0.1
|
||||||
|
postgres-array: 2.0.0
|
||||||
|
postgres-bytea: 1.0.0
|
||||||
|
postgres-date: 1.0.7
|
||||||
|
postgres-interval: 1.2.0
|
||||||
|
|
||||||
|
pg@8.11.0:
|
||||||
|
dependencies:
|
||||||
|
buffer-writer: 2.0.0
|
||||||
|
packet-reader: 1.0.0
|
||||||
|
pg-connection-string: 2.7.0
|
||||||
|
pg-pool: 3.7.0(pg@8.11.0)
|
||||||
|
pg-protocol: 1.7.0
|
||||||
|
pg-types: 2.2.0
|
||||||
|
pgpass: 1.0.5
|
||||||
|
optionalDependencies:
|
||||||
|
pg-cloudflare: 1.1.1
|
||||||
|
|
||||||
|
pgpass@1.0.5:
|
||||||
|
dependencies:
|
||||||
|
split2: 4.2.0
|
||||||
|
|
||||||
picocolors@1.0.1: {}
|
picocolors@1.0.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
@@ -17753,6 +17858,16 @@ snapshots:
|
|||||||
picocolors: 1.0.1
|
picocolors: 1.0.1
|
||||||
source-map-js: 1.2.0
|
source-map-js: 1.2.0
|
||||||
|
|
||||||
|
postgres-array@2.0.0: {}
|
||||||
|
|
||||||
|
postgres-bytea@1.0.0: {}
|
||||||
|
|
||||||
|
postgres-date@1.0.7: {}
|
||||||
|
|
||||||
|
postgres-interval@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
xtend: 4.0.2
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier-linter-helpers@1.0.0:
|
prettier-linter-helpers@1.0.0:
|
||||||
@@ -18361,8 +18476,6 @@ snapshots:
|
|||||||
|
|
||||||
reflect-metadata@0.1.13: {}
|
reflect-metadata@0.1.13: {}
|
||||||
|
|
||||||
reflect-metadata@0.1.14: {}
|
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.6:
|
reflect.getprototypeof@1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
@@ -18884,6 +18997,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
|
split2@4.2.0: {}
|
||||||
|
|
||||||
split@1.0.1:
|
split@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
through: 2.3.8
|
through: 2.3.8
|
||||||
@@ -19420,26 +19535,27 @@ snapshots:
|
|||||||
|
|
||||||
typedarray@0.0.6: {}
|
typedarray@0.0.6: {}
|
||||||
|
|
||||||
typeorm@0.3.12(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)):
|
typeorm@0.3.11(pg@8.11.0)(sqlite3@5.1.4(encoding@0.1.13))(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sqltools/formatter': 1.2.5
|
'@sqltools/formatter': 1.2.5
|
||||||
app-root-path: 3.1.0
|
app-root-path: 3.1.0
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cli-highlight: 2.1.11
|
cli-highlight: 2.1.11
|
||||||
date-fns: 2.30.0
|
date-fns: 2.29.3
|
||||||
debug: 4.3.5(supports-color@8.1.1)
|
debug: 4.3.5(supports-color@8.1.1)
|
||||||
dotenv: 16.4.5
|
dotenv: 16.4.5
|
||||||
glob: 8.1.0
|
glob: 7.2.3
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
mkdirp: 2.1.6
|
mkdirp: 1.0.4
|
||||||
reflect-metadata: 0.1.14
|
reflect-metadata: 0.1.13
|
||||||
sha.js: 2.4.11
|
sha.js: 2.4.11
|
||||||
tslib: 2.6.3
|
tslib: 2.6.3
|
||||||
uuid: 9.0.1
|
uuid: 8.3.2
|
||||||
xml2js: 0.4.23
|
xml2js: 0.4.23
|
||||||
yargs: 17.7.2
|
yargs: 17.7.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
pg: 8.11.0
|
||||||
sqlite3: 5.1.4(encoding@0.1.13)
|
sqlite3: 5.1.4(encoding@0.1.13)
|
||||||
ts-node: 10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)
|
ts-node: 10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@20.14.8)(typescript@4.9.5)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -19475,6 +19591,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@5.26.5: {}
|
undici-types@5.26.5: {}
|
||||||
|
|
||||||
|
undici@6.20.1: {}
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.0: {}
|
unicode-canonical-property-names-ecmascript@2.0.0: {}
|
||||||
|
|
||||||
unicode-emoji-utils@1.2.0:
|
unicode-emoji-utils@1.2.0:
|
||||||
|
|||||||
@@ -32,13 +32,27 @@ class ExternalAPI {
|
|||||||
this.fetch = fetch;
|
this.fetch = fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.baseUrl = baseUrl;
|
const url = new URL(baseUrl);
|
||||||
this.params = params;
|
|
||||||
this.defaultHeaders = {
|
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')}`,
|
||||||
|
}),
|
||||||
...options.headers,
|
...options.headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (url.username || url.password) {
|
||||||
|
url.username = '';
|
||||||
|
url.password = '';
|
||||||
|
baseUrl = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
this.params = params;
|
||||||
this.cache = options.nodeCache;
|
this.cache = options.nodeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +90,7 @@ class ExternalAPI {
|
|||||||
}
|
}
|
||||||
const data = await this.getDataFromResponse(response);
|
const data = await this.getDataFromResponse(response);
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache && ttl !== 0) {
|
||||||
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, data, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +99,7 @@ class ExternalAPI {
|
|||||||
|
|
||||||
protected async post<T>(
|
protected async post<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: Record<string, unknown>,
|
data?: Record<string, unknown>,
|
||||||
params?: Record<string, string>,
|
params?: Record<string, string>,
|
||||||
ttl?: number,
|
ttl?: number,
|
||||||
config?: RequestInit
|
config?: RequestInit
|
||||||
@@ -107,7 +121,7 @@ class ExternalAPI {
|
|||||||
...this.defaultHeaders,
|
...this.defaultHeaders,
|
||||||
...config?.headers,
|
...config?.headers,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -120,7 +134,7 @@ class ExternalAPI {
|
|||||||
}
|
}
|
||||||
const resData = await this.getDataFromResponse(response);
|
const resData = await this.getDataFromResponse(response);
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache && ttl !== 0) {
|
||||||
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +178,7 @@ class ExternalAPI {
|
|||||||
}
|
}
|
||||||
const resData = await this.getDataFromResponse(response);
|
const resData = await this.getDataFromResponse(response);
|
||||||
|
|
||||||
if (this.cache) {
|
if (this.cache && ttl !== 0) {
|
||||||
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, resData, ttl ?? DEFAULT_TTL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +300,12 @@ class ExternalAPI {
|
|||||||
...this.params,
|
...this.params,
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
return `${href}?${searchParams.toString()}`;
|
return (
|
||||||
|
href +
|
||||||
|
(searchParams.toString().length
|
||||||
|
? '?' + searchParams.toString()
|
||||||
|
: searchParams.toString())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private serializeCacheKey(
|
private serializeCacheKey(
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class JellyfinAPI extends ExternalAPI {
|
class JellyfinAPI extends ExternalAPI {
|
||||||
private authToken?: string;
|
|
||||||
private userId?: string;
|
private userId?: string;
|
||||||
private jellyfinHost: string;
|
|
||||||
|
|
||||||
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
constructor(jellyfinHost: string, authToken?: string, deviceId?: string) {
|
||||||
let authHeaderVal: string;
|
let authHeaderVal: string;
|
||||||
@@ -114,9 +112,6 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.jellyfinHost = jellyfinHost;
|
|
||||||
this.authToken = authToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async login(
|
public async login(
|
||||||
@@ -143,39 +138,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.cause?.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +197,8 @@ 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.cause?.status, ApiErrorCode.Unknown);
|
throw new ApiError(e.cause?.status, ApiErrorCode.Unknown);
|
||||||
@@ -218,8 +212,8 @@ 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.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
@@ -234,8 +228,8 @@ 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.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
@@ -258,8 +252,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 [];
|
||||||
@@ -313,8 +310,8 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
);
|
);
|
||||||
} 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.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
@@ -334,8 +331,8 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
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.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
@@ -359,8 +356,8 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
}
|
}
|
||||||
@@ -373,8 +370,8 @@ 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.cause?.status, ApiErrorCode.InvalidAuthToken);
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
@@ -398,13 +395,30 @@ 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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default JellyfinAPI;
|
export default JellyfinAPI;
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class PlexAPI {
|
|||||||
settings.plex.libraries = [];
|
settings.plex.libraries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.save();
|
await settings.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLibraryContents(
|
public async getLibraryContents(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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';
|
||||||
|
|
||||||
interface PlexAccountResponse {
|
interface PlexAccountResponse {
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -261,6 +267,11 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
items: PlexWatchlistItem[];
|
items: PlexWatchlistItem[];
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
|
const watchlistCache = cacheManager.getCache('plexwatchlist');
|
||||||
|
let cachedWatchlist = watchlistCache.data.get<PlexWatchlistCache>(
|
||||||
|
this.authToken
|
||||||
|
);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
'X-Plex-Container-Start': offset.toString(),
|
'X-Plex-Container-Start': offset.toString(),
|
||||||
'X-Plex-Container-Size': size.toString(),
|
'X-Plex-Container-Size': size.toString(),
|
||||||
@@ -268,42 +279,62 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
const response = await this.fetch(
|
const response = await this.fetch(
|
||||||
`https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`,
|
`https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`,
|
||||||
{
|
{
|
||||||
headers: this.defaultHeaders,
|
headers: {
|
||||||
|
...this.defaultHeaders,
|
||||||
|
...(cachedWatchlist?.etag
|
||||||
|
? { 'If-None-Match': cachedWatchlist.etag }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const data = (await response.json()) as WatchlistResponse;
|
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(
|
||||||
(data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => {
|
(cachedWatchlist?.response.MediaContainer.Metadata ?? []).map(
|
||||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
async (watchlistItem) => {
|
||||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||||
{},
|
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||||
undefined,
|
{},
|
||||||
{},
|
undefined,
|
||||||
'https://metadata.provider.plex.tv'
|
{},
|
||||||
);
|
'https://metadata.provider.plex.tv'
|
||||||
|
);
|
||||||
|
|
||||||
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||||
|
|
||||||
const tmdbString = metadata.Guid.find((guid) =>
|
const tmdbString = metadata.Guid.find((guid) =>
|
||||||
guid.id.startsWith('tmdb')
|
guid.id.startsWith('tmdb')
|
||||||
);
|
);
|
||||||
const tvdbString = metadata.Guid.find((guid) =>
|
const tvdbString = metadata.Guid.find((guid) =>
|
||||||
guid.id.startsWith('tvdb')
|
guid.id.startsWith('tvdb')
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ratingKey: metadata.ratingKey,
|
ratingKey: metadata.ratingKey,
|
||||||
// This should always be set? But I guess it also cannot be?
|
// This should always be set? But I guess it also cannot be?
|
||||||
// We will filter out the 0's afterwards
|
// We will filter out the 0's afterwards
|
||||||
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
||||||
tvdbId: tvdbString
|
tvdbId: tvdbString
|
||||||
? Number(tvdbString.id.split('//')[1])
|
? Number(tvdbString.id.split('//')[1])
|
||||||
: undefined,
|
: undefined,
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
type: metadata.type,
|
type: metadata.type,
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
|
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
|
||||||
@@ -311,7 +342,7 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
return {
|
return {
|
||||||
offset,
|
offset,
|
||||||
size,
|
size,
|
||||||
totalSize: data.MediaContainer.totalSize,
|
totalSize: cachedWatchlist?.response.MediaContainer.totalSize ?? 0,
|
||||||
items: filteredList,
|
items: filteredList,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -327,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;
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tvshow) {
|
if (!tvshow || !tvshow.rottenTomatoes) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,9 +157,13 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
|
|
||||||
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
|
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
|
||||||
try {
|
try {
|
||||||
const data = await this.get<QueueResponse<QueueItemAppendT>>(`/queue`, {
|
const data = await this.get<QueueResponse<QueueItemAppendT>>(
|
||||||
includeEpisode: 'true',
|
`/queue`,
|
||||||
});
|
{
|
||||||
|
includeEpisode: 'true',
|
||||||
|
},
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
return data.records;
|
return data.records;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -193,15 +197,24 @@ class ServarrBase<QueueItemAppendT> extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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.post(`/command`, {
|
await this.post(
|
||||||
name: commandName,
|
`/command`,
|
||||||
...options,
|
{
|
||||||
});
|
name: commandName,
|
||||||
|
...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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,10 +303,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,
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ 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 {
|
||||||
@@ -523,6 +524,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(
|
||||||
@@ -570,6 +572,7 @@ class TheMovieDb extends ExternalAPI {
|
|||||||
'vote_count.lte': voteCountLte || '',
|
'vote_count.lte': voteCountLte || '',
|
||||||
with_watch_providers: watchProviders || '',
|
with_watch_providers: watchProviders || '',
|
||||||
watch_region: watchRegion || '',
|
watch_region: watchRegion || '',
|
||||||
|
with_status: withStatus || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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',
|
||||||
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
||||||
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
||||||
|
|||||||
@@ -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,56 @@ 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_HOST,
|
||||||
|
port: 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_HOST,
|
||||||
|
port: 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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,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;
|
||||||
@@ -143,6 +144,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;
|
||||||
}
|
}
|
||||||
@@ -246,9 +257,7 @@ export class MediaRequest {
|
|||||||
>;
|
>;
|
||||||
const requestedSeasons =
|
const 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[]);
|
||||||
let existingSeasons: number[] = [];
|
let existingSeasons: number[] = [];
|
||||||
|
|
||||||
@@ -376,6 +385,7 @@ export class MediaRequest {
|
|||||||
@ManyToOne(() => Media, (media) => media.requests, {
|
@ManyToOne(() => Media, (media) => media.requests, {
|
||||||
eager: true,
|
eager: true,
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
|
nullable: false,
|
||||||
})
|
})
|
||||||
public media: Media;
|
public media: Media;
|
||||||
|
|
||||||
@@ -848,7 +858,7 @@ export class MediaRequest {
|
|||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
this.status = MediaRequestStatus.FAILED;
|
this.status = MediaRequestStatus.FAILED;
|
||||||
requestRepository.save(this);
|
await requestRepository.save(this);
|
||||||
|
|
||||||
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',
|
||||||
@@ -1123,13 +1133,14 @@ export class MediaRequest {
|
|||||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||||
sonarrSeries.titleSlug;
|
sonarrSeries.titleSlug;
|
||||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
|
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
|
||||||
|
|
||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
})
|
})
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
this.status = MediaRequestStatus.FAILED;
|
this.status = MediaRequestStatus.FAILED;
|
||||||
requestRepository.save(this);
|
await requestRepository.save(this);
|
||||||
|
|
||||||
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',
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ 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',
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
public media: Promise<Media>;
|
public media: Promise<Media>;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export class Watchlist implements WatchlistItem {
|
|||||||
@ManyToOne(() => Media, (media) => media.watchlists, {
|
@ManyToOne(() => Media, (media) => media.watchlists, {
|
||||||
eager: true,
|
eager: true,
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
|
nullable: false,
|
||||||
})
|
})
|
||||||
public media: Media;
|
public media: Media;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import PlexAPI from '@server/api/plexapi';
|
import PlexAPI from '@server/api/plexapi';
|
||||||
import dataSource, { getRepository } from '@server/datasource';
|
import dataSource, { getRepository, isPgsql } from '@server/datasource';
|
||||||
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||||
import { Session } from '@server/entity/Session';
|
import { Session } from '@server/entity/Session';
|
||||||
import { User } from '@server/entity/User';
|
import { User } from '@server/entity/User';
|
||||||
@@ -19,8 +19,11 @@ import { getSettings } from '@server/lib/settings';
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import clearCookies from '@server/middleware/clearcookies';
|
import clearCookies from '@server/middleware/clearcookies';
|
||||||
import routes from '@server/routes';
|
import routes from '@server/routes';
|
||||||
|
import avatarproxy from '@server/routes/avatarproxy';
|
||||||
import imageproxy from '@server/routes/imageproxy';
|
import imageproxy from '@server/routes/imageproxy';
|
||||||
|
import { appDataPermissions } from '@server/utils/appDataVolume';
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
|
import createCustomProxyAgent from '@server/utils/customProxyAgent';
|
||||||
import restartFlag from '@server/utils/restartFlag';
|
import restartFlag from '@server/utils/restartFlag';
|
||||||
import { getClientIp } from '@supercharge/request-ip';
|
import { getClientIp } from '@supercharge/request-ip';
|
||||||
import { TypeormStore } from 'connect-typeorm/out';
|
import { TypeormStore } from 'connect-typeorm/out';
|
||||||
@@ -50,6 +53,12 @@ const dev = process.env.NODE_ENV !== 'production';
|
|||||||
const app = next({ dev });
|
const app = next({ dev });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
|
|
||||||
|
if (!appDataPermissions()) {
|
||||||
|
logger.error(
|
||||||
|
'Something went wrong while checking config folder! Please ensure the config folder is set up properly.\nhttps://docs.jellyseerr.dev/getting-started'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
app
|
app
|
||||||
.prepare()
|
.prepare()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -57,15 +66,24 @@ app
|
|||||||
|
|
||||||
// Run migrations in production
|
// Run migrations in production
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
await dbConnection.query('PRAGMA foreign_keys=OFF');
|
if (isPgsql) {
|
||||||
await dbConnection.runMigrations();
|
await dbConnection.runMigrations();
|
||||||
await dbConnection.query('PRAGMA foreign_keys=ON');
|
} else {
|
||||||
|
await dbConnection.query('PRAGMA foreign_keys=OFF');
|
||||||
|
await dbConnection.runMigrations();
|
||||||
|
await dbConnection.query('PRAGMA foreign_keys=ON');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Settings
|
// Load Settings
|
||||||
const settings = getSettings();
|
const settings = await getSettings().load();
|
||||||
restartFlag.initializeSettings(settings.main);
|
restartFlag.initializeSettings(settings.main);
|
||||||
|
|
||||||
|
// Register HTTP proxy
|
||||||
|
if (settings.main.proxy.enabled) {
|
||||||
|
await createCustomProxyAgent(settings.main.proxy);
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate library types
|
// Migrate library types
|
||||||
if (
|
if (
|
||||||
settings.plex.libraries.length > 1 &&
|
settings.plex.libraries.length > 1 &&
|
||||||
@@ -174,7 +192,7 @@ app
|
|||||||
},
|
},
|
||||||
store: new TypeormStore({
|
store: new TypeormStore({
|
||||||
cleanupLimit: 2,
|
cleanupLimit: 2,
|
||||||
ttl: 1000 * 60 * 60 * 24 * 30,
|
ttl: 60 * 60 * 24 * 30,
|
||||||
}).connect(sessionRespository) as Store,
|
}).connect(sessionRespository) as Store,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -202,6 +220,7 @@ app
|
|||||||
|
|
||||||
// Do not set cookies so CDNs can cache them
|
// Do not set cookies so CDNs can cache them
|
||||||
server.use('/imageproxy', clearCookies, imageproxy);
|
server.use('/imageproxy', clearCookies, imageproxy);
|
||||||
|
server.use('/avatarproxy', clearCookies, avatarproxy);
|
||||||
|
|
||||||
server.get('*', (req, res) => handle(req, res));
|
server.get('*', (req, res) => handle(req, res));
|
||||||
server.use(
|
server.use(
|
||||||
|
|||||||
14
server/interfaces/api/blacklistInterfaces.ts
Normal file
14
server/interfaces/api/blacklistInterfaces.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { User } from '@server/entity/User';
|
||||||
|
import type { PaginatedResponse } from '@server/interfaces/api/common';
|
||||||
|
|
||||||
|
export interface BlacklistItem {
|
||||||
|
tmdbId: number;
|
||||||
|
mediaType: 'movie' | 'tv';
|
||||||
|
title?: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlacklistResultsResponse extends PaginatedResponse {
|
||||||
|
results: BlacklistItem[];
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ export interface CacheItem {
|
|||||||
|
|
||||||
export interface CacheResponse {
|
export interface CacheResponse {
|
||||||
apiCaches: CacheItem[];
|
apiCaches: CacheItem[];
|
||||||
imageCache: Record<'tmdb', { size: number; imageCount: number }>;
|
imageCache: Record<'tmdb' | 'avatar', { size: number; imageCount: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StatusResponse {
|
export interface StatusResponse {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { MediaServerType } from '@server/constants/server';
|
|||||||
import availabilitySync from '@server/lib/availabilitySync';
|
import availabilitySync from '@server/lib/availabilitySync';
|
||||||
import downloadTracker from '@server/lib/downloadtracker';
|
import downloadTracker from '@server/lib/downloadtracker';
|
||||||
import ImageProxy from '@server/lib/imageproxy';
|
import ImageProxy from '@server/lib/imageproxy';
|
||||||
|
import refreshToken from '@server/lib/refreshToken';
|
||||||
import {
|
import {
|
||||||
jellyfinFullScanner,
|
jellyfinFullScanner,
|
||||||
jellyfinRecentScanner,
|
jellyfinRecentScanner,
|
||||||
@@ -13,7 +14,6 @@ import type { JobId } from '@server/lib/settings';
|
|||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import watchlistSync from '@server/lib/watchlistsync';
|
import watchlistSync from '@server/lib/watchlistsync';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import random from 'lodash/random';
|
|
||||||
import schedule from 'node-schedule';
|
import schedule from 'node-schedule';
|
||||||
|
|
||||||
interface ScheduledJob {
|
interface ScheduledJob {
|
||||||
@@ -113,30 +113,20 @@ export const startJobs = (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Watchlist Sync
|
// Watchlist Sync
|
||||||
const watchlistSyncJob: ScheduledJob = {
|
scheduledJobs.push({
|
||||||
id: 'plex-watchlist-sync',
|
id: 'plex-watchlist-sync',
|
||||||
name: 'Plex Watchlist Sync',
|
name: 'Plex Watchlist Sync',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'fixed',
|
interval: 'seconds',
|
||||||
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
||||||
job: schedule.scheduleJob(new Date(Date.now() + 1000 * 60 * 20), () => {
|
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
watchlistSync.syncWatchlist();
|
watchlistSync.syncWatchlist();
|
||||||
}),
|
}),
|
||||||
};
|
|
||||||
|
|
||||||
// To help alleviate load on Plex's servers, we will add some fuzziness to the next schedule
|
|
||||||
// after each run
|
|
||||||
watchlistSyncJob.job.on('run', () => {
|
|
||||||
watchlistSyncJob.job.schedule(
|
|
||||||
new Date(Math.floor(Date.now() + 1000 * 60 * random(14, 24, true)))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
scheduledJobs.push(watchlistSyncJob);
|
|
||||||
|
|
||||||
// Run full radarr scan every 24 hours
|
// Run full radarr scan every 24 hours
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'radarr-scan',
|
id: 'radarr-scan',
|
||||||
@@ -227,6 +217,23 @@ export const startJobs = (): void => {
|
|||||||
});
|
});
|
||||||
// Clean TMDB image cache
|
// Clean TMDB image cache
|
||||||
ImageProxy.clearCache('tmdb');
|
ImageProxy.clearCache('tmdb');
|
||||||
|
|
||||||
|
// Clean users avatar image cache
|
||||||
|
ImageProxy.clearCache('avatar');
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduledJobs.push({
|
||||||
|
id: 'plex-refresh-token',
|
||||||
|
name: 'Plex Refresh Token',
|
||||||
|
type: 'process',
|
||||||
|
interval: 'fixed',
|
||||||
|
cronSchedule: jobs['plex-refresh-token'].schedule,
|
||||||
|
job: schedule.scheduleJob(jobs['plex-refresh-token'].schedule, () => {
|
||||||
|
logger.info('Starting scheduled job: Plex Refresh Token', {
|
||||||
|
label: 'Jobs',
|
||||||
|
});
|
||||||
|
refreshToken.run();
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,12 +63,7 @@ class AvailabilitySync {
|
|||||||
) {
|
) {
|
||||||
admin = await userRepository.findOne({
|
admin = await userRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
select: [
|
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||||
'id',
|
|
||||||
'jellyfinAuthToken',
|
|
||||||
'jellyfinUserId',
|
|
||||||
'jellyfinDeviceId',
|
|
||||||
],
|
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -86,7 +81,7 @@ class AvailabilitySync {
|
|||||||
if (admin) {
|
if (admin) {
|
||||||
this.jellyfinClient = new JellyfinAPI(
|
this.jellyfinClient = new JellyfinAPI(
|
||||||
getHostname(),
|
getHostname(),
|
||||||
admin.jellyfinAuthToken,
|
settings.jellyfin.apiKey,
|
||||||
admin.jellyfinDeviceId
|
admin.jellyfinDeviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export type AvailableCacheIds =
|
|||||||
| 'imdb'
|
| 'imdb'
|
||||||
| 'github'
|
| 'github'
|
||||||
| 'plexguid'
|
| 'plexguid'
|
||||||
| 'plextv';
|
| 'plextv'
|
||||||
|
| 'plexwatchlist';
|
||||||
|
|
||||||
const DEFAULT_TTL = 300;
|
const DEFAULT_TTL = 300;
|
||||||
const DEFAULT_CHECK_PERIOD = 120;
|
const DEFAULT_CHECK_PERIOD = 120;
|
||||||
@@ -68,6 +69,7 @@ class CacheManager {
|
|||||||
stdTtl: 86400 * 7, // 1 week cache
|
stdTtl: 86400 * 7, // 1 week cache
|
||||||
checkPeriod: 60,
|
checkPeriod: 60,
|
||||||
}),
|
}),
|
||||||
|
plexwatchlist: new Cache('plexwatchlist', 'Plex Watchlist'),
|
||||||
};
|
};
|
||||||
|
|
||||||
public getCache(id: AvailableCacheIds): Cache {
|
public getCache(id: AvailableCacheIds): Cache {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface DownloadingItem {
|
|||||||
timeLeft: string;
|
timeLeft: string;
|
||||||
estimatedCompletionTime: Date;
|
estimatedCompletionTime: Date;
|
||||||
title: string;
|
title: string;
|
||||||
|
downloadId: string;
|
||||||
episode?: EpisodeNumberResult;
|
episode?: EpisodeNumberResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ class DownloadTracker {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await radarr.refreshMonitoredDownloads();
|
||||||
const queueItems = await radarr.getQueue();
|
const queueItems = await radarr.getQueue();
|
||||||
|
|
||||||
this.radarrServers[server.id] = queueItems.map((item) => ({
|
this.radarrServers[server.id] = queueItems.map((item) => ({
|
||||||
@@ -95,6 +97,7 @@ class DownloadTracker {
|
|||||||
status: item.status,
|
status: item.status,
|
||||||
timeLeft: item.timeleft,
|
timeLeft: item.timeleft,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
|
downloadId: item.downloadId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (queueItems.length > 0) {
|
if (queueItems.length > 0) {
|
||||||
@@ -160,6 +163,7 @@ class DownloadTracker {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await sonarr.refreshMonitoredDownloads();
|
||||||
const queueItems = await sonarr.getQueue();
|
const queueItems = await sonarr.getQueue();
|
||||||
|
|
||||||
this.sonarrServers[server.id] = queueItems.map((item) => ({
|
this.sonarrServers[server.id] = queueItems.map((item) => ({
|
||||||
@@ -172,6 +176,7 @@ class DownloadTracker {
|
|||||||
timeLeft: item.timeleft,
|
timeLeft: item.timeleft,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
episode: item.episode,
|
episode: item.episode,
|
||||||
|
downloadId: item.downloadId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (queueItems.length > 0) {
|
if (queueItems.length > 0) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { RateLimitOptions } from '@server/utils/rateLimit';
|
|||||||
import rateLimit from '@server/utils/rateLimit';
|
import rateLimit from '@server/utils/rateLimit';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { promises } from 'fs';
|
import { promises } from 'fs';
|
||||||
|
import mime from 'mime/lite';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
|
|
||||||
type ImageResponse = {
|
type ImageResponse = {
|
||||||
@@ -11,7 +12,7 @@ type ImageResponse = {
|
|||||||
curRevalidate: number;
|
curRevalidate: number;
|
||||||
isStale: boolean;
|
isStale: boolean;
|
||||||
etag: string;
|
etag: string;
|
||||||
extension: string;
|
extension: string | null;
|
||||||
cacheKey: string;
|
cacheKey: string;
|
||||||
cacheMiss: boolean;
|
cacheMiss: boolean;
|
||||||
};
|
};
|
||||||
@@ -27,29 +28,45 @@ class ImageProxy {
|
|||||||
let deletedImages = 0;
|
let deletedImages = 0;
|
||||||
const cacheDirectory = path.join(baseCacheDirectory, key);
|
const cacheDirectory = path.join(baseCacheDirectory, key);
|
||||||
|
|
||||||
const files = await promises.readdir(cacheDirectory);
|
try {
|
||||||
|
const files = await promises.readdir(cacheDirectory);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(cacheDirectory, file);
|
const filePath = path.join(cacheDirectory, file);
|
||||||
const stat = await promises.lstat(filePath);
|
const stat = await promises.lstat(filePath);
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
const imageFiles = await promises.readdir(filePath);
|
const imageFiles = await promises.readdir(filePath);
|
||||||
|
|
||||||
for (const imageFile of imageFiles) {
|
for (const imageFile of imageFiles) {
|
||||||
const [, expireAtSt] = imageFile.split('.');
|
const [, expireAtSt] = imageFile.split('.');
|
||||||
const expireAt = Number(expireAtSt);
|
const expireAt = Number(expireAtSt);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (now > expireAt) {
|
if (now > expireAt) {
|
||||||
await promises.rm(path.join(filePath, imageFile));
|
await promises.rm(path.join(filePath), {
|
||||||
deletedImages += 1;
|
recursive: true,
|
||||||
|
});
|
||||||
|
deletedImages += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ENOENT') {
|
||||||
|
logger.error('Directory not found', {
|
||||||
|
label: 'Image Cache',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.error('Failed to read directory', {
|
||||||
|
label: 'Image Cache',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Cleared ${deletedImages} stale image(s) from cache`, {
|
logger.info(`Cleared ${deletedImages} stale image(s) from cache '${key}'`, {
|
||||||
label: 'Image Cache',
|
label: 'Image Cache',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,39 +86,56 @@ class ImageProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static async getDirectorySize(dir: string): Promise<number> {
|
private static async getDirectorySize(dir: string): Promise<number> {
|
||||||
const files = await promises.readdir(dir, {
|
try {
|
||||||
withFileTypes: true,
|
const files = await promises.readdir(dir, {
|
||||||
});
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
|
||||||
const paths = files.map(async (file) => {
|
const paths = files.map(async (file) => {
|
||||||
const path = join(dir, file.name);
|
const path = join(dir, file.name);
|
||||||
|
|
||||||
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
|
if (file.isDirectory()) return await ImageProxy.getDirectorySize(path);
|
||||||
|
|
||||||
if (file.isFile()) {
|
if (file.isFile()) {
|
||||||
const { size } = await promises.stat(path);
|
const { size } = await promises.stat(path);
|
||||||
|
|
||||||
return size;
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await Promise.all(paths))
|
||||||
|
.flat(Infinity)
|
||||||
|
.reduce((i, size) => i + size, 0);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ENOENT') {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
|
||||||
|
|
||||||
return (await Promise.all(paths))
|
|
||||||
.flat(Infinity)
|
|
||||||
.reduce((i, size) => i + size, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async getImageCount(dir: string) {
|
private static async getImageCount(dir: string) {
|
||||||
const files = await promises.readdir(dir);
|
try {
|
||||||
|
const files = await promises.readdir(dir);
|
||||||
|
|
||||||
return files.length;
|
return files.length;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ENOENT') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetch: typeof fetch;
|
private fetch: typeof fetch;
|
||||||
private cacheVersion;
|
private cacheVersion;
|
||||||
private key;
|
private key;
|
||||||
private baseUrl;
|
private baseUrl;
|
||||||
|
private headers: HeadersInit | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
key: string,
|
key: string,
|
||||||
@@ -109,6 +143,7 @@ class ImageProxy {
|
|||||||
options: {
|
options: {
|
||||||
cacheVersion?: number;
|
cacheVersion?: number;
|
||||||
rateLimitOptions?: RateLimitOptions;
|
rateLimitOptions?: RateLimitOptions;
|
||||||
|
headers?: HeadersInit;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
this.cacheVersion = options.cacheVersion ?? 1;
|
this.cacheVersion = options.cacheVersion ?? 1;
|
||||||
@@ -122,9 +157,13 @@ class ImageProxy {
|
|||||||
} else {
|
} else {
|
||||||
this.fetch = fetch;
|
this.fetch = fetch;
|
||||||
}
|
}
|
||||||
|
this.headers = options.headers || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getImage(path: string): Promise<ImageResponse> {
|
public async getImage(
|
||||||
|
path: string,
|
||||||
|
fallbackPath?: string
|
||||||
|
): Promise<ImageResponse> {
|
||||||
const cacheKey = this.getCacheKey(path);
|
const cacheKey = this.getCacheKey(path);
|
||||||
|
|
||||||
const imageResponse = await this.get(cacheKey);
|
const imageResponse = await this.get(cacheKey);
|
||||||
@@ -133,7 +172,11 @@ class ImageProxy {
|
|||||||
const newImage = await this.set(path, cacheKey);
|
const newImage = await this.set(path, cacheKey);
|
||||||
|
|
||||||
if (!newImage) {
|
if (!newImage) {
|
||||||
throw new Error('Failed to load image');
|
if (fallbackPath) {
|
||||||
|
return await this.getImage(fallbackPath);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to load image');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newImage;
|
return newImage;
|
||||||
@@ -147,6 +190,27 @@ class ImageProxy {
|
|||||||
return imageResponse;
|
return imageResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async clearCachedImage(path: string) {
|
||||||
|
// find cacheKey
|
||||||
|
const cacheKey = this.getCacheKey(path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||||
|
const files = await promises.readdir(directory);
|
||||||
|
|
||||||
|
await promises.rm(directory, { recursive: true });
|
||||||
|
|
||||||
|
logger.info(`Cleared ${files[0]} from cache 'avatar'`, {
|
||||||
|
label: 'Image Cache',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to clear cached image', {
|
||||||
|
label: 'Image Cache',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async get(cacheKey: string): Promise<ImageResponse | null> {
|
private async get(cacheKey: string): Promise<ImageResponse | null> {
|
||||||
try {
|
try {
|
||||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||||
@@ -187,16 +251,30 @@ class ImageProxy {
|
|||||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||||
const href =
|
const href =
|
||||||
this.baseUrl +
|
this.baseUrl +
|
||||||
(this.baseUrl.endsWith('/') ? '' : '/') +
|
(this.baseUrl.length > 0
|
||||||
|
? this.baseUrl.endsWith('/')
|
||||||
|
? ''
|
||||||
|
: '/'
|
||||||
|
: '') +
|
||||||
(path.startsWith('/') ? path.slice(1) : path);
|
(path.startsWith('/') ? path.slice(1) : path);
|
||||||
const response = await this.fetch(href);
|
const response = await this.fetch(href, {
|
||||||
|
headers: this.headers || undefined,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
const extension = path.split('.').pop() ?? '';
|
const extension = mime.getExtension(
|
||||||
const maxAge = Number(
|
response.headers.get('content-type') ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
let maxAge = Number(
|
||||||
(response.headers.get('cache-control') ?? '0').split('=')[1]
|
(response.headers.get('cache-control') ?? '0').split('=')[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!maxAge) maxAge = 86400;
|
||||||
const expireAt = Date.now() + maxAge * 1000;
|
const expireAt = Date.now() + maxAge * 1000;
|
||||||
const etag = (response.headers.get('etag') ?? '').replace(/"/g, '');
|
const etag = (response.headers.get('etag') ?? '').replace(/"/g, '');
|
||||||
|
|
||||||
@@ -232,7 +310,7 @@ class ImageProxy {
|
|||||||
|
|
||||||
private async writeToCacheDir(
|
private async writeToCacheDir(
|
||||||
dir: string,
|
dir: string,
|
||||||
extension: string,
|
extension: string | null,
|
||||||
maxAge: number,
|
maxAge: number,
|
||||||
expireAt: number,
|
expireAt: number,
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
|
|||||||
@@ -291,6 +291,10 @@ class DiscordAgent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.options.webhookRoleId) {
|
||||||
|
userMentions.push(`<@&${settings.options.webhookRoleId}>`);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(settings.options.webhookUrl, {
|
const response = await fetch(settings.options.webhookUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export enum Permission {
|
|||||||
AUTO_REQUEST_TV = 33554432,
|
AUTO_REQUEST_TV = 33554432,
|
||||||
RECENT_VIEW = 67108864,
|
RECENT_VIEW = 67108864,
|
||||||
WATCHLIST_VIEW = 134217728,
|
WATCHLIST_VIEW = 134217728,
|
||||||
|
MANAGE_BLACKLIST = 268435456,
|
||||||
|
VIEW_BLACKLIST = 1073741824,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PermissionCheckOptions {
|
export interface PermissionCheckOptions {
|
||||||
|
|||||||
37
server/lib/refreshToken.ts
Normal file
37
server/lib/refreshToken.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import PlexTvAPI from '@server/api/plextv';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import { User } from '@server/entity/User';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
|
||||||
|
class RefreshToken {
|
||||||
|
public async run() {
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
|
||||||
|
const users = await userRepository
|
||||||
|
.createQueryBuilder('user')
|
||||||
|
.addSelect('user.plexToken')
|
||||||
|
.where("user.plexToken != ''")
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
await this.refreshUserToken(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshUserToken(user: User) {
|
||||||
|
if (!user.plexToken) {
|
||||||
|
logger.warn('Skipping user refresh token for user without plex token', {
|
||||||
|
label: 'Plex Refresh Token',
|
||||||
|
user: user.displayName,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plexTvApi = new PlexTvAPI(user.plexToken);
|
||||||
|
plexTvApi.pingToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshToken = new RefreshToken();
|
||||||
|
|
||||||
|
export default refreshToken;
|
||||||
@@ -567,7 +567,10 @@ class JellyfinScanner {
|
|||||||
public async run(): Promise<void> {
|
public async run(): Promise<void> {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
if (settings.main.mediaServerType != MediaServerType.JELLYFIN) {
|
if (
|
||||||
|
settings.main.mediaServerType != MediaServerType.JELLYFIN &&
|
||||||
|
settings.main.mediaServerType != MediaServerType.EMBY
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,12 +585,7 @@ class JellyfinScanner {
|
|||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
const admin = await userRepository.findOne({
|
const admin = await userRepository.findOne({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
select: [
|
select: ['id', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||||
'id',
|
|
||||||
'jellyfinAuthToken',
|
|
||||||
'jellyfinUserId',
|
|
||||||
'jellyfinDeviceId',
|
|
||||||
],
|
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -597,7 +595,7 @@ class JellyfinScanner {
|
|||||||
|
|
||||||
this.jfClient = new JellyfinAPI(
|
this.jfClient = new JellyfinAPI(
|
||||||
getHostname(),
|
getHostname(),
|
||||||
admin.jellyfinAuthToken,
|
settings.jellyfin.apiKey,
|
||||||
admin.jellyfinDeviceId
|
admin.jellyfinDeviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class PlexScanner
|
|||||||
});
|
});
|
||||||
|
|
||||||
settings.plex.libraries = newLibraries;
|
settings.plex.libraries = newLibraries;
|
||||||
settings.save();
|
await settings.save();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const library of this.libraries) {
|
for (const library of this.libraries) {
|
||||||
@@ -278,9 +278,7 @@ class PlexScanner
|
|||||||
const seasons = tvShow.seasons;
|
const seasons = tvShow.seasons;
|
||||||
const processableSeasons: ProcessableSeason[] = [];
|
const processableSeasons: ProcessableSeason[] = [];
|
||||||
|
|
||||||
const filteredSeasons = seasons.filter((sn) => sn.season_number !== 0);
|
for (const season of seasons) {
|
||||||
|
|
||||||
for (const season of filteredSeasons) {
|
|
||||||
const matchedPlexSeason = metadata.Children?.Metadata.find(
|
const matchedPlexSeason = metadata.Children?.Metadata.find(
|
||||||
(md) => Number(md.index) === season.season_number
|
(md) => Number(md.index) === season.season_number
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -103,10 +103,8 @@ class SonarrScanner
|
|||||||
|
|
||||||
const tmdbId = tvShow.id;
|
const tmdbId = tvShow.id;
|
||||||
|
|
||||||
const filteredSeasons = sonarrSeries.seasons.filter(
|
const filteredSeasons = sonarrSeries.seasons.filter((sn) =>
|
||||||
(sn) =>
|
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
|
||||||
sn.seasonNumber !== 0 &&
|
|
||||||
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const season of filteredSeasons) {
|
for (const season of filteredSeasons) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MediaServerType } from '@server/constants/server';
|
|||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { runMigrations } from '@server/lib/settings/migrator';
|
import { runMigrations } from '@server/lib/settings/migrator';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs/promises';
|
||||||
import { merge } from 'lodash';
|
import { merge } from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import webpush from 'web-push';
|
import webpush from 'web-push';
|
||||||
@@ -47,6 +47,7 @@ export interface JellyfinSettings {
|
|||||||
jellyfinForgotPasswordUrl?: string;
|
jellyfinForgotPasswordUrl?: string;
|
||||||
libraries: Library[];
|
libraries: Library[];
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
apiKey: string;
|
||||||
}
|
}
|
||||||
export interface TautulliSettings {
|
export interface TautulliSettings {
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
@@ -98,6 +99,17 @@ interface Quota {
|
|||||||
quotaDays?: number;
|
quotaDays?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProxySettings {
|
||||||
|
enabled: boolean;
|
||||||
|
hostname: string;
|
||||||
|
port: number;
|
||||||
|
useSsl: boolean;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
bypassFilter: string;
|
||||||
|
bypassLocalAddresses: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MainSettings {
|
export interface MainSettings {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
applicationTitle: string;
|
applicationTitle: string;
|
||||||
@@ -118,6 +130,7 @@ export interface MainSettings {
|
|||||||
mediaServerType: number;
|
mediaServerType: number;
|
||||||
partialRequestsEnabled: boolean;
|
partialRequestsEnabled: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
proxy: ProxySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PublicSettings {
|
interface PublicSettings {
|
||||||
@@ -157,6 +170,7 @@ export interface NotificationAgentDiscord extends NotificationAgentConfig {
|
|||||||
botUsername?: string;
|
botUsername?: string;
|
||||||
botAvatarUrl?: string;
|
botAvatarUrl?: string;
|
||||||
webhookUrl: string;
|
webhookUrl: string;
|
||||||
|
webhookRoleId?: string;
|
||||||
enableMentions: boolean;
|
enableMentions: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -268,6 +282,7 @@ export type JobId =
|
|||||||
| 'plex-recently-added-scan'
|
| 'plex-recently-added-scan'
|
||||||
| 'plex-full-scan'
|
| 'plex-full-scan'
|
||||||
| 'plex-watchlist-sync'
|
| 'plex-watchlist-sync'
|
||||||
|
| 'plex-refresh-token'
|
||||||
| 'radarr-scan'
|
| 'radarr-scan'
|
||||||
| 'sonarr-scan'
|
| 'sonarr-scan'
|
||||||
| 'download-sync'
|
| 'download-sync'
|
||||||
@@ -324,6 +339,16 @@ class Settings {
|
|||||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||||
partialRequestsEnabled: true,
|
partialRequestsEnabled: true,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
proxy: {
|
||||||
|
enabled: false,
|
||||||
|
hostname: '',
|
||||||
|
port: 8080,
|
||||||
|
useSsl: false,
|
||||||
|
user: '',
|
||||||
|
password: '',
|
||||||
|
bypassFilter: '',
|
||||||
|
bypassLocalAddresses: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plex: {
|
plex: {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -342,6 +367,7 @@ class Settings {
|
|||||||
jellyfinForgotPasswordUrl: '',
|
jellyfinForgotPasswordUrl: '',
|
||||||
libraries: [],
|
libraries: [],
|
||||||
serverId: '',
|
serverId: '',
|
||||||
|
apiKey: '',
|
||||||
},
|
},
|
||||||
tautulli: {},
|
tautulli: {},
|
||||||
radarr: [],
|
radarr: [],
|
||||||
@@ -370,6 +396,7 @@ class Settings {
|
|||||||
types: 0,
|
types: 0,
|
||||||
options: {
|
options: {
|
||||||
webhookUrl: '',
|
webhookUrl: '',
|
||||||
|
webhookRoleId: '',
|
||||||
enableMentions: true,
|
enableMentions: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -443,7 +470,10 @@ class Settings {
|
|||||||
schedule: '0 0 3 * * *',
|
schedule: '0 0 3 * * *',
|
||||||
},
|
},
|
||||||
'plex-watchlist-sync': {
|
'plex-watchlist-sync': {
|
||||||
schedule: '0 */10 * * * *',
|
schedule: '0 */3 * * * *',
|
||||||
|
},
|
||||||
|
'plex-refresh-token': {
|
||||||
|
schedule: '0 0 5 * * *',
|
||||||
},
|
},
|
||||||
'radarr-scan': {
|
'radarr-scan': {
|
||||||
schedule: '0 0 4 * * *',
|
schedule: '0 0 4 * * *',
|
||||||
@@ -477,10 +507,6 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get main(): MainSettings {
|
get main(): MainSettings {
|
||||||
if (!this.data.main.apiKey) {
|
|
||||||
this.data.main.apiKey = this.generateApiKey();
|
|
||||||
this.save();
|
|
||||||
}
|
|
||||||
return this.data.main;
|
return this.data.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,42 +608,28 @@ class Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get clientId(): string {
|
get clientId(): string {
|
||||||
if (!this.data.clientId) {
|
|
||||||
this.data.clientId = randomUUID();
|
|
||||||
this.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.data.clientId;
|
return this.data.clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
get vapidPublic(): string {
|
get vapidPublic(): string {
|
||||||
this.generateVapidKeys();
|
|
||||||
|
|
||||||
return this.data.vapidPublic;
|
return this.data.vapidPublic;
|
||||||
}
|
}
|
||||||
|
|
||||||
get vapidPrivate(): string {
|
get vapidPrivate(): string {
|
||||||
this.generateVapidKeys();
|
|
||||||
|
|
||||||
return this.data.vapidPrivate;
|
return this.data.vapidPrivate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public regenerateApiKey(): MainSettings {
|
public async regenerateApiKey(): Promise<MainSettings> {
|
||||||
this.main.apiKey = this.generateApiKey();
|
this.main.apiKey = this.generateApiKey();
|
||||||
this.save();
|
await this.save();
|
||||||
return this.main;
|
return this.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateApiKey(): string {
|
private generateApiKey(): string {
|
||||||
return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64');
|
if (process.env.API_KEY) {
|
||||||
}
|
return process.env.API_KEY;
|
||||||
|
} else {
|
||||||
private generateVapidKeys(force = false): void {
|
return Buffer.from(`${Date.now()}${randomUUID()}`).toString('base64');
|
||||||
if (!this.data.vapidPublic || !this.data.vapidPrivate || force) {
|
|
||||||
const vapidKeys = webpush.generateVAPIDKeys();
|
|
||||||
this.data.vapidPrivate = vapidKeys.privateKey;
|
|
||||||
this.data.vapidPublic = vapidKeys.publicKey;
|
|
||||||
this.save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,34 +641,60 @@ class Settings {
|
|||||||
* @param overrideSettings If passed in, will override all existing settings with these
|
* @param overrideSettings If passed in, will override all existing settings with these
|
||||||
* values
|
* values
|
||||||
*/
|
*/
|
||||||
public load(overrideSettings?: AllSettings): Settings {
|
public async load(overrideSettings?: AllSettings): Promise<Settings> {
|
||||||
if (overrideSettings) {
|
if (overrideSettings) {
|
||||||
this.data = overrideSettings;
|
this.data = overrideSettings;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(SETTINGS_PATH)) {
|
let data;
|
||||||
this.save();
|
try {
|
||||||
|
data = await fs.readFile(SETTINGS_PATH, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
await this.save();
|
||||||
}
|
}
|
||||||
const data = fs.readFileSync(SETTINGS_PATH, 'utf-8');
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const parsedJson = JSON.parse(data);
|
const parsedJson = JSON.parse(data);
|
||||||
this.data = runMigrations(parsedJson);
|
const migratedData = await runMigrations(parsedJson, SETTINGS_PATH);
|
||||||
|
this.data = merge(this.data, migratedData);
|
||||||
this.data = merge(this.data, parsedJson);
|
|
||||||
|
|
||||||
this.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generate keys and ids if it's missing
|
||||||
|
let change = false;
|
||||||
|
if (!this.data.main.apiKey) {
|
||||||
|
this.data.main.apiKey = this.generateApiKey();
|
||||||
|
change = true;
|
||||||
|
} else if (process.env.API_KEY) {
|
||||||
|
if (this.main.apiKey != process.env.API_KEY) {
|
||||||
|
this.main.apiKey = process.env.API_KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.data.clientId) {
|
||||||
|
this.data.clientId = randomUUID();
|
||||||
|
change = true;
|
||||||
|
}
|
||||||
|
if (!this.data.vapidPublic || !this.data.vapidPrivate) {
|
||||||
|
const vapidKeys = webpush.generateVAPIDKeys();
|
||||||
|
this.data.vapidPrivate = vapidKeys.privateKey;
|
||||||
|
this.data.vapidPublic = vapidKeys.publicKey;
|
||||||
|
change = true;
|
||||||
|
}
|
||||||
|
if (change) {
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public save(): void {
|
public async save(): Promise<void> {
|
||||||
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(this.data, undefined, ' '));
|
await fs.writeFile(
|
||||||
|
SETTINGS_PATH,
|
||||||
|
JSON.stringify(this.data, undefined, ' ')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let loaded = false;
|
|
||||||
let settings: Settings | undefined;
|
let settings: Settings | undefined;
|
||||||
|
|
||||||
export const getSettings = (initialSettings?: AllSettings): Settings => {
|
export const getSettings = (initialSettings?: AllSettings): Settings => {
|
||||||
@@ -664,11 +702,6 @@ export const getSettings = (initialSettings?: AllSettings): Settings => {
|
|||||||
settings = new Settings(initialSettings);
|
settings = new Settings(initialSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
|
||||||
settings.load();
|
|
||||||
loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import type { AllSettings } from '@server/lib/settings';
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
|
|
||||||
const migrateHostname = (settings: any): AllSettings => {
|
const migrateHostname = (settings: any): AllSettings => {
|
||||||
const oldJellyfinSettings = settings.jellyfin;
|
if (settings.jellyfin?.hostname) {
|
||||||
if (oldJellyfinSettings && oldJellyfinSettings.hostname) {
|
const { hostname } = settings.jellyfin;
|
||||||
const { hostname } = oldJellyfinSettings;
|
|
||||||
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
const protocolMatch = hostname.match(/^(https?):\/\//i);
|
||||||
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
const useSsl = protocolMatch && protocolMatch[1].toLowerCase() === 'https';
|
||||||
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
const remainingUrl = hostname.replace(/^(https?):\/\//i, '');
|
||||||
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
const urlMatch = remainingUrl.match(/^([^:]+)(:([0-9]+))?(\/.*)?$/);
|
||||||
|
|
||||||
delete oldJellyfinSettings.hostname;
|
delete settings.jellyfin.hostname;
|
||||||
if (urlMatch) {
|
if (urlMatch) {
|
||||||
const [, ip, , port, urlBase] = urlMatch;
|
const [, ip, , port, urlBase] = urlMatch;
|
||||||
settings.jellyfin = {
|
settings.jellyfin = {
|
||||||
@@ -21,9 +20,7 @@ const migrateHostname = (settings: any): AllSettings => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (settings.jellyfin && settings.jellyfin.hostname) {
|
|
||||||
delete settings.jellyfin.hostname;
|
|
||||||
}
|
|
||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
42
server/lib/settings/migrations/0002_migrate_apitokens.ts
Normal file
42
server/lib/settings/migrations/0002_migrate_apitokens.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import JellyfinAPI from '@server/api/jellyfin';
|
||||||
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import { User } from '@server/entity/User';
|
||||||
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
|
|
||||||
|
const migrateApiTokens = async (settings: any): Promise<AllSettings> => {
|
||||||
|
const mediaServerType = settings.main.mediaServerType;
|
||||||
|
if (
|
||||||
|
!settings.jellyfin.apiKey &&
|
||||||
|
(mediaServerType === MediaServerType.JELLYFIN ||
|
||||||
|
mediaServerType === MediaServerType.EMBY)
|
||||||
|
) {
|
||||||
|
const userRepository = getRepository(User);
|
||||||
|
const admin = await userRepository.findOne({
|
||||||
|
where: { id: 1 },
|
||||||
|
select: ['id', 'jellyfinAuthToken', 'jellyfinUserId', 'jellyfinDeviceId'],
|
||||||
|
order: { id: 'ASC' },
|
||||||
|
});
|
||||||
|
if (!admin) {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
const jellyfinClient = new JellyfinAPI(
|
||||||
|
getHostname(settings.jellyfin),
|
||||||
|
admin.jellyfinAuthToken,
|
||||||
|
admin.jellyfinDeviceId
|
||||||
|
);
|
||||||
|
jellyfinClient.setUserId(admin.jellyfinUserId ?? '');
|
||||||
|
try {
|
||||||
|
const apiKey = await jellyfinClient.createApiToken('Jellyseerr');
|
||||||
|
settings.jellyfin.apiKey = apiKey;
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to create Jellyfin API token from admin account. Please check your network configuration or edit your settings.json by adding an 'apiKey' field inside of the 'jellyfin' section to fix this issue."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default migrateApiTokens;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
|
|
||||||
|
const migrateHostname = (settings: any): AllSettings => {
|
||||||
|
const oldMediaServerType = settings.main.mediaServerType;
|
||||||
|
if (
|
||||||
|
oldMediaServerType === MediaServerType.JELLYFIN &&
|
||||||
|
process.env.JELLYFIN_TYPE === 'emby'
|
||||||
|
) {
|
||||||
|
settings.main.mediaServerType = MediaServerType.EMBY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default migrateHostname;
|
||||||
@@ -1,20 +1,100 @@
|
|||||||
import type { AllSettings } from '@server/lib/settings';
|
import type { AllSettings } from '@server/lib/settings';
|
||||||
import fs from 'fs';
|
import logger from '@server/logger';
|
||||||
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const migrationsDir = path.join(__dirname, 'migrations');
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
|
|
||||||
export const runMigrations = (settings: AllSettings): AllSettings => {
|
export const runMigrations = async (
|
||||||
const migrations = fs
|
settings: AllSettings,
|
||||||
.readdirSync(migrationsDir)
|
SETTINGS_PATH: string
|
||||||
.filter((file) => file.endsWith('.js') || file.endsWith('.ts'))
|
): Promise<AllSettings> => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
.map((file) => require(path.join(migrationsDir, file)).default);
|
|
||||||
|
|
||||||
let migrated = settings;
|
let migrated = settings;
|
||||||
|
|
||||||
for (const migration of migrations) {
|
try {
|
||||||
migrated = migration(migrated);
|
// we read old backup and create a backup of currents settings
|
||||||
|
const BACKUP_PATH = SETTINGS_PATH.replace('.json', '.old.json');
|
||||||
|
let oldBackup: string | null = null;
|
||||||
|
try {
|
||||||
|
oldBackup = await fs.readFile(BACKUP_PATH, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
await fs.writeFile(BACKUP_PATH, JSON.stringify(settings, undefined, ' '));
|
||||||
|
|
||||||
|
const migrations = (await fs.readdir(migrationsDir)).filter(
|
||||||
|
(file) => file.endsWith('.js') || file.endsWith('.ts')
|
||||||
|
);
|
||||||
|
|
||||||
|
const settingsBefore = JSON.stringify(migrated);
|
||||||
|
|
||||||
|
for (const migration of migrations) {
|
||||||
|
try {
|
||||||
|
logger.debug(`Checking migration '${migration}'...`, {
|
||||||
|
label: 'Settings Migrator',
|
||||||
|
});
|
||||||
|
const { default: migrationFn } = await import(
|
||||||
|
path.join(migrationsDir, migration)
|
||||||
|
);
|
||||||
|
const newSettings = await migrationFn(structuredClone(migrated));
|
||||||
|
if (JSON.stringify(migrated) !== JSON.stringify(newSettings)) {
|
||||||
|
logger.debug(`Migration '${migration}' has been applied.`, {
|
||||||
|
label: 'Settings Migrator',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
migrated = newSettings;
|
||||||
|
} catch (e) {
|
||||||
|
// we stop jellyseerr if the migration failed
|
||||||
|
logger.error(
|
||||||
|
`Error while running migration '${migration}': ${e.message}`,
|
||||||
|
{
|
||||||
|
label: 'Settings Migrator',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
logger.error(
|
||||||
|
'A common cause for this error is a permission issue with your configuration folder, a network issue or a corrupted database.',
|
||||||
|
{
|
||||||
|
label: 'Settings Migrator',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsAfter = JSON.stringify(migrated);
|
||||||
|
|
||||||
|
if (settingsBefore !== settingsAfter) {
|
||||||
|
// a migration occured
|
||||||
|
// we check that the new config will be saved
|
||||||
|
await fs.writeFile(
|
||||||
|
SETTINGS_PATH,
|
||||||
|
JSON.stringify(migrated, undefined, ' ')
|
||||||
|
);
|
||||||
|
const fileSaved = JSON.parse(await fs.readFile(SETTINGS_PATH, 'utf-8'));
|
||||||
|
if (JSON.stringify(fileSaved) !== settingsAfter) {
|
||||||
|
// something went wrong while saving file
|
||||||
|
throw new Error('Unable to save settings after migration.');
|
||||||
|
}
|
||||||
|
} else if (oldBackup) {
|
||||||
|
// no migration occured
|
||||||
|
// we save the old backup (to avoid settings.json and settings.old.json being the same)
|
||||||
|
await fs.writeFile(BACKUP_PATH, oldBackup.toString());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// we stop jellyseerr if the migration failed
|
||||||
|
logger.error(
|
||||||
|
`Something went wrong while running settings migrations: ${e.message}`,
|
||||||
|
{
|
||||||
|
label: 'Settings Migrator',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
logger.error(
|
||||||
|
'A common cause for this issue is a permission error of your configuration folder.',
|
||||||
|
{
|
||||||
|
label: 'Settings Migrator',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
process.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
return migrated;
|
return migrated;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class WatchlistSync {
|
|||||||
|
|
||||||
const plexTvApi = new PlexTvAPI(user.plexToken);
|
const plexTvApi = new PlexTvAPI(user.plexToken);
|
||||||
|
|
||||||
const response = await plexTvApi.getWatchlist({ size: 200 });
|
const response = await plexTvApi.getWatchlist({ size: 20 });
|
||||||
|
|
||||||
const mediaItems = await Media.getRelatedMedia(
|
const mediaItems = await Media.getRelatedMedia(
|
||||||
user,
|
user,
|
||||||
|
|||||||
304
server/migration/postgres/1705599190375-InitialMigration.ts
Normal file
304
server/migration/postgres/1705599190375-InitialMigration.ts
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class InitialMigration1705599190375 implements MigrationInterface {
|
||||||
|
name = 'InitialMigration1705599190375';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists session
|
||||||
|
(
|
||||||
|
"expiredAt" bigint,
|
||||||
|
id text,
|
||||||
|
json text
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create index if not exists "idx_194703_IDX_28c5d1d16da7908c97c9bc2f74"
|
||||||
|
on session ("expiredAt");`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index if not exists idx_194703_sqlite_autoindex_session_1
|
||||||
|
on session (id);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists media
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
"mediaType" text,
|
||||||
|
"tmdbId" int,
|
||||||
|
"tvdbId" int,
|
||||||
|
"imdbId" text,
|
||||||
|
status int default '1'::int,
|
||||||
|
status4k int default '1'::int,
|
||||||
|
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"lastSeasonChange" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"mediaAddedAt" timestamp with time zone,
|
||||||
|
"serviceId" int,
|
||||||
|
"serviceId4k" int,
|
||||||
|
"externalServiceId" int,
|
||||||
|
"externalServiceId4k" int,
|
||||||
|
"externalServiceSlug" text,
|
||||||
|
"externalServiceSlug4k" text,
|
||||||
|
"ratingKey" text,
|
||||||
|
"ratingKey4k" text,
|
||||||
|
"jellyfinMediaId" text,
|
||||||
|
"jellyfinMediaId4k" text,
|
||||||
|
constraint idx_194722_media_pkey
|
||||||
|
primary key (id)
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists season
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
"seasonNumber" int,
|
||||||
|
status int default '1'::int,
|
||||||
|
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"mediaId" int not null,
|
||||||
|
status4k int default '1'::int,
|
||||||
|
constraint idx_194715_season_pkey
|
||||||
|
primary key (id),
|
||||||
|
foreign key ("mediaId") references media
|
||||||
|
on delete cascade
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create index if not exists "idx_194722_IDX_7ff2d11f6a83cb52386eaebe74"
|
||||||
|
on media ("imdbId");`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create index if not exists "idx_194722_IDX_41a289eb1fa489c1bc6f38d9c3"
|
||||||
|
on media ("tvdbId");`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create index if not exists "idx_194722_IDX_7157aad07c73f6a6ae3bbd5ef5"
|
||||||
|
on media ("tmdbId");`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index if not exists idx_194722_sqlite_autoindex_media_1
|
||||||
|
on media ("tvdbId");`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists "user"
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
email text,
|
||||||
|
username text,
|
||||||
|
"plexId" int,
|
||||||
|
"plexToken" text,
|
||||||
|
permissions int default '0'::int,
|
||||||
|
avatar text,
|
||||||
|
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
password text,
|
||||||
|
"userType" int default '1'::int,
|
||||||
|
"plexUsername" text,
|
||||||
|
"resetPasswordGuid" text,
|
||||||
|
"recoveryLinkExpirationDate" date,
|
||||||
|
"movieQuotaLimit" int,
|
||||||
|
"movieQuotaDays" int,
|
||||||
|
"tvQuotaLimit" int,
|
||||||
|
"tvQuotaDays" int,
|
||||||
|
"jellyfinUsername" text,
|
||||||
|
"jellyfinAuthToken" text,
|
||||||
|
"jellyfinUserId" text,
|
||||||
|
"jellyfinDeviceId" text,
|
||||||
|
constraint idx_194731_user_pkey
|
||||||
|
primary key (id)
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index if not exists idx_194731_sqlite_autoindex_user_1
|
||||||
|
on "user" (email);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists user_push_subscription
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
endpoint text,
|
||||||
|
p256dh text,
|
||||||
|
auth text,
|
||||||
|
"userId" int,
|
||||||
|
constraint idx_194740_user_push_subscription_pkey
|
||||||
|
primary key (id),
|
||||||
|
foreign key ("userId") references "user"
|
||||||
|
on delete cascade
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index if not exists idx_194740_sqlite_autoindex_user_push_subscription_1
|
||||||
|
on user_push_subscription (auth);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists issue
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
"issueType" int,
|
||||||
|
status int default '1'::int,
|
||||||
|
"problemSeason" int default '0'::int,
|
||||||
|
"problemEpisode" int default '0'::int,
|
||||||
|
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"mediaId" int not null,
|
||||||
|
"createdById" int,
|
||||||
|
"modifiedById" int,
|
||||||
|
constraint idx_194747_issue_pkey
|
||||||
|
primary key (id),
|
||||||
|
foreign key ("modifiedById") references "user"
|
||||||
|
on delete cascade,
|
||||||
|
foreign key ("createdById") references "user"
|
||||||
|
on delete cascade,
|
||||||
|
foreign key ("mediaId") references media
|
||||||
|
on delete cascade
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists issue_comment
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
message text,
|
||||||
|
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"userId" int,
|
||||||
|
"issueId" int,
|
||||||
|
constraint idx_194755_issue_comment_pkey
|
||||||
|
primary key (id),
|
||||||
|
foreign key ("issueId") references issue
|
||||||
|
on delete cascade,
|
||||||
|
foreign key ("userId") references "user"
|
||||||
|
on delete cascade
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists user_settings
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
"notificationTypes" text,
|
||||||
|
"discordId" text,
|
||||||
|
"userId" int,
|
||||||
|
region text,
|
||||||
|
"originalLanguage" text,
|
||||||
|
"telegramChatId" text,
|
||||||
|
"telegramSendSilently" boolean,
|
||||||
|
"pgpKey" text,
|
||||||
|
locale text default ''::text,
|
||||||
|
"pushbulletAccessToken" text,
|
||||||
|
"pushoverApplicationToken" text,
|
||||||
|
"pushoverUserKey" text,
|
||||||
|
"watchlistSyncMovies" boolean,
|
||||||
|
"watchlistSyncTv" boolean,
|
||||||
|
"pushoverSound" varchar,
|
||||||
|
constraint idx_194762_user_settings_pkey
|
||||||
|
primary key (id),
|
||||||
|
foreign key ("userId") references "user"
|
||||||
|
on delete cascade
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index if not exists idx_194762_sqlite_autoindex_user_settings_1
|
||||||
|
on user_settings ("userId");`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists media_request
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
status int,
|
||||||
|
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
type text,
|
||||||
|
"mediaId" int not null,
|
||||||
|
"requestedById" int,
|
||||||
|
"modifiedById" int,
|
||||||
|
is4k boolean default false,
|
||||||
|
"serverId" int,
|
||||||
|
"profileId" int,
|
||||||
|
"rootFolder" text,
|
||||||
|
"languageProfileId" int,
|
||||||
|
tags text,
|
||||||
|
"isAutoRequest" boolean default false,
|
||||||
|
constraint idx_194770_media_request_pkey
|
||||||
|
primary key (id),
|
||||||
|
foreign key ("modifiedById") references "user"
|
||||||
|
on delete set null,
|
||||||
|
foreign key ("requestedById") references "user"
|
||||||
|
on delete cascade,
|
||||||
|
foreign key ("mediaId") references media
|
||||||
|
on delete cascade
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists season_request
|
||||||
|
(
|
||||||
|
id serial NOT NULL,
|
||||||
|
"seasonNumber" int,
|
||||||
|
status int default '1'::int,
|
||||||
|
"createdAt" timestamp with time zone default now(),
|
||||||
|
"updatedAt" timestamp with time zone default now(),
|
||||||
|
"requestId" int,
|
||||||
|
constraint idx_194709_season_request_pkey
|
||||||
|
primary key (id),
|
||||||
|
foreign key ("requestId") references media_request
|
||||||
|
on delete cascade
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists discover_slider
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
type integer,
|
||||||
|
"order" integer,
|
||||||
|
"isBuiltIn" boolean default false,
|
||||||
|
enabled boolean default true,
|
||||||
|
title text,
|
||||||
|
data text,
|
||||||
|
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
constraint idx_194779_discover_slider_pkey
|
||||||
|
primary key (id)
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create table if not exists watchlist
|
||||||
|
(
|
||||||
|
id serial,
|
||||||
|
"ratingKey" text,
|
||||||
|
"mediaType" text,
|
||||||
|
title text,
|
||||||
|
"tmdbId" int,
|
||||||
|
"createdAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp with time zone default CURRENT_TIMESTAMP,
|
||||||
|
"requestedById" int,
|
||||||
|
"mediaId" int not null,
|
||||||
|
constraint idx_194788_watchlist_pkey
|
||||||
|
primary key (id)
|
||||||
|
);`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create index if not exists "idx_194788_IDX_939f205946256cc0d2a1ac51a8"
|
||||||
|
on watchlist ("tmdbId");`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`create unique index if not exists idx_194788_sqlite_autoindex_watchlist_1
|
||||||
|
on watchlist ("tmdbId", "requestedById");`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`drop table if exists session cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists season_request cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists season cascade`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`drop table if exists user_push_subscription cascade`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`drop table if exists issue_comment cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists issue cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists user_settings cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists media_request cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists media cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists "user" cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists discover_slider cascade`);
|
||||||
|
await queryRunner.query(`drop table if exists watchlist cascade`);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
server/migration/postgres/1730770837441-AddBlacklist.ts
Normal file
32
server/migration/postgres/1730770837441-AddBlacklist.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddBlacklist1730770837441 implements MigrationInterface {
|
||||||
|
name = 'AddBlacklist1730770837441';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "blacklist"
|
||||||
|
(
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"mediaType" VARCHAR NOT NULL,
|
||||||
|
"title" VARCHAR,
|
||||||
|
"tmdbId" INTEGER NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
"userId" INTEGER,
|
||||||
|
"mediaId" INTEGER,
|
||||||
|
CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId", "userId")
|
||||||
|
)`
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId")`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX IF EXISTS "IDX_6bbafa28411e6046421991ea21"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE IF EXISTS "blacklist"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user