Compare commits
10 Commits
preview-go
...
preview-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a23f62a02 | ||
|
|
29034b350d | ||
|
|
7438042757 | ||
|
|
0b0b76e58c | ||
|
|
a5cb505609 | ||
|
|
7cb127ec3f | ||
|
|
1635932375 | ||
|
|
c1aeab9538 | ||
|
|
70fb1f2b00 | ||
|
|
4cd02babba |
@@ -249,7 +249,8 @@
|
|||||||
"avatar_url": "https://avatars.githubusercontent.com/u/854646?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/854646?v=4",
|
||||||
"profile": "http://www.piribisoft.com",
|
"profile": "http://www.piribisoft.com",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc",
|
||||||
|
"code"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -711,6 +712,105 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "j0srisk",
|
||||||
|
"name": "Joseph Risk",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
|
||||||
|
"profile": "http://josephrisk.com",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Loetwiek",
|
||||||
|
"name": "Loetwiek",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
|
||||||
|
"profile": "https://github.com/Loetwiek",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Fuochi",
|
||||||
|
"name": "Fuochi",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
|
||||||
|
"profile": "https://github.com/Fuochi",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "demrich",
|
||||||
|
"name": "David Emrich",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
|
||||||
|
"profile": "https://github.com/demrich",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "maxnatamo",
|
||||||
|
"name": "Max T. Kristiansen",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
|
||||||
|
"profile": "https://maxtrier.dk",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "DamsDev1",
|
||||||
|
"name": "Damien Fajole",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
|
||||||
|
"profile": "https://damsdev.me",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "AhmedNSidd",
|
||||||
|
"name": "Ahmed Siddiqui",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
|
||||||
|
"profile": "https://github.com/AhmedNSidd",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "JackW6809",
|
||||||
|
"name": "JackOXI",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/53652452?v=4",
|
||||||
|
"profile": "https://github.com/JackW6809",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "StancuFlorin",
|
||||||
|
"name": "Stancu Florin",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1199404?v=4",
|
||||||
|
"profile": "http://indicus.ro",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "lmiklosko",
|
||||||
|
"name": "Lukas Miklosko",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/44380311?v=4",
|
||||||
|
"profile": "https://github.com/lmiklosko",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "gauthier-th",
|
||||||
|
"name": "Gauthier",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/37781713?v=4",
|
||||||
|
"profile": "https://gauthierth.fr/",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -98,6 +98,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
COMMIT_TAG=${{ github.sha }}
|
COMMIT_TAG=${{ github.sha }}
|
||||||
|
BUILD_VERSION=develop
|
||||||
|
BUILD_DATE=${{ github.event.repository.updated_at }}
|
||||||
outputs: |
|
outputs: |
|
||||||
type=image,push-by-digest=true,name=fallenbagel/jellyseerr,push=true
|
type=image,push-by-digest=true,name=fallenbagel/jellyseerr,push=true
|
||||||
type=image,push-by-digest=true,name=ghcr.io/${{ env.OWNER_LC }}/jellyseerr,push=true
|
type=image,push-by-digest=true,name=ghcr.io/${{ env.OWNER_LC }}/jellyseerr,push=true
|
||||||
|
|||||||
2
.github/workflows/preview.yml
vendored
2
.github/workflows/preview.yml
vendored
@@ -33,5 +33,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
COMMIT_TAG=${{ github.sha }}
|
COMMIT_TAG=${{ github.sha }}
|
||||||
|
BUILD_VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||||
|
BUILD_DATE=${{ github.event.repository.updated_at }}
|
||||||
tags: |
|
tags: |
|
||||||
fallenbagel/jellyseerr:${{ steps.get_version.outputs.VERSION }}
|
fallenbagel/jellyseerr:${{ steps.get_version.outputs.VERSION }}
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@@ -38,8 +38,17 @@ RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
|||||||
|
|
||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
# Metadata for Github Package Registry
|
# OCI Meta information
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr"
|
ARG BUILD_DATE
|
||||||
|
ARG BUILD_VERSION
|
||||||
|
LABEL \
|
||||||
|
org.opencontainers.image.authors="Fallenbagel" \
|
||||||
|
org.opencontainers.image.source="https://github.com/fallenbagel/jellyseerr" \
|
||||||
|
org.opencontainers.image.created=${BUILD_DATE} \
|
||||||
|
org.opencontainers.image.version=${BUILD_VERSION} \
|
||||||
|
org.opencontainers.image.title="Jellyseerr" \
|
||||||
|
org.opencontainers.image.description="Open-source media request and discovery manager for Jellyfin, Plex, and Emby." \
|
||||||
|
org.opencontainers.image.licenses="MIT"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,8 @@ 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/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JackW6809"><img src="https://avatars.githubusercontent.com/u/53652452?v=4?s=100" width="100px;" alt="JackOXI"/><br /><sub><b>JackOXI</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JackW6809" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JackW6809"><img src="https://avatars.githubusercontent.com/u/53652452?v=4?s=100" width="100px;" alt="JackOXI"/><br /><sub><b>JackOXI</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=JackW6809" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="http://indicus.ro"><img src="https://avatars.githubusercontent.com/u/1199404?v=4?s=100" width="100px;" alt="Stancu Florin"/><br /><sub><b>Stancu Florin</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=StancuFlorin" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lmiklosko"><img src="https://avatars.githubusercontent.com/u/44380311?v=4?s=100" width="100px;" alt="Lukas Miklosko"/><br /><sub><b>Lukas Miklosko</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=lmiklosko" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=gauthier-th" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ docker run -d \
|
|||||||
-p 5055:5055 \
|
-p 5055:5055 \
|
||||||
-v /path/to/appdata/config:/app/config \
|
-v /path/to/appdata/config:/app/config \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
fallenbagel/jellyseerr
|
ghcr.io/fallenbagel/jellyseerr
|
||||||
```
|
```
|
||||||
:::tip
|
:::tip
|
||||||
If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable to `emby`.
|
If you are using emby, make sure to set the `JELLYFIN_TYPE` environment variable to `emby`.
|
||||||
@@ -55,7 +55,7 @@ docker stop jellyseerr && docker rm Jellyseerr
|
|||||||
```
|
```
|
||||||
Pull the latest image:
|
Pull the latest image:
|
||||||
```bash
|
```bash
|
||||||
docker pull fallenbagel/jellyseerr
|
docker pull ghcr.io/fallenbagel/jellyseerr
|
||||||
```
|
```
|
||||||
Finally, run the container with the same parameters originally used to create the container:
|
Finally, run the container with the same parameters originally used to create the container:
|
||||||
```bash
|
```bash
|
||||||
@@ -78,7 +78,7 @@ Define the `jellyseerr` service in your `compose.yaml` as follows:
|
|||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
jellyseerr:
|
jellyseerr:
|
||||||
image: fallenbagel/jellyseerr:latest
|
image: ghcr.io/fallenbagel/jellyseerr:latest
|
||||||
container_name: jellyseerr
|
container_name: jellyseerr
|
||||||
environment:
|
environment:
|
||||||
- LOG_LEVEL=debug
|
- LOG_LEVEL=debug
|
||||||
@@ -146,7 +146,7 @@ Then, create and start the Jellyseerr container:
|
|||||||
<Tabs groupId="docker-methods" queryString>
|
<Tabs groupId="docker-methods" queryString>
|
||||||
<TabItem value="docker-cli" label="Docker CLI">
|
<TabItem value="docker-cli" label="Docker CLI">
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name jellyseerr -e LOG_LEVEL=debug -e TZ=Asia/Tashkent -p 5055:5055 -v "jellyseerr-data:/app/config" --restart unless-stopped fallenbagel/jellyseerr:latest
|
docker run -d --name jellyseerr -e LOG_LEVEL=debug -e TZ=Asia/Tashkent -p 5055:5055 -v "jellyseerr-data:/app/config" --restart unless-stopped ghcr.io/fallenbagel/jellyseerr:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Updating:
|
#### Updating:
|
||||||
|
|||||||
@@ -3965,6 +3965,8 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
p256dh:
|
p256dh:
|
||||||
type: string
|
type: string
|
||||||
|
userAgent:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- endpoint
|
- endpoint
|
||||||
- auth
|
- auth
|
||||||
@@ -3972,6 +3974,88 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: Successfully registered push subscription
|
description: Successfully registered push subscription
|
||||||
|
/user/{userId}/pushSubscriptions:
|
||||||
|
get:
|
||||||
|
summary: Get all web push notification settings for a user
|
||||||
|
description: |
|
||||||
|
Returns all web push notification settings for a user in a JSON object.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User web push notification settings in JSON
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
endpoint:
|
||||||
|
type: string
|
||||||
|
p256dh:
|
||||||
|
type: string
|
||||||
|
auth:
|
||||||
|
type: string
|
||||||
|
userAgent:
|
||||||
|
type: string
|
||||||
|
/user/{userId}/pushSubscription/{key}:
|
||||||
|
get:
|
||||||
|
summary: Get web push notification settings for a user
|
||||||
|
description: |
|
||||||
|
Returns web push notification settings for a user in a JSON object.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
- in: path
|
||||||
|
name: key
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User web push notification settings in JSON
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
endpoint:
|
||||||
|
type: string
|
||||||
|
p256dh:
|
||||||
|
type: string
|
||||||
|
auth:
|
||||||
|
type: string
|
||||||
|
userAgent:
|
||||||
|
type: string
|
||||||
|
delete:
|
||||||
|
summary: Delete user push subscription by key
|
||||||
|
description: Deletes the user push subscription with the provided key.
|
||||||
|
tags:
|
||||||
|
- users
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: userId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
- in: path
|
||||||
|
name: key
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Successfully removed user push subscription
|
||||||
/user/{userId}:
|
/user/{userId}:
|
||||||
get:
|
get:
|
||||||
summary: Get user by ID
|
summary: Get user by ID
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"@svgr/webpack": "6.5.1",
|
"@svgr/webpack": "6.5.1",
|
||||||
"@tanem/react-nprogress": "5.0.30",
|
"@tanem/react-nprogress": "5.0.30",
|
||||||
"@types/wink-jaro-distance": "^2.0.2",
|
"@types/wink-jaro-distance": "^2.0.2",
|
||||||
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"ace-builds": "1.15.2",
|
"ace-builds": "1.15.2",
|
||||||
"bcrypt": "5.1.0",
|
"bcrypt": "5.1.0",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"gravatar-url": "3.1.0",
|
"gravatar-url": "3.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mime": "3",
|
"mime": "3",
|
||||||
"next": "^14.2.24",
|
"next": "^14.2.25",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-gyp": "9.3.1",
|
"node-gyp": "9.3.1",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
@@ -99,6 +100,7 @@
|
|||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"typeorm": "0.3.11",
|
"typeorm": "0.3.11",
|
||||||
"undici": "^7.3.0",
|
"undici": "^7.3.0",
|
||||||
|
"ua-parser-js": "^1.0.35",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"wink-jaro-distance": "^2.0.0",
|
"wink-jaro-distance": "^2.0.0",
|
||||||
"winston": "3.8.2",
|
"winston": "3.8.2",
|
||||||
|
|||||||
111
pnpm-lock.yaml
generated
111
pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ importers:
|
|||||||
'@tanem/react-nprogress':
|
'@tanem/react-nprogress':
|
||||||
specifier: 5.0.30
|
specifier: 5.0.30
|
||||||
version: 5.0.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 5.0.30(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@types/ua-parser-js':
|
||||||
|
specifier: ^0.7.36
|
||||||
|
version: 0.7.39
|
||||||
'@types/wink-jaro-distance':
|
'@types/wink-jaro-distance':
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
@@ -105,8 +108,8 @@ importers:
|
|||||||
specifier: '3'
|
specifier: '3'
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
next:
|
next:
|
||||||
specifier: ^14.2.24
|
specifier: ^14.2.25
|
||||||
version: 14.2.24(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 14.2.25(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
node-cache:
|
node-cache:
|
||||||
specifier: 5.1.2
|
specifier: 5.1.2
|
||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
@@ -206,6 +209,9 @@ importers:
|
|||||||
typeorm:
|
typeorm:
|
||||||
specifier: 0.3.11
|
specifier: 0.3.11
|
||||||
version: 0.3.11(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))
|
version: 0.3.11(pg@8.11.0)(sqlite3@5.1.7)(ts-node@10.9.1(@swc/core@1.6.5(@swc/helpers@0.5.11))(@types/node@22.10.5)(typescript@4.9.5))
|
||||||
|
ua-parser-js:
|
||||||
|
specifier: ^1.0.35
|
||||||
|
version: 1.0.40
|
||||||
undici:
|
undici:
|
||||||
specifier: ^7.3.0
|
specifier: ^7.3.0
|
||||||
version: 7.3.0
|
version: 7.3.0
|
||||||
@@ -2133,62 +2139,62 @@ packages:
|
|||||||
'@messageformat/runtime@3.0.1':
|
'@messageformat/runtime@3.0.1':
|
||||||
resolution: {integrity: sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==}
|
resolution: {integrity: sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==}
|
||||||
|
|
||||||
'@next/env@14.2.24':
|
'@next/env@14.2.25':
|
||||||
resolution: {integrity: sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==}
|
resolution: {integrity: sha512-JnzQ2cExDeG7FxJwqAksZ3aqVJrHjFwZQAEJ9gQZSoEhIow7SNoKZzju/AwQ+PLIR4NY8V0rhcVozx/2izDO0w==}
|
||||||
|
|
||||||
'@next/eslint-plugin-next@14.2.4':
|
'@next/eslint-plugin-next@14.2.4':
|
||||||
resolution: {integrity: sha512-svSFxW9f3xDaZA3idQmlFw7SusOuWTpDTAeBlO3AEPDltrraV+lqs7mAc6A27YdnpQVVIA3sODqUAAHdWhVWsA==}
|
resolution: {integrity: sha512-svSFxW9f3xDaZA3idQmlFw7SusOuWTpDTAeBlO3AEPDltrraV+lqs7mAc6A27YdnpQVVIA3sODqUAAHdWhVWsA==}
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@14.2.24':
|
'@next/swc-darwin-arm64@14.2.25':
|
||||||
resolution: {integrity: sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==}
|
resolution: {integrity: sha512-09clWInF1YRd6le00vt750s3m7SEYNehz9C4PUcSu3bAdCTpjIV4aTYQZ25Ehrr83VR1rZeqtKUPWSI7GfuKZQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/swc-darwin-x64@14.2.24':
|
'@next/swc-darwin-x64@14.2.25':
|
||||||
resolution: {integrity: sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==}
|
resolution: {integrity: sha512-V+iYM/QR+aYeJl3/FWWU/7Ix4b07ovsQ5IbkwgUK29pTHmq+5UxeDr7/dphvtXEq5pLB/PucfcBNh9KZ8vWbug==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-gnu@14.2.24':
|
'@next/swc-linux-arm64-gnu@14.2.25':
|
||||||
resolution: {integrity: sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==}
|
resolution: {integrity: sha512-LFnV2899PJZAIEHQ4IMmZIgL0FBieh5keMnriMY1cK7ompR+JUd24xeTtKkcaw8QmxmEdhoE5Mu9dPSuDBgtTg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@14.2.24':
|
'@next/swc-linux-arm64-musl@14.2.25':
|
||||||
resolution: {integrity: sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==}
|
resolution: {integrity: sha512-QC5y5PPTmtqFExcKWKYgUNkHeHE/z3lUsu83di488nyP0ZzQ3Yse2G6TCxz6nNsQwgAx1BehAJTZez+UQxzLfw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@14.2.24':
|
'@next/swc-linux-x64-gnu@14.2.25':
|
||||||
resolution: {integrity: sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==}
|
resolution: {integrity: sha512-y6/ML4b9eQ2D/56wqatTJN5/JR8/xdObU2Fb1RBidnrr450HLCKr6IJZbPqbv7NXmje61UyxjF5kvSajvjye5w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@14.2.24':
|
'@next/swc-linux-x64-musl@14.2.25':
|
||||||
resolution: {integrity: sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==}
|
resolution: {integrity: sha512-sPX0TSXHGUOZFvv96GoBXpB3w4emMqKeMgemrSxI7A6l55VBJp/RKYLwZIB9JxSqYPApqiREaIIap+wWq0RU8w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@14.2.24':
|
'@next/swc-win32-arm64-msvc@14.2.25':
|
||||||
resolution: {integrity: sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==}
|
resolution: {integrity: sha512-ReO9S5hkA1DU2cFCsGoOEp7WJkhFzNbU/3VUF6XxNGUCQChyug6hZdYL/istQgfT/GWE6PNIg9cm784OI4ddxQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@next/swc-win32-ia32-msvc@14.2.24':
|
'@next/swc-win32-ia32-msvc@14.2.25':
|
||||||
resolution: {integrity: sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==}
|
resolution: {integrity: sha512-DZ/gc0o9neuCDyD5IumyTGHVun2dCox5TfPQI/BJTYwpSNYM3CZDI4i6TOdjeq1JMo+Ug4kPSMuZdwsycwFbAw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@next/swc-win32-x64-msvc@14.2.24':
|
'@next/swc-win32-x64-msvc@14.2.25':
|
||||||
resolution: {integrity: sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==}
|
resolution: {integrity: sha512-KSznmS6eFjQ9RJ1nEc66kJvtGIL1iZMYmGEXsZPh2YtnLtqrgdVvKXJY2ScjjoFnG6nGLyPFR0UiEvDwVah4Tw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -3412,6 +3418,9 @@ packages:
|
|||||||
'@types/triple-beam@1.3.5':
|
'@types/triple-beam@1.3.5':
|
||||||
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
||||||
|
|
||||||
|
'@types/ua-parser-js@0.7.39':
|
||||||
|
resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==}
|
||||||
|
|
||||||
'@types/unist@2.0.10':
|
'@types/unist@2.0.10':
|
||||||
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
||||||
|
|
||||||
@@ -7017,8 +7026,8 @@ packages:
|
|||||||
nerf-dart@1.0.0:
|
nerf-dart@1.0.0:
|
||||||
resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==}
|
resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==}
|
||||||
|
|
||||||
next@14.2.24:
|
next@14.2.25:
|
||||||
resolution: {integrity: sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==}
|
resolution: {integrity: sha512-N5M7xMc4wSb4IkPvEV5X2BRRXUmhVHNyaXwEM86+voXthSZz8ZiRyQW4p9mwAoAPIm6OzuVZtn7idgEJeAJN3Q==}
|
||||||
engines: {node: '>=18.17.0'}
|
engines: {node: '>=18.17.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -9223,6 +9232,10 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ua-parser-js@1.0.40:
|
||||||
|
resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
uc.micro@2.1.0:
|
uc.micro@2.1.0:
|
||||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||||
|
|
||||||
@@ -11860,37 +11873,37 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
make-plural: 7.4.0
|
make-plural: 7.4.0
|
||||||
|
|
||||||
'@next/env@14.2.24': {}
|
'@next/env@14.2.25': {}
|
||||||
|
|
||||||
'@next/eslint-plugin-next@14.2.4':
|
'@next/eslint-plugin-next@14.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 10.3.10
|
glob: 10.3.10
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@14.2.24':
|
'@next/swc-darwin-arm64@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-darwin-x64@14.2.24':
|
'@next/swc-darwin-x64@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-arm64-gnu@14.2.24':
|
'@next/swc-linux-arm64-gnu@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@14.2.24':
|
'@next/swc-linux-arm64-musl@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@14.2.24':
|
'@next/swc-linux-x64-gnu@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@14.2.24':
|
'@next/swc-linux-x64-musl@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@14.2.24':
|
'@next/swc-win32-arm64-msvc@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-ia32-msvc@14.2.24':
|
'@next/swc-win32-ia32-msvc@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-x64-msvc@14.2.24':
|
'@next/swc-win32-x64-msvc@14.2.25':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
@@ -13506,7 +13519,7 @@ snapshots:
|
|||||||
'@swc/helpers@0.5.5':
|
'@swc/helpers@0.5.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/counter': 0.1.3
|
'@swc/counter': 0.1.3
|
||||||
tslib: 2.6.3
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@swc/types@0.1.17':
|
'@swc/types@0.1.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13778,6 +13791,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/triple-beam@1.3.5': {}
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
|
'@types/ua-parser-js@0.7.39': {}
|
||||||
|
|
||||||
'@types/unist@2.0.10': {}
|
'@types/unist@2.0.10': {}
|
||||||
|
|
||||||
'@types/web-push@3.3.2':
|
'@types/web-push@3.3.2':
|
||||||
@@ -18289,27 +18304,27 @@ snapshots:
|
|||||||
|
|
||||||
nerf-dart@1.0.0: {}
|
nerf-dart@1.0.0: {}
|
||||||
|
|
||||||
next@14.2.24(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
next@14.2.25(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 14.2.24
|
'@next/env': 14.2.25
|
||||||
'@swc/helpers': 0.5.5
|
'@swc/helpers': 0.5.5
|
||||||
busboy: 1.6.0
|
busboy: 1.6.0
|
||||||
caniuse-lite: 1.0.30001636
|
caniuse-lite: 1.0.30001700
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
styled-jsx: 5.1.1(@babel/core@7.24.7)(react@18.3.1)
|
styled-jsx: 5.1.1(@babel/core@7.24.7)(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 14.2.24
|
'@next/swc-darwin-arm64': 14.2.25
|
||||||
'@next/swc-darwin-x64': 14.2.24
|
'@next/swc-darwin-x64': 14.2.25
|
||||||
'@next/swc-linux-arm64-gnu': 14.2.24
|
'@next/swc-linux-arm64-gnu': 14.2.25
|
||||||
'@next/swc-linux-arm64-musl': 14.2.24
|
'@next/swc-linux-arm64-musl': 14.2.25
|
||||||
'@next/swc-linux-x64-gnu': 14.2.24
|
'@next/swc-linux-x64-gnu': 14.2.25
|
||||||
'@next/swc-linux-x64-musl': 14.2.24
|
'@next/swc-linux-x64-musl': 14.2.25
|
||||||
'@next/swc-win32-arm64-msvc': 14.2.24
|
'@next/swc-win32-arm64-msvc': 14.2.25
|
||||||
'@next/swc-win32-ia32-msvc': 14.2.24
|
'@next/swc-win32-ia32-msvc': 14.2.25
|
||||||
'@next/swc-win32-x64-msvc': 14.2.24
|
'@next/swc-win32-x64-msvc': 14.2.25
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
@@ -20672,6 +20687,8 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.5.2: {}
|
typescript@5.5.2: {}
|
||||||
|
|
||||||
|
ua-parser-js@1.0.40: {}
|
||||||
|
|
||||||
uc.micro@2.1.0:
|
uc.micro@2.1.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ class ExternalAPI {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected removeCache(endpoint: string, options?: Record<string, string>) {
|
protected removeCache(endpoint: string, options?: Record<string, unknown>) {
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
...this.params,
|
...this.params,
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -110,11 +110,18 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
deviceId?: string | null
|
deviceId?: string | null
|
||||||
) {
|
) {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
const safeDeviceId =
|
||||||
|
deviceId && deviceId.length > 0
|
||||||
|
? deviceId
|
||||||
|
: Buffer.from(`BOT_jellyseerr_fallback_${Date.now()}`).toString(
|
||||||
|
'base64'
|
||||||
|
);
|
||||||
|
|
||||||
let authHeaderVal: string;
|
let authHeaderVal: string;
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
|
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${safeDeviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
|
||||||
} else {
|
} else {
|
||||||
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}"`;
|
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${safeDeviceId}", Version="${getAppVersion()}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
super(
|
super(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface RadarrMovie {
|
|||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
added: string;
|
added: string;
|
||||||
hasFile: boolean;
|
hasFile: boolean;
|
||||||
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||||
@@ -104,7 +105,7 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
minimumAvailability: options.minimumAvailability,
|
minimumAvailability: options.minimumAvailability,
|
||||||
tmdbId: options.tmdbId,
|
tmdbId: options.tmdbId,
|
||||||
year: options.year,
|
year: options.year,
|
||||||
tags: options.tags,
|
tags: Array.from(new Set([...movie.tags, ...options.tags])),
|
||||||
rootFolderPath: options.rootFolderPath,
|
rootFolderPath: options.rootFolderPath,
|
||||||
monitored: options.monitored,
|
monitored: options.monitored,
|
||||||
addOptions: {
|
addOptions: {
|
||||||
@@ -241,10 +242,13 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
|||||||
if (tmdbId) {
|
if (tmdbId) {
|
||||||
this.removeCache('/movie/lookup', {
|
this.removeCache('/movie/lookup', {
|
||||||
term: `tmdb:${tmdbId}`,
|
term: `tmdb:${tmdbId}`,
|
||||||
|
headers: this.defaultHeaders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (externalId) {
|
if (externalId) {
|
||||||
this.removeCache(`/movie/${externalId}`);
|
this.removeCache(`/movie/${externalId}`, {
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,9 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
// If the series already exists, we will simply just update it
|
// If the series already exists, we will simply just update it
|
||||||
if (series.id) {
|
if (series.id) {
|
||||||
series.monitored = options.monitored ?? series.monitored;
|
series.monitored = options.monitored ?? series.monitored;
|
||||||
series.tags = options.tags ?? series.tags;
|
series.tags = options.tags
|
||||||
|
? Array.from(new Set([...series.tags, ...options.tags]))
|
||||||
|
: series.tags;
|
||||||
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
|
||||||
|
|
||||||
const newSeriesData = await this.put<SonarrSeries>(
|
const newSeriesData = await this.put<SonarrSeries>(
|
||||||
@@ -366,14 +368,18 @@ class SonarrAPI extends ServarrBase<{
|
|||||||
if (tvdbId) {
|
if (tvdbId) {
|
||||||
this.removeCache('/series/lookup', {
|
this.removeCache('/series/lookup', {
|
||||||
term: `tvdb:${tvdbId}`,
|
term: `tvdb:${tvdbId}`,
|
||||||
|
headers: this.defaultHeaders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (externalId) {
|
if (externalId) {
|
||||||
this.removeCache(`/series/${externalId}`);
|
this.removeCache(`/series/${externalId}`, {
|
||||||
|
headers: this.defaultHeaders,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (title) {
|
if (title) {
|
||||||
this.removeCache('/series/lookup', {
|
this.removeCache('/series/lookup', {
|
||||||
term: title,
|
term: title,
|
||||||
|
headers: this.defaultHeaders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ export class User {
|
|||||||
@Column()
|
@Column()
|
||||||
public avatar: string;
|
public avatar: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
public avatarETag?: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
public avatarVersion?: string | null;
|
||||||
|
|
||||||
@RelationCount((user: User) => user.requests)
|
@RelationCount((user: User) => user.requests)
|
||||||
public requestCount: number;
|
public requestCount: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -18,9 +24,15 @@ export class UserPushSubscription {
|
|||||||
@Column()
|
@Column()
|
||||||
public p256dh: string;
|
public p256dh: string;
|
||||||
|
|
||||||
@Column({ unique: true })
|
@Column()
|
||||||
public auth: string;
|
public auth: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
public userAgent: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ nullable: true })
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
constructor(init?: Partial<UserPushSubscription>) {
|
constructor(init?: Partial<UserPushSubscription>) {
|
||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
|
|||||||
import type { NonFunctionProperties, PaginatedResponse } from './common';
|
import type { NonFunctionProperties, PaginatedResponse } from './common';
|
||||||
|
|
||||||
export interface RequestResultsResponse extends PaginatedResponse {
|
export interface RequestResultsResponse extends PaginatedResponse {
|
||||||
results: NonFunctionProperties<MediaRequest>[];
|
results: (NonFunctionProperties<MediaRequest> & {
|
||||||
|
profileName?: string;
|
||||||
|
canRemove?: boolean;
|
||||||
|
})[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MediaRequestBody = {
|
export type MediaRequestBody = {
|
||||||
|
|||||||
@@ -193,14 +193,34 @@ class ImageProxy {
|
|||||||
public async clearCachedImage(path: string) {
|
public async clearCachedImage(path: string) {
|
||||||
// find cacheKey
|
// find cacheKey
|
||||||
const cacheKey = this.getCacheKey(path);
|
const cacheKey = this.getCacheKey(path);
|
||||||
|
const directory = join(this.getCacheDirectory(), cacheKey);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await promises.access(directory);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ENOENT') {
|
||||||
|
logger.debug(
|
||||||
|
`Cache directory '${cacheKey}' does not exist; nothing to clear.`,
|
||||||
|
{
|
||||||
|
label: 'Image Cache',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
logger.error('Error checking cache directory existence', {
|
||||||
|
label: 'Image Cache',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const directory = join(this.getCacheDirectory(), cacheKey);
|
|
||||||
const files = await promises.readdir(directory);
|
const files = await promises.readdir(directory);
|
||||||
|
|
||||||
await promises.rm(directory, { recursive: true });
|
await promises.rm(directory, { recursive: true });
|
||||||
|
|
||||||
logger.info(`Cleared ${files[0]} from cache 'avatar'`, {
|
logger.debug(`Cleared ${files[0]} from cache 'avatar'`, {
|
||||||
label: 'Image Cache',
|
label: 'Image Cache',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
29
server/migration/postgres/1743023615532-UpdateWebPush.ts
Normal file
29
server/migration/postgres/1743023615532-UpdateWebPush.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class UpdateWebPush1743023615532 implements MigrationInterface {
|
||||||
|
name = 'UpdateWebPush1743023615532';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" ADD "userAgent" character varying`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" ADD "createdAt" TIMESTAMP DEFAULT now()`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" DROP CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" ADD CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth")`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" DROP COLUMN "createdAt"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" DROP COLUMN "userAgent"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddUserAvatarCacheFields1743107707465
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddUserAvatarCacheFields1743107707465';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user" ADD "avatarETag" character varying`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user" ADD "avatarVersion" character varying`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarVersion"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarETag"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
203
server/migration/sqlite/1743023610704-UpdateWebPush.ts
Normal file
203
server/migration/sqlite/1743023610704-UpdateWebPush.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class UpdateWebPush1743023610704 implements MigrationInterface {
|
||||||
|
name = 'UpdateWebPush1743023610704';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "status4k" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaAddedAt" datetime DEFAULT (CURRENT_TIMESTAMP), "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaId" varchar, "jellyfinMediaId4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k" FROM "media"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "media"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_939f205946256cc0d2a1ac51a8"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_watchlist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ratingKey" varchar NOT NULL, "mediaType" varchar NOT NULL, "title" varchar NOT NULL, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_watchlist"("id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId") SELECT "id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId" FROM "watchlist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "watchlist"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_watchlist" RENAME TO "watchlist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"), CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "blacklist" RENAME TO "temporary_blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "temporary_blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_blacklist"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_939f205946256cc0d2a1ac51a8"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "watchlist" RENAME TO "temporary_watchlist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "watchlist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ratingKey" varchar NOT NULL, "mediaType" varchar NOT NULL, "title" varchar NOT NULL, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "watchlist"("id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId") SELECT "id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId" FROM "temporary_watchlist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_watchlist"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "status4k" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaId" varchar, "jellyfinMediaId4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k" FROM "temporary_media"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_media"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "blacklist" RENAME TO "temporary_blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "temporary_blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_blacklist"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_6bbafa28411e6046421991ea21"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "blacklist" RENAME TO "temporary_blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId", "userId"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId" FROM "temporary_blacklist"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_blacklist"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId") SELECT "id", "endpoint", "p256dh", "auth", "userId" FROM "temporary_user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddUserAvatarCacheFields1743107645301
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddUserAvatarCacheFields1743107645301';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId" FROM "user"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId" FROM "temporary_user"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { Permission } from '@server/lib/permissions';
|
|||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { isAuthenticated } from '@server/middleware/auth';
|
import { isAuthenticated } from '@server/middleware/auth';
|
||||||
|
import { checkAvatarChanged } from '@server/routes/avatarproxy';
|
||||||
import { ApiError } from '@server/types/error';
|
import { ApiError } from '@server/types/error';
|
||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import * as EmailValidator from 'email-validator';
|
import * as EmailValidator from 'email-validator';
|
||||||
@@ -216,6 +217,10 @@ authRoutes.post('/plex', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getUserAvatarUrl(user: User): string {
|
||||||
|
return `/avatarproxy/${user.jellyfinUserId}?v=${user.avatarVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
authRoutes.post('/jellyfin', async (req, res, next) => {
|
authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
@@ -343,12 +348,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
jellyfinAuthToken: account.AccessToken,
|
jellyfinAuthToken: account.AccessToken,
|
||||||
permissions: Permission.ADMIN,
|
permissions: Permission.ADMIN,
|
||||||
avatar: `/avatarproxy/${account.User.Id}`,
|
|
||||||
userType:
|
userType:
|
||||||
body.serverType === MediaServerType.JELLYFIN
|
body.serverType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
: UserType.EMBY,
|
: UserType.EMBY,
|
||||||
});
|
});
|
||||||
|
user.avatar = getUserAvatarUrl(user);
|
||||||
|
|
||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
} else {
|
} else {
|
||||||
@@ -375,7 +380,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
user.jellyfinDeviceId = deviceId;
|
user.jellyfinDeviceId = deviceId;
|
||||||
user.jellyfinAuthToken = account.AccessToken;
|
user.jellyfinAuthToken = account.AccessToken;
|
||||||
user.permissions = Permission.ADMIN;
|
user.permissions = Permission.ADMIN;
|
||||||
user.avatar = `/avatarproxy/${account.User.Id}`;
|
user.avatar = getUserAvatarUrl(user);
|
||||||
user.userType =
|
user.userType =
|
||||||
body.serverType === MediaServerType.JELLYFIN
|
body.serverType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
@@ -422,7 +427,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
user.avatar = `/avatarproxy/${account.User.Id}`;
|
user.avatar = getUserAvatarUrl(user);
|
||||||
user.jellyfinUsername = account.User.Name;
|
user.jellyfinUsername = account.User.Name;
|
||||||
|
|
||||||
if (user.username === account.User.Name) {
|
if (user.username === account.User.Name) {
|
||||||
@@ -460,12 +465,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
jellyfinUserId: account.User.Id,
|
jellyfinUserId: account.User.Id,
|
||||||
jellyfinDeviceId: deviceId,
|
jellyfinDeviceId: deviceId,
|
||||||
permissions: settings.main.defaultPermissions,
|
permissions: settings.main.defaultPermissions,
|
||||||
avatar: `/avatarproxy/${account.User.Id}`,
|
|
||||||
userType:
|
userType:
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
? UserType.JELLYFIN
|
? UserType.JELLYFIN
|
||||||
: UserType.EMBY,
|
: UserType.EMBY,
|
||||||
});
|
});
|
||||||
|
user.avatar = getUserAvatarUrl(user);
|
||||||
|
|
||||||
//initialize Jellyfin/Emby users with local login
|
//initialize Jellyfin/Emby users with local login
|
||||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||||
@@ -475,6 +480,26 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
await userRepository.save(user);
|
await userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user && user.jellyfinUserId) {
|
||||||
|
try {
|
||||||
|
const { changed } = await checkAvatarChanged(user);
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
user.avatar = getUserAvatarUrl(user);
|
||||||
|
await userRepository.save(user);
|
||||||
|
logger.debug('Avatar updated during login', {
|
||||||
|
userId: user.id,
|
||||||
|
jellyfinUserId: user.jellyfinUserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error handling avatar during login', {
|
||||||
|
label: 'Auth',
|
||||||
|
errorMessage: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set logged in session
|
// Set logged in session
|
||||||
if (req.session) {
|
if (req.session) {
|
||||||
req.session.userId = user?.id;
|
req.session.userId = user?.id;
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import { getAppVersion } from '@server/utils/appVersion';
|
|||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import gravatarUrl from 'gravatar-url';
|
import gravatarUrl from 'gravatar-url';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
let _avatarImageProxy: ImageProxy | null = null;
|
let _avatarImageProxy: ImageProxy | null = null;
|
||||||
|
|
||||||
async function initAvatarImageProxy() {
|
async function initAvatarImageProxy() {
|
||||||
if (!_avatarImageProxy) {
|
if (!_avatarImageProxy) {
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
@@ -31,6 +33,79 @@ async function initAvatarImageProxy() {
|
|||||||
return _avatarImageProxy;
|
return _avatarImageProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getJellyfinAvatarUrl(userId: string) {
|
||||||
|
const settings = getSettings();
|
||||||
|
return settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
|
? `${getHostname()}/UserImage?UserId=${userId}`
|
||||||
|
: `${getHostname()}/Users/${userId}/Images/Primary?quality=90`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeImageHash(buffer: Buffer): string {
|
||||||
|
return createHash('sha256').update(buffer).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAvatarChanged(
|
||||||
|
user: User
|
||||||
|
): Promise<{ changed: boolean; etag?: string }> {
|
||||||
|
try {
|
||||||
|
if (!user || !user.jellyfinUserId) {
|
||||||
|
return { changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const jellyfinAvatarUrl = getJellyfinAvatarUrl(user.jellyfinUserId);
|
||||||
|
|
||||||
|
const headResponse = await fetch(jellyfinAvatarUrl, { method: 'HEAD' });
|
||||||
|
if (!headResponse.ok) {
|
||||||
|
return { changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getSettings();
|
||||||
|
let remoteVersion: string;
|
||||||
|
if (settings.main.mediaServerType === MediaServerType.JELLYFIN) {
|
||||||
|
const remoteLastModifiedStr =
|
||||||
|
headResponse.headers.get('last-modified') || '';
|
||||||
|
remoteVersion = (
|
||||||
|
Date.parse(remoteLastModifiedStr) || Date.now()
|
||||||
|
).toString();
|
||||||
|
} else if (settings.main.mediaServerType === MediaServerType.EMBY) {
|
||||||
|
remoteVersion =
|
||||||
|
headResponse.headers.get('etag')?.replace(/"/g, '') ||
|
||||||
|
Date.now().toString();
|
||||||
|
} else {
|
||||||
|
remoteVersion = Date.now().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.avatarVersion && user.avatarVersion === remoteVersion) {
|
||||||
|
return { changed: false, etag: user.avatarETag ?? undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarImageCache = await initAvatarImageProxy();
|
||||||
|
await avatarImageCache.clearCachedImage(jellyfinAvatarUrl);
|
||||||
|
const imageData = await avatarImageCache.getImage(
|
||||||
|
jellyfinAvatarUrl,
|
||||||
|
gravatarUrl(user.email || 'none', { default: 'mm', size: 200 })
|
||||||
|
);
|
||||||
|
|
||||||
|
const newHash = computeImageHash(imageData.imageBuffer);
|
||||||
|
|
||||||
|
const hasChanged = user.avatarETag !== newHash;
|
||||||
|
|
||||||
|
user.avatarVersion = remoteVersion;
|
||||||
|
if (hasChanged) {
|
||||||
|
user.avatarETag = newHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
await getRepository(User).save(user);
|
||||||
|
|
||||||
|
return { changed: hasChanged, etag: newHash };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error checking avatar changes', {
|
||||||
|
errorMessage: error.message,
|
||||||
|
});
|
||||||
|
return { changed: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
router.get('/:jellyfinUserId', async (req, res) => {
|
router.get('/:jellyfinUserId', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) {
|
if (!req.params.jellyfinUserId.match(/^[a-f0-9]{32}$/)) {
|
||||||
@@ -46,6 +121,10 @@ router.get('/:jellyfinUserId', async (req, res) => {
|
|||||||
|
|
||||||
const avatarImageCache = await initAvatarImageProxy();
|
const avatarImageCache = await initAvatarImageProxy();
|
||||||
|
|
||||||
|
const userEtag = req.headers['if-none-match'];
|
||||||
|
|
||||||
|
const versionParam = req.query.v;
|
||||||
|
|
||||||
const user = await getRepository(User).findOne({
|
const user = await getRepository(User).findOne({
|
||||||
where: { jellyfinUserId: req.params.jellyfinUserId },
|
where: { jellyfinUserId: req.params.jellyfinUserId },
|
||||||
});
|
});
|
||||||
@@ -55,13 +134,7 @@ router.get('/:jellyfinUserId', async (req, res) => {
|
|||||||
size: 200,
|
size: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
const setttings = getSettings();
|
const jellyfinAvatarUrl = getJellyfinAvatarUrl(req.params.jellyfinUserId);
|
||||||
const jellyfinAvatarUrl =
|
|
||||||
setttings.main.mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? `${getHostname()}/UserImage?UserId=${req.params.jellyfinUserId}`
|
|
||||||
: `${getHostname()}/Users/${
|
|
||||||
req.params.jellyfinUserId
|
|
||||||
}/Images/Primary?quality=90`;
|
|
||||||
|
|
||||||
let imageData = await avatarImageCache.getImage(
|
let imageData = await avatarImageCache.getImage(
|
||||||
jellyfinAvatarUrl,
|
jellyfinAvatarUrl,
|
||||||
@@ -73,10 +146,15 @@ router.get('/:jellyfinUserId', async (req, res) => {
|
|||||||
imageData = await avatarImageCache.getImage(fallbackUrl);
|
imageData = await avatarImageCache.getImage(fallbackUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userEtag && userEtag === `"${imageData.meta.etag}"` && !versionParam) {
|
||||||
|
return res.status(304).end();
|
||||||
|
}
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': `image/${imageData.meta.extension}`,
|
'Content-Type': `image/${imageData.meta.extension}`,
|
||||||
'Content-Length': imageData.imageBuffer.length,
|
'Content-Length': imageData.imageBuffer.length,
|
||||||
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
|
'Cache-Control': `public, max-age=${imageData.meta.curRevalidate}`,
|
||||||
|
ETag: `"${imageData.meta.etag}"`,
|
||||||
'OS-Cache-Key': imageData.meta.cacheKey,
|
'OS-Cache-Key': imageData.meta.cacheKey,
|
||||||
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
|
'OS-Cache-Status': imageData.meta.cacheMiss ? 'MISS' : 'HIT',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -237,19 +237,6 @@ mediaRoutes.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMovie) {
|
if (isMovie) {
|
||||||
// check if the movie exists
|
|
||||||
try {
|
|
||||||
await (service as RadarrAPI).getMovie({
|
|
||||||
id: parseInt(
|
|
||||||
is4k
|
|
||||||
? (media.externalServiceSlug4k as string)
|
|
||||||
: (media.externalServiceSlug as string)
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return res.status(204).send();
|
|
||||||
}
|
|
||||||
// remove the movie
|
|
||||||
await (service as RadarrAPI).removeMovie(
|
await (service as RadarrAPI).removeMovie(
|
||||||
parseInt(
|
parseInt(
|
||||||
is4k
|
is4k
|
||||||
@@ -264,13 +251,6 @@ mediaRoutes.delete(
|
|||||||
if (!tvdbId) {
|
if (!tvdbId) {
|
||||||
throw new Error('TVDB ID not found');
|
throw new Error('TVDB ID not found');
|
||||||
}
|
}
|
||||||
// check if the series exists
|
|
||||||
try {
|
|
||||||
await (service as SonarrAPI).getSeriesByTvdbId(tvdbId);
|
|
||||||
} catch {
|
|
||||||
return res.status(204).send();
|
|
||||||
}
|
|
||||||
// remove the series
|
|
||||||
await (service as SonarrAPI).removeSerie(tvdbId);
|
await (service as SonarrAPI).removeSerie(tvdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// add profile names to the media requests, with undefined if not found
|
// add profile names to the media requests, with undefined if not found
|
||||||
const requestsWithProfileNames = requests.map((r) => {
|
let mappedRequests = requests.map((r) => {
|
||||||
switch (r.type) {
|
switch (r.type) {
|
||||||
case MediaType.MOVIE: {
|
case MediaType.MOVIE: {
|
||||||
const profileName = radarrServers
|
const profileName = radarrServers
|
||||||
@@ -212,6 +212,36 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// add canRemove prop if user has permission
|
||||||
|
if (req.user?.hasPermission(Permission.MANAGE_REQUESTS)) {
|
||||||
|
mappedRequests = mappedRequests.map((r) => {
|
||||||
|
switch (r.type) {
|
||||||
|
case MediaType.MOVIE: {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
// check if the radarr server for this request is configured
|
||||||
|
canRemove: radarrServers.some(
|
||||||
|
(server) =>
|
||||||
|
server.id ===
|
||||||
|
(r.is4k ? r.media.serviceId4k : r.media.serviceId)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case MediaType.TV: {
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
// check if the sonarr server for this request is configured
|
||||||
|
canRemove: sonarrServers.some(
|
||||||
|
(server) =>
|
||||||
|
server.id ===
|
||||||
|
(r.is4k ? r.media.serviceId4k : r.media.serviceId)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
pages: Math.ceil(requestCount / pageSize),
|
pages: Math.ceil(requestCount / pageSize),
|
||||||
@@ -219,7 +249,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
|
|||||||
results: requestCount,
|
results: requestCount,
|
||||||
page: Math.ceil(skip / pageSize) + 1,
|
page: Math.ceil(skip / pageSize) + 1,
|
||||||
},
|
},
|
||||||
results: requestsWithProfileNames,
|
results: mappedRequests,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next({ status: 500, message: e.message });
|
next({ status: 500, message: e.message });
|
||||||
|
|||||||
@@ -184,13 +184,15 @@ router.post<
|
|||||||
endpoint: string;
|
endpoint: string;
|
||||||
p256dh: string;
|
p256dh: string;
|
||||||
auth: string;
|
auth: string;
|
||||||
|
userAgent: string;
|
||||||
}
|
}
|
||||||
>('/registerPushSubscription', async (req, res, next) => {
|
>('/registerPushSubscription', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const userPushSubRepository = getRepository(UserPushSubscription);
|
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||||
|
|
||||||
const existingSubs = await userPushSubRepository.find({
|
const existingSubs = await userPushSubRepository.find({
|
||||||
where: { auth: req.body.auth },
|
relations: { user: true },
|
||||||
|
where: { auth: req.body.auth, user: { id: req.user?.id } },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingSubs.length > 0) {
|
if (existingSubs.length > 0) {
|
||||||
@@ -205,6 +207,7 @@ router.post<
|
|||||||
auth: req.body.auth,
|
auth: req.body.auth,
|
||||||
endpoint: req.body.endpoint,
|
endpoint: req.body.endpoint,
|
||||||
p256dh: req.body.p256dh,
|
p256dh: req.body.p256dh,
|
||||||
|
userAgent: req.body.userAgent,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,6 +222,79 @@ router.post<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get<{ userId: number }>(
|
||||||
|
'/:userId/pushSubscriptions',
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||||
|
|
||||||
|
const userPushSubs = await userPushSubRepository.find({
|
||||||
|
relations: { user: true },
|
||||||
|
where: { user: { id: req.params.userId } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(userPushSubs);
|
||||||
|
} catch (e) {
|
||||||
|
next({ status: 404, message: 'User subscriptions not found.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get<{ userId: number; key: string }>(
|
||||||
|
'/:userId/pushSubscription/:key',
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||||
|
|
||||||
|
const userPushSub = await userPushSubRepository.findOneOrFail({
|
||||||
|
relations: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
user: { id: req.params.userId },
|
||||||
|
p256dh: req.params.key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json(userPushSub);
|
||||||
|
} catch (e) {
|
||||||
|
next({ status: 404, message: 'User subscription not found.' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete<{ userId: number; key: string }>(
|
||||||
|
'/:userId/pushSubscription/:key',
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||||
|
|
||||||
|
const userPushSub = await userPushSubRepository.findOneOrFail({
|
||||||
|
relations: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
user: { id: req.params.userId },
|
||||||
|
p256dh: req.params.key,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await userPushSubRepository.remove(userPushSub);
|
||||||
|
return res.status(204).send();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong deleting the user push subcription', {
|
||||||
|
label: 'API',
|
||||||
|
key: req.params.key,
|
||||||
|
errorMessage: e.message,
|
||||||
|
});
|
||||||
|
return next({
|
||||||
|
status: 500,
|
||||||
|
message: 'User push subcription not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.get<{ id: string }>('/:id', async (req, res, next) => {
|
router.get<{ id: string }>('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
|
|||||||
@@ -255,7 +255,9 @@ const MobileMenu = ({
|
|||||||
router.pathname.match(link.activeRegExp)
|
router.pathname.match(link.activeRegExp)
|
||||||
? 'border-indigo-600 from-indigo-700 to-purple-700'
|
? 'border-indigo-600 from-indigo-700 to-purple-700'
|
||||||
: 'border-indigo-500 from-indigo-600 to-purple-600'
|
: 'border-indigo-500 from-indigo-600 to-purple-600'
|
||||||
} flex h-4 w-4 items-center justify-center !px-[9px] !py-[9px] text-[9px]`}
|
} flex ${
|
||||||
|
pendingRequestsCount > 99 ? 'w-6' : 'w-4'
|
||||||
|
} h-4 items-center justify-center !px-[5px] !py-[7px] text-[8px]`}
|
||||||
>
|
>
|
||||||
{pendingRequestsCount > 99
|
{pendingRequestsCount > 99
|
||||||
? '99+'
|
? '99+'
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import { MediaRequestStatus, MediaType } from '@server/constants/media';
|
import { MediaRequestStatus } from '@server/constants/media';
|
||||||
import type { MediaRequest } from '@server/entity/MediaRequest';
|
import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
import type { NonFunctionProperties } from '@server/interfaces/api/common';
|
||||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
||||||
import type { MovieDetails } from '@server/models/Movie';
|
import type { MovieDetails } from '@server/models/Movie';
|
||||||
import type { TvDetails } from '@server/models/Tv';
|
import type { TvDetails } from '@server/models/Tv';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -292,18 +292,11 @@ const RequestItemError = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RequestItemProps {
|
interface RequestItemProps {
|
||||||
request: NonFunctionProperties<MediaRequest> & { profileName?: string };
|
request: RequestResultsResponse['results'][number];
|
||||||
revalidateList: () => void;
|
revalidateList: () => void;
|
||||||
radarrData?: RadarrSettings[];
|
|
||||||
sonarrData?: SonarrSettings[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestItem = ({
|
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||||
request,
|
|
||||||
revalidateList,
|
|
||||||
radarrData,
|
|
||||||
sonarrData,
|
|
||||||
}: RequestItemProps) => {
|
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
@@ -398,23 +391,6 @@ const RequestItem = ({
|
|||||||
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
iOSPlexUrl4k: requestData?.media?.iOSPlexUrl4k,
|
||||||
});
|
});
|
||||||
|
|
||||||
const serviceExists = () => {
|
|
||||||
if (title?.mediaInfo) {
|
|
||||||
if (title?.mediaInfo.mediaType === MediaType.MOVIE) {
|
|
||||||
return (
|
|
||||||
radarrData?.find((radarr) => radarr.id === request.serverId) !==
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
sonarrData?.find((sonarr) => sonarr.id === request.serverId) !==
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!title && !error) {
|
if (!title && !error) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -722,6 +698,7 @@ const RequestItem = ({
|
|||||||
)}
|
)}
|
||||||
{requestData.status !== MediaRequestStatus.PENDING &&
|
{requestData.status !== MediaRequestStatus.PENDING &&
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
|
<>
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
onClick={() => deleteRequest()}
|
onClick={() => deleteRequest()}
|
||||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||||
@@ -730,10 +707,7 @@ const RequestItem = ({
|
|||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
<span>{intl.formatMessage(messages.deleterequest)}</span>
|
||||||
</ConfirmButton>
|
</ConfirmButton>
|
||||||
)}
|
{request.canRemove && (
|
||||||
{hasPermission(Permission.MANAGE_REQUESTS) &&
|
|
||||||
title?.mediaInfo?.serviceId &&
|
|
||||||
serviceExists() && (
|
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
onClick={() => deleteMediaFile()}
|
onClick={() => deleteMediaFile()}
|
||||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||||
@@ -747,6 +721,8 @@ const RequestItem = ({
|
|||||||
</span>
|
</span>
|
||||||
</ConfirmButton>
|
</ConfirmButton>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{requestData.status === MediaRequestStatus.PENDING &&
|
{requestData.status === MediaRequestStatus.PENDING &&
|
||||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||||
<div className="flex w-full flex-row space-x-2">
|
<div className="flex w-full flex-row space-x-2">
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import {
|
|||||||
FunnelIcon,
|
FunnelIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
import type { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces';
|
||||||
import { Permission } from '@server/lib/permissions';
|
|
||||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -53,7 +51,7 @@ const RequestList = () => {
|
|||||||
const { user } = useUser({
|
const { user } = useUser({
|
||||||
id: Number(router.query.userId),
|
id: Number(router.query.userId),
|
||||||
});
|
});
|
||||||
const { user: currentUser, hasPermission } = useUser();
|
const { user: currentUser } = useUser();
|
||||||
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
const [currentFilter, setCurrentFilter] = useState<Filter>(Filter.PENDING);
|
||||||
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
const [currentSort, setCurrentSort] = useState<Sort>('added');
|
||||||
const [currentSortDirection, setCurrentSortDirection] =
|
const [currentSortDirection, setCurrentSortDirection] =
|
||||||
@@ -64,13 +62,6 @@ const RequestList = () => {
|
|||||||
const pageIndex = page - 1;
|
const pageIndex = page - 1;
|
||||||
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
const updateQueryParams = useUpdateQueryParams({ page: page.toString() });
|
||||||
|
|
||||||
const { data: radarrData } = useSWR<RadarrSettings[]>(
|
|
||||||
hasPermission(Permission.ADMIN) ? '/api/v1/settings/radarr' : null
|
|
||||||
);
|
|
||||||
const { data: sonarrData } = useSWR<SonarrSettings[]>(
|
|
||||||
hasPermission(Permission.ADMIN) ? '/api/v1/settings/sonarr' : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
error,
|
error,
|
||||||
@@ -254,8 +245,6 @@ const RequestList = () => {
|
|||||||
<RequestItem
|
<RequestItem
|
||||||
request={request}
|
request={request}
|
||||||
revalidateList={() => revalidate()}
|
revalidateList={() => revalidate()}
|
||||||
radarrData={radarrData}
|
|
||||||
sonarrData={sonarrData}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import useSettings from '@app/hooks/useSettings';
|
|
||||||
import { useUser } from '@app/hooks/useUser';
|
import { useUser } from '@app/hooks/useUser';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const ServiceWorkerSetup = () => {
|
const ServiceWorkerSetup = () => {
|
||||||
const { currentSettings } = useSettings();
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ('serviceWorker' in navigator && user?.id) {
|
if ('serviceWorker' in navigator && user?.id) {
|
||||||
@@ -15,40 +14,12 @@ const ServiceWorkerSetup = () => {
|
|||||||
'[SW] Registration successful, scope is:',
|
'[SW] Registration successful, scope is:',
|
||||||
registration.scope
|
registration.scope
|
||||||
);
|
);
|
||||||
|
|
||||||
if (currentSettings.enablePushRegistration) {
|
|
||||||
const sub = await registration.pushManager.subscribe({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: currentSettings.vapidPublic,
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsedSub = JSON.parse(JSON.stringify(sub));
|
|
||||||
|
|
||||||
if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
|
|
||||||
const res = await fetch('/api/v1/user/registerPushSubscription', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
endpoint: parsedSub.endpoint,
|
|
||||||
p256dh: parsedSub.keys.p256dh,
|
|
||||||
auth: parsedSub.keys.auth,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.log('[SW] Service worker registration failed, error:', error);
|
console.log('[SW] Service worker registration failed, error:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [
|
}, [user]);
|
||||||
user,
|
|
||||||
currentSettings.vapidPublic,
|
|
||||||
currentSettings.enablePushRegistration,
|
|
||||||
]);
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const OverrideRuleTiles = ({
|
|||||||
}
|
}
|
||||||
setUsers(users);
|
setUsers(users);
|
||||||
})();
|
})();
|
||||||
}, [rules]);
|
}, [rules, users]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
const { data: createdUsers } = await res.json();
|
const createdUsers = await res.json();
|
||||||
|
|
||||||
if (!createdUsers.length) {
|
if (!Array.isArray(createdUsers) || createdUsers.length === 0) {
|
||||||
throw new Error('No users were imported from Plex.');
|
throw new Error('No users were imported from Plex.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
import Button from '@app/components/Common/Button';
|
|
||||||
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
|
||||||
import NotificationTypeSelector, {
|
|
||||||
ALL_NOTIFICATIONS,
|
|
||||||
} from '@app/components/NotificationTypeSelector';
|
|
||||||
import { useUser } from '@app/hooks/useUser';
|
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
|
||||||
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
|
||||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
|
||||||
import { Form, Formik } from 'formik';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import { useToasts } from 'react-toast-notifications';
|
|
||||||
import useSWR, { mutate } from 'swr';
|
|
||||||
|
|
||||||
const messages = defineMessages(
|
|
||||||
'components.UserProfile.UserSettings.UserNotificationSettings',
|
|
||||||
{
|
|
||||||
webpushsettingssaved: 'Web push notification settings saved successfully!',
|
|
||||||
webpushsettingsfailed: 'Web push notification settings failed to save.',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const UserWebPushSettings = () => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const { addToast } = useToasts();
|
|
||||||
const router = useRouter();
|
|
||||||
const { user } = useUser({ id: Number(router.query.userId) });
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
mutate: revalidate,
|
|
||||||
} = useSWR<UserSettingsNotificationsResponse>(
|
|
||||||
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!data && !error) {
|
|
||||||
return <LoadingSpinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
types: data?.notificationTypes.webpush ?? ALL_NOTIFICATIONS,
|
|
||||||
}}
|
|
||||||
enableReinitialize
|
|
||||||
onSubmit={async (values) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`/api/v1/user/${user?.id}/settings/notifications`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
pgpKey: data?.pgpKey,
|
|
||||||
discordId: data?.discordId,
|
|
||||||
pushbulletAccessToken: data?.pushbulletAccessToken,
|
|
||||||
pushoverApplicationToken: data?.pushoverApplicationToken,
|
|
||||||
pushoverUserKey: data?.pushoverUserKey,
|
|
||||||
telegramChatId: data?.telegramChatId,
|
|
||||||
telegramSendSilently: data?.telegramSendSilently,
|
|
||||||
notificationTypes: {
|
|
||||||
webpush: values.types,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
mutate('/api/v1/settings/public');
|
|
||||||
addToast(intl.formatMessage(messages.webpushsettingssaved), {
|
|
||||||
appearance: 'success',
|
|
||||||
autoDismiss: true,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
addToast(intl.formatMessage(messages.webpushsettingsfailed), {
|
|
||||||
appearance: 'error',
|
|
||||||
autoDismiss: true,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
revalidate();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
errors,
|
|
||||||
touched,
|
|
||||||
isSubmitting,
|
|
||||||
isValid,
|
|
||||||
values,
|
|
||||||
setFieldValue,
|
|
||||||
setFieldTouched,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Form className="section">
|
|
||||||
<NotificationTypeSelector
|
|
||||||
user={user}
|
|
||||||
currentTypes={values.types}
|
|
||||||
onUpdate={(newTypes) => {
|
|
||||||
setFieldValue('types', newTypes);
|
|
||||||
setFieldTouched('types');
|
|
||||||
}}
|
|
||||||
error={
|
|
||||||
errors.types && touched.types
|
|
||||||
? (errors.types as string)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="actions">
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
|
||||||
<Button
|
|
||||||
buttonType="primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting || !isValid}
|
|
||||||
>
|
|
||||||
<ArrowDownOnSquareIcon />
|
|
||||||
<span>
|
|
||||||
{isSubmitting
|
|
||||||
? intl.formatMessage(globalMessages.saving)
|
|
||||||
: intl.formatMessage(globalMessages.save)}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserWebPushSettings;
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import ConfirmButton from '@app/components/Common/ConfirmButton';
|
||||||
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
|
import {
|
||||||
|
ComputerDesktopIcon,
|
||||||
|
DevicePhoneMobileIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from '@heroicons/react/24/solid';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
|
||||||
|
interface DeviceItemProps {
|
||||||
|
disablePushNotifications: (p256dh: string) => void;
|
||||||
|
device: {
|
||||||
|
endpoint: string;
|
||||||
|
p256dh: string;
|
||||||
|
auth: string;
|
||||||
|
userAgent: string;
|
||||||
|
createdAt: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages(
|
||||||
|
'components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush',
|
||||||
|
{
|
||||||
|
operatingsystem: 'Operating System',
|
||||||
|
browser: 'Browser',
|
||||||
|
engine: 'Engine',
|
||||||
|
deletesubscription: 'Delete Subscription',
|
||||||
|
unknown: 'Unknown',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex w-full flex-col justify-between overflow-hidden rounded-xl bg-gray-800 py-4 text-gray-400 shadow-md ring-1 ring-gray-700 xl:h-28 xl:flex-row">
|
||||||
|
<div className="relative flex w-full flex-col justify-between overflow-hidden sm:flex-row">
|
||||||
|
<div className="relative z-10 flex w-full items-center overflow-hidden pl-4 pr-4 sm:pr-0 xl:w-7/12 2xl:w-2/3">
|
||||||
|
<div className="relative h-auto w-12 flex-shrink-0 scale-100 transform-gpu overflow-hidden rounded-md transition duration-300 hover:scale-105">
|
||||||
|
{UAParser(device.userAgent).device.type === 'mobile' ? (
|
||||||
|
<DevicePhoneMobileIcon />
|
||||||
|
) : (
|
||||||
|
<ComputerDesktopIcon />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||||
|
<div className="pt-0.5 text-xs font-medium text-white sm:pt-1">
|
||||||
|
{device.createdAt
|
||||||
|
? intl.formatDate(device.createdAt, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
: 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div className="mr-2 min-w-0 truncate text-lg font-bold text-white hover:underline xl:text-xl">
|
||||||
|
{device.userAgent
|
||||||
|
? UAParser(device.userAgent).device.model
|
||||||
|
: intl.formatMessage(messages.unknown)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="z-10 mt-4 ml-4 flex w-full flex-col justify-center overflow-hidden pr-4 text-sm sm:ml-2 sm:mt-0 xl:flex-1 xl:pr-0">
|
||||||
|
<div className="card-field">
|
||||||
|
<span className="card-field-name">
|
||||||
|
{intl.formatMessage(messages.operatingsystem)}
|
||||||
|
</span>
|
||||||
|
<span className="flex truncate text-sm text-gray-300">
|
||||||
|
{device.userAgent ? UAParser(device.userAgent).os.name : 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-field">
|
||||||
|
<span className="card-field-name">
|
||||||
|
{intl.formatMessage(messages.browser)}
|
||||||
|
</span>
|
||||||
|
<span className="flex truncate text-sm text-gray-300">
|
||||||
|
{device.userAgent
|
||||||
|
? UAParser(device.userAgent).browser.name
|
||||||
|
: 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="card-field">
|
||||||
|
<span className="card-field-name">
|
||||||
|
{intl.formatMessage(messages.engine)}
|
||||||
|
</span>
|
||||||
|
<span className="flex truncate text-sm text-gray-300">
|
||||||
|
{device.userAgent
|
||||||
|
? UAParser(device.userAgent).engine.name
|
||||||
|
: 'N/A'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
||||||
|
<ConfirmButton
|
||||||
|
onClick={() => disablePushNotifications(device.p256dh)}
|
||||||
|
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
<span>{intl.formatMessage(messages.deletesubscription)}</span>
|
||||||
|
</ConfirmButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeviceItem;
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
import Alert from '@app/components/Common/Alert';
|
||||||
|
import Button from '@app/components/Common/Button';
|
||||||
|
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
||||||
|
import NotificationTypeSelector, {
|
||||||
|
ALL_NOTIFICATIONS,
|
||||||
|
} from '@app/components/NotificationTypeSelector';
|
||||||
|
import DeviceItem from '@app/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem';
|
||||||
|
import useSettings from '@app/hooks/useSettings';
|
||||||
|
import { useUser } from '@app/hooks/useUser';
|
||||||
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
|
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
||||||
|
import {
|
||||||
|
CloudArrowDownIcon,
|
||||||
|
CloudArrowUpIcon,
|
||||||
|
} from '@heroicons/react/24/solid';
|
||||||
|
import type { UserPushSubscription } from '@server/entity/UserPushSubscription';
|
||||||
|
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||||
|
import { Form, Formik } from 'formik';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
|
|
||||||
|
const messages = defineMessages(
|
||||||
|
'components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush',
|
||||||
|
{
|
||||||
|
webpushsettingssaved: 'Web push notification settings saved successfully!',
|
||||||
|
webpushsettingsfailed: 'Web push notification settings failed to save.',
|
||||||
|
enablewebpush: 'Enable web push',
|
||||||
|
disablewebpush: 'Disable web push',
|
||||||
|
managedevices: 'Manage Devices',
|
||||||
|
type: 'type',
|
||||||
|
created: 'Created',
|
||||||
|
device: 'Device',
|
||||||
|
subscriptiondeleted: 'Subscription deleted.',
|
||||||
|
subscriptiondeleteerror:
|
||||||
|
'Something went wrong while deleting the user subscription.',
|
||||||
|
nodevicestoshow: 'You have no web push subscriptions to show.',
|
||||||
|
webpushhasbeenenabled: 'Web push has been enabled.',
|
||||||
|
webpushhasbeendisabled: 'Web push has been disabled.',
|
||||||
|
enablingwebpusherror: 'Something went wrong while enabling web push.',
|
||||||
|
disablingwebpusherror: 'Something went wrong while disabling web push.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserWebPushSettings = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
const router = useRouter();
|
||||||
|
const { user } = useUser({ id: Number(router.query.userId) });
|
||||||
|
const { currentSettings } = useSettings();
|
||||||
|
const [webPushEnabled, setWebPushEnabled] = useState(false);
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
mutate: revalidate,
|
||||||
|
} = useSWR<UserSettingsNotificationsResponse>(
|
||||||
|
user ? `/api/v1/user/${user?.id}/settings/notifications` : null
|
||||||
|
);
|
||||||
|
const { data: dataDevices, mutate: revalidateDevices } = useSWR<
|
||||||
|
{
|
||||||
|
endpoint: string;
|
||||||
|
p256dh: string;
|
||||||
|
auth: string;
|
||||||
|
userAgent: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}[]
|
||||||
|
>(`/api/v1/user/${user?.id}/pushSubscriptions`, { revalidateOnMount: true });
|
||||||
|
|
||||||
|
// Subscribes to the push manager
|
||||||
|
// Will only add to the database if subscribing for the first time
|
||||||
|
const enablePushNotifications = () => {
|
||||||
|
if ('serviceWorker' in navigator && user?.id) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.getRegistration('/sw.js')
|
||||||
|
.then(async (registration) => {
|
||||||
|
if (currentSettings.enablePushRegistration) {
|
||||||
|
const sub = await registration?.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: currentSettings.vapidPublic,
|
||||||
|
});
|
||||||
|
const parsedSub = JSON.parse(JSON.stringify(sub));
|
||||||
|
|
||||||
|
if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
|
||||||
|
const res = await fetch('/api/v1/user/registerPushSubscription', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
endpoint: parsedSub.endpoint,
|
||||||
|
p256dh: parsedSub.keys.p256dh,
|
||||||
|
auth: parsedSub.keys.auth,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(res.statusText);
|
||||||
|
}
|
||||||
|
setWebPushEnabled(true);
|
||||||
|
addToast(intl.formatMessage(messages.webpushhasbeenenabled), {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
addToast(intl.formatMessage(messages.enablingwebpusherror), {
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'error',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
revalidateDevices();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unsubscribes from the push manager
|
||||||
|
// Deletes/disables corresponding push subscription from database
|
||||||
|
const disablePushNotifications = async (p256dh?: string) => {
|
||||||
|
if ('serviceWorker' in navigator && user?.id) {
|
||||||
|
navigator.serviceWorker.getRegistration('/sw.js').then((registration) => {
|
||||||
|
registration?.pushManager
|
||||||
|
.getSubscription()
|
||||||
|
.then(async (subscription) => {
|
||||||
|
const parsedSub = JSON.parse(JSON.stringify(subscription));
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/v1/user/${user?.id}/pushSubscription/${
|
||||||
|
p256dh ? p256dh : parsedSub.keys.p256dh
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(res.statusText);
|
||||||
|
}
|
||||||
|
if (subscription && (p256dh === parsedSub.keys.p256dh || !p256dh)) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
setWebPushEnabled(false);
|
||||||
|
}
|
||||||
|
addToast(
|
||||||
|
intl.formatMessage(
|
||||||
|
p256dh
|
||||||
|
? messages.subscriptiondeleted
|
||||||
|
: messages.webpushhasbeendisabled
|
||||||
|
),
|
||||||
|
{
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'success',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
addToast(
|
||||||
|
intl.formatMessage(
|
||||||
|
p256dh
|
||||||
|
? messages.subscriptiondeleteerror
|
||||||
|
: messages.disablingwebpusherror
|
||||||
|
),
|
||||||
|
{
|
||||||
|
autoDismiss: true,
|
||||||
|
appearance: 'error',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
revalidateDevices();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Checks our current subscription on page load
|
||||||
|
// Will set the web push state to true if subscribed
|
||||||
|
useEffect(() => {
|
||||||
|
if ('serviceWorker' in navigator && user?.id) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.getRegistration('/sw.js')
|
||||||
|
.then(async (registration) => {
|
||||||
|
await registration?.pushManager
|
||||||
|
.getSubscription()
|
||||||
|
.then(async (subscription) => {
|
||||||
|
if (subscription) {
|
||||||
|
const parsedKey = JSON.parse(JSON.stringify(subscription));
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/v1/user/${user.id}/pushSubscription/${parsedKey.keys.p256dh}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUserPushSub = {
|
||||||
|
data: (await response.json()) as UserPushSubscription,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentUserPushSub.data.p256dh !== parsedKey.keys.p256dh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setWebPushEnabled(true);
|
||||||
|
} else {
|
||||||
|
setWebPushEnabled(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
setWebPushEnabled(false);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
'[SW] Failure retrieving push manager subscription, error:',
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user?.id]);
|
||||||
|
|
||||||
|
if (!data && !error) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
types: data?.notificationTypes.webpush ?? ALL_NOTIFICATIONS,
|
||||||
|
}}
|
||||||
|
enableReinitialize
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/v1/user/${user?.id}/settings/notifications`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
pgpKey: data?.pgpKey,
|
||||||
|
discordId: data?.discordId,
|
||||||
|
pushbulletAccessToken: data?.pushbulletAccessToken,
|
||||||
|
pushoverApplicationToken: data?.pushoverApplicationToken,
|
||||||
|
pushoverUserKey: data?.pushoverUserKey,
|
||||||
|
telegramChatId: data?.telegramChatId,
|
||||||
|
telegramSendSilently: data?.telegramSendSilently,
|
||||||
|
notificationTypes: {
|
||||||
|
webpush: values.types,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(res.statusText);
|
||||||
|
}
|
||||||
|
mutate('/api/v1/settings/public');
|
||||||
|
addToast(intl.formatMessage(messages.webpushsettingssaved), {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(messages.webpushsettingsfailed), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
isSubmitting,
|
||||||
|
isValid,
|
||||||
|
values,
|
||||||
|
setFieldValue,
|
||||||
|
setFieldTouched,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Form className="section">
|
||||||
|
<NotificationTypeSelector
|
||||||
|
user={user}
|
||||||
|
currentTypes={values.types}
|
||||||
|
onUpdate={(newTypes) => {
|
||||||
|
setFieldValue('types', newTypes);
|
||||||
|
setFieldTouched('types');
|
||||||
|
}}
|
||||||
|
error={
|
||||||
|
errors.types && touched.types
|
||||||
|
? (errors.types as string)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="actions">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType={`${webPushEnabled ? 'danger' : 'primary'}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
webPushEnabled
|
||||||
|
? disablePushNotifications()
|
||||||
|
: enablePushNotifications()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{webPushEnabled ? (
|
||||||
|
<CloudArrowDownIcon />
|
||||||
|
) : (
|
||||||
|
<CloudArrowUpIcon />
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{webPushEnabled
|
||||||
|
? intl.formatMessage(messages.disablewebpush)
|
||||||
|
: intl.formatMessage(messages.enablewebpush)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
|
<ArrowDownOnSquareIcon />
|
||||||
|
<span>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
<div className="mt-10 mb-6">
|
||||||
|
<h3 className="heading">
|
||||||
|
{intl.formatMessage(messages.managedevices)}
|
||||||
|
</h3>
|
||||||
|
<div className="section">
|
||||||
|
{dataDevices?.length ? (
|
||||||
|
dataDevices
|
||||||
|
?.sort((a, b) => {
|
||||||
|
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||||
|
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||||
|
return dateB - dateA;
|
||||||
|
})
|
||||||
|
.map((device, index) => (
|
||||||
|
<div className="py-2" key={`device-list-${index}`}>
|
||||||
|
<DeviceItem
|
||||||
|
key={index}
|
||||||
|
disablePushNotifications={disablePushNotifications}
|
||||||
|
device={device}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
title={intl.formatMessage(messages.nodevicestoshow)}
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserWebPushSettings;
|
||||||
@@ -1339,6 +1339,26 @@
|
|||||||
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "You do not have permission to modify this user's linked accounts.",
|
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "You do not have permission to modify this user's linked accounts.",
|
||||||
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "This account is already linked to a Plex user",
|
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "This account is already linked to a Plex user",
|
||||||
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "Unable to connect to Plex using your credentials",
|
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "Unable to connect to Plex using your credentials",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "Browser",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "Created",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "Delete Subscription",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "Device",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablewebpush": "Disable web push",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.disablingwebpusherror": "Something went wrong while disabling web push.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablewebpush": "Enable web push",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.enablingwebpusherror": "Something went wrong while enabling web push.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "Engine",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "Manage Devices",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.nodevicestoshow": "You have no web push subscriptions to show.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.operatingsystem": "Operating System",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "Subscription deleted.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleteerror": "Something went wrong while deleting the user subscription.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "type",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "Unknown",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeendisabled": "Web push has been disabled.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushhasbeenenabled": "Web push has been enabled.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "Web push notification settings failed to save.",
|
||||||
|
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "Web push notification settings saved successfully!",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default",
|
"components.UserProfile.UserSettings.UserNotificationSettings.deviceDefault": "Device Default",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID",
|
"components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your user account",
|
"components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The <FindDiscordIdLink>multi-digit ID number</FindDiscordIdLink> associated with your user account",
|
||||||
@@ -1378,8 +1398,6 @@
|
|||||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid chat ID",
|
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid chat ID",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "The thread/topic ID must be a positive whole number",
|
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramMessageThreadId": "The thread/topic ID must be a positive whole number",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push",
|
"components.UserProfile.UserSettings.UserNotificationSettings.webpush": "Web Push",
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingsfailed": "Web push notification settings failed to save.",
|
|
||||||
"components.UserProfile.UserSettings.UserNotificationSettings.webpushsettingssaved": "Web push notification settings saved successfully!",
|
|
||||||
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password",
|
"components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password",
|
||||||
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password",
|
"components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password",
|
||||||
"components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password",
|
"components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password",
|
||||||
|
|||||||
@@ -242,7 +242,9 @@ CoreApp.getInitialProps = async (initialProps) => {
|
|||||||
if (ctx.res) {
|
if (ctx.res) {
|
||||||
// Check if app is initialized and redirect if necessary
|
// Check if app is initialized and redirect if necessary
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`http://localhost:${process.env.PORT || 5055}/api/v1/settings/public`
|
`http://${process.env.HOST || 'localhost'}:${
|
||||||
|
process.env.PORT || 5055
|
||||||
|
}/api/v1/settings/public`
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
currentSettings = await res.json();
|
currentSettings = await res.json();
|
||||||
@@ -260,7 +262,9 @@ CoreApp.getInitialProps = async (initialProps) => {
|
|||||||
try {
|
try {
|
||||||
// Attempt to get the user by running a request to the local api
|
// Attempt to get the user by running a request to the local api
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`,
|
`http://${process.env.HOST || 'localhost'}:${
|
||||||
|
process.env.PORT || 5055
|
||||||
|
}/api/v1/auth/me`,
|
||||||
{
|
{
|
||||||
headers:
|
headers:
|
||||||
ctx.req && ctx.req.headers.cookie
|
ctx.req && ctx.req.headers.cookie
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export const getServerSideProps: GetServerSideProps<
|
|||||||
CollectionPageProps
|
CollectionPageProps
|
||||||
> = async (ctx) => {
|
> = async (ctx) => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
|
`http://${process.env.HOST || 'localhost'}:${
|
||||||
ctx.query.collectionId
|
process.env.PORT || 5055
|
||||||
}`,
|
}/api/v1/collection/${ctx.query.collectionId}`,
|
||||||
{
|
{
|
||||||
headers: ctx.req?.headers?.cookie
|
headers: ctx.req?.headers?.cookie
|
||||||
? { cookie: ctx.req.headers.cookie }
|
? { cookie: ctx.req.headers.cookie }
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export const getServerSideProps: GetServerSideProps<MoviePageProps> = async (
|
|||||||
ctx
|
ctx
|
||||||
) => {
|
) => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
|
`http://${process.env.HOST || 'localhost'}:${
|
||||||
ctx.query.movieId
|
process.env.PORT || 5055
|
||||||
}`,
|
}/api/v1/movie/${ctx.query.movieId}`,
|
||||||
{
|
{
|
||||||
headers: ctx.req?.headers?.cookie
|
headers: ctx.req?.headers?.cookie
|
||||||
? { cookie: ctx.req.headers.cookie }
|
? { cookie: ctx.req.headers.cookie }
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ export const getServerSideProps: GetServerSideProps<TvPageProps> = async (
|
|||||||
ctx
|
ctx
|
||||||
) => {
|
) => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`,
|
`http://${process.env.HOST || 'localhost'}:${
|
||||||
|
process.env.PORT || 5055
|
||||||
|
}/api/v1/tv/${ctx.query.tvId}`,
|
||||||
{
|
{
|
||||||
headers: ctx.req?.headers?.cookie
|
headers: ctx.req?.headers?.cookie
|
||||||
? { cookie: ctx.req.headers.cookie }
|
? { cookie: ctx.req.headers.cookie }
|
||||||
|
|||||||
Reference in New Issue
Block a user