Compare commits

..

1 Commits

Author SHA1 Message Date
jesterr0
54e3df2494 feat: now uses markdown linebreaks instead of relying purely on newlines
refactor: displayUrl now applies an ellipsis if the url is longer than 40 characters
2025-03-22 14:36:12 -04:00
42 changed files with 368 additions and 1464 deletions

View File

@@ -249,8 +249,7 @@
"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"
] ]
}, },
{ {
@@ -712,105 +711,6 @@
"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"
]
} }
] ]
} }

View File

@@ -98,8 +98,6 @@ 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

View File

@@ -33,7 +33,5 @@ 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 }}

View File

@@ -65,18 +65,18 @@ All help is welcome and greatly appreciated! If you would like to contribute to
git push origin BRANCH_NAME -f git push origin BRANCH_NAME -f
``` ```
### Helm Chart ### Helm Chart
Tools Required: Tools Required:
- [Helm](https://helm.sh/docs/intro/install/) - [Helm](https://helm.sh/docs/intro/install/)
- [helm-docs](https://github.com/norwoodj/helm-docs) - [helm-docs](https://github.com/norwoodj/helm-docs)
Steps: Steps:
1. Make the necessary changes. 1. Make the necessary changes.
2. Test your changes. 2. Test your changes.
3. Update the `version` in `charts/jellyseerr-chart/Chart.yaml` following [Semantic Versioning (SemVer)](https://semver.org/). 3. Update the `version` in `charts/jellyseerr-chart/Chart.yaml` following [Semantic Versioning (SemVer)](https://semver.org/).
4. Run the `helm-docs` command to regenerate the chart's README. 4. Run the `helm-docs` command to regenerate the chart's README.
### Contributing Code ### Contributing Code

View File

@@ -38,17 +38,8 @@ RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
FROM node:22-alpine FROM node:22-alpine
# OCI Meta information # Metadata for Github Package Registry
ARG BUILD_DATE LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr"
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

View File

@@ -335,8 +335,6 @@ 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>

View File

@@ -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 \
ghcr.io/fallenbagel/jellyseerr 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 ghcr.io/fallenbagel/jellyseerr docker pull 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: ghcr.io/fallenbagel/jellyseerr:latest image: 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 ghcr.io/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 fallenbagel/jellyseerr:latest
``` ```
#### Updating: #### Updating:

View File

@@ -3965,8 +3965,6 @@ paths:
type: string type: string
p256dh: p256dh:
type: string type: string
userAgent:
type: string
required: required:
- endpoint - endpoint
- auth - auth
@@ -3974,88 +3972,6 @@ 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

View File

@@ -44,7 +44,6 @@
"@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",
@@ -65,7 +64,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.25", "next": "^14.2.24",
"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",
@@ -100,7 +99,6 @@
"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
View File

@@ -41,9 +41,6 @@ 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
@@ -108,8 +105,8 @@ importers:
specifier: '3' specifier: '3'
version: 3.0.0 version: 3.0.0
next: next:
specifier: ^14.2.25 specifier: ^14.2.24
version: 14.2.25(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 14.2.24(@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
@@ -209,9 +206,6 @@ 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
@@ -2139,62 +2133,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.25': '@next/env@14.2.24':
resolution: {integrity: sha512-JnzQ2cExDeG7FxJwqAksZ3aqVJrHjFwZQAEJ9gQZSoEhIow7SNoKZzju/AwQ+PLIR4NY8V0rhcVozx/2izDO0w==} resolution: {integrity: sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==}
'@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.25': '@next/swc-darwin-arm64@14.2.24':
resolution: {integrity: sha512-09clWInF1YRd6le00vt750s3m7SEYNehz9C4PUcSu3bAdCTpjIV4aTYQZ25Ehrr83VR1rZeqtKUPWSI7GfuKZQ==} resolution: {integrity: sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@next/swc-darwin-x64@14.2.25': '@next/swc-darwin-x64@14.2.24':
resolution: {integrity: sha512-V+iYM/QR+aYeJl3/FWWU/7Ix4b07ovsQ5IbkwgUK29pTHmq+5UxeDr7/dphvtXEq5pLB/PucfcBNh9KZ8vWbug==} resolution: {integrity: sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@next/swc-linux-arm64-gnu@14.2.25': '@next/swc-linux-arm64-gnu@14.2.24':
resolution: {integrity: sha512-LFnV2899PJZAIEHQ4IMmZIgL0FBieh5keMnriMY1cK7ompR+JUd24xeTtKkcaw8QmxmEdhoE5Mu9dPSuDBgtTg==} resolution: {integrity: sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-arm64-musl@14.2.25': '@next/swc-linux-arm64-musl@14.2.24':
resolution: {integrity: sha512-QC5y5PPTmtqFExcKWKYgUNkHeHE/z3lUsu83di488nyP0ZzQ3Yse2G6TCxz6nNsQwgAx1BehAJTZez+UQxzLfw==} resolution: {integrity: sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@next/swc-linux-x64-gnu@14.2.25': '@next/swc-linux-x64-gnu@14.2.24':
resolution: {integrity: sha512-y6/ML4b9eQ2D/56wqatTJN5/JR8/xdObU2Fb1RBidnrr450HLCKr6IJZbPqbv7NXmje61UyxjF5kvSajvjye5w==} resolution: {integrity: sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-linux-x64-musl@14.2.25': '@next/swc-linux-x64-musl@14.2.24':
resolution: {integrity: sha512-sPX0TSXHGUOZFvv96GoBXpB3w4emMqKeMgemrSxI7A6l55VBJp/RKYLwZIB9JxSqYPApqiREaIIap+wWq0RU8w==} resolution: {integrity: sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@next/swc-win32-arm64-msvc@14.2.25': '@next/swc-win32-arm64-msvc@14.2.24':
resolution: {integrity: sha512-ReO9S5hkA1DU2cFCsGoOEp7WJkhFzNbU/3VUF6XxNGUCQChyug6hZdYL/istQgfT/GWE6PNIg9cm784OI4ddxQ==} resolution: {integrity: sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@next/swc-win32-ia32-msvc@14.2.25': '@next/swc-win32-ia32-msvc@14.2.24':
resolution: {integrity: sha512-DZ/gc0o9neuCDyD5IumyTGHVun2dCox5TfPQI/BJTYwpSNYM3CZDI4i6TOdjeq1JMo+Ug4kPSMuZdwsycwFbAw==} resolution: {integrity: sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@next/swc-win32-x64-msvc@14.2.25': '@next/swc-win32-x64-msvc@14.2.24':
resolution: {integrity: sha512-KSznmS6eFjQ9RJ1nEc66kJvtGIL1iZMYmGEXsZPh2YtnLtqrgdVvKXJY2ScjjoFnG6nGLyPFR0UiEvDwVah4Tw==} resolution: {integrity: sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -3418,9 +3412,6 @@ 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==}
@@ -7026,8 +7017,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.25: next@14.2.24:
resolution: {integrity: sha512-N5M7xMc4wSb4IkPvEV5X2BRRXUmhVHNyaXwEM86+voXthSZz8ZiRyQW4p9mwAoAPIm6OzuVZtn7idgEJeAJN3Q==} resolution: {integrity: sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==}
engines: {node: '>=18.17.0'} engines: {node: '>=18.17.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -9232,10 +9223,6 @@ 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==}
@@ -11873,37 +11860,37 @@ snapshots:
dependencies: dependencies:
make-plural: 7.4.0 make-plural: 7.4.0
'@next/env@14.2.25': {} '@next/env@14.2.24': {}
'@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.25': '@next/swc-darwin-arm64@14.2.24':
optional: true optional: true
'@next/swc-darwin-x64@14.2.25': '@next/swc-darwin-x64@14.2.24':
optional: true optional: true
'@next/swc-linux-arm64-gnu@14.2.25': '@next/swc-linux-arm64-gnu@14.2.24':
optional: true optional: true
'@next/swc-linux-arm64-musl@14.2.25': '@next/swc-linux-arm64-musl@14.2.24':
optional: true optional: true
'@next/swc-linux-x64-gnu@14.2.25': '@next/swc-linux-x64-gnu@14.2.24':
optional: true optional: true
'@next/swc-linux-x64-musl@14.2.25': '@next/swc-linux-x64-musl@14.2.24':
optional: true optional: true
'@next/swc-win32-arm64-msvc@14.2.25': '@next/swc-win32-arm64-msvc@14.2.24':
optional: true optional: true
'@next/swc-win32-ia32-msvc@14.2.25': '@next/swc-win32-ia32-msvc@14.2.24':
optional: true optional: true
'@next/swc-win32-x64-msvc@14.2.25': '@next/swc-win32-x64-msvc@14.2.24':
optional: true optional: true
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
@@ -13519,7 +13506,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.8.1 tslib: 2.6.3
'@swc/types@0.1.17': '@swc/types@0.1.17':
dependencies: dependencies:
@@ -13791,8 +13778,6 @@ 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':
@@ -18304,27 +18289,27 @@ snapshots:
nerf-dart@1.0.0: {} nerf-dart@1.0.0: {}
next@14.2.25(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): next@14.2.24(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@next/env': 14.2.25 '@next/env': 14.2.24
'@swc/helpers': 0.5.5 '@swc/helpers': 0.5.5
busboy: 1.6.0 busboy: 1.6.0
caniuse-lite: 1.0.30001700 caniuse-lite: 1.0.30001636
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.25 '@next/swc-darwin-arm64': 14.2.24
'@next/swc-darwin-x64': 14.2.25 '@next/swc-darwin-x64': 14.2.24
'@next/swc-linux-arm64-gnu': 14.2.25 '@next/swc-linux-arm64-gnu': 14.2.24
'@next/swc-linux-arm64-musl': 14.2.25 '@next/swc-linux-arm64-musl': 14.2.24
'@next/swc-linux-x64-gnu': 14.2.25 '@next/swc-linux-x64-gnu': 14.2.24
'@next/swc-linux-x64-musl': 14.2.25 '@next/swc-linux-x64-musl': 14.2.24
'@next/swc-win32-arm64-msvc': 14.2.25 '@next/swc-win32-arm64-msvc': 14.2.24
'@next/swc-win32-ia32-msvc': 14.2.25 '@next/swc-win32-ia32-msvc': 14.2.24
'@next/swc-win32-x64-msvc': 14.2.25 '@next/swc-win32-x64-msvc': 14.2.24
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
@@ -20687,8 +20672,6 @@ 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

View File

@@ -289,7 +289,7 @@ class ExternalAPI {
return data; return data;
} }
protected removeCache(endpoint: string, options?: Record<string, unknown>) { protected removeCache(endpoint: string, options?: Record<string, string>) {
const cacheKey = this.serializeCacheKey(endpoint, { const cacheKey = this.serializeCacheKey(endpoint, {
...this.params, ...this.params,
...options, ...options,

View File

@@ -110,18 +110,11 @@ 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="${safeDeviceId}", Version="${getAppVersion()}", Token="${authToken}"`; authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}", Token="${authToken}"`;
} else { } else {
authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${safeDeviceId}", Version="${getAppVersion()}"`; authHeaderVal = `MediaBrowser Client="Jellyseerr", Device="Jellyseerr", DeviceId="${deviceId}", Version="${getAppVersion()}"`;
} }
super( super(

View File

@@ -28,7 +28,6 @@ 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 }> {
@@ -105,7 +104,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: Array.from(new Set([...movie.tags, ...options.tags])), tags: options.tags,
rootFolderPath: options.rootFolderPath, rootFolderPath: options.rootFolderPath,
monitored: options.monitored, monitored: options.monitored,
addOptions: { addOptions: {
@@ -242,13 +241,10 @@ 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,
});
} }
}; };
} }

View File

@@ -184,9 +184,7 @@ 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 = options.tags ?? series.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>(
@@ -368,18 +366,14 @@ 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,
}); });
} }
}; };

View File

@@ -98,12 +98,6 @@ 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;

View File

@@ -1,10 +1,4 @@
import { import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './User'; import { User } from './User';
@Entity() @Entity()
@@ -24,15 +18,9 @@ export class UserPushSubscription {
@Column() @Column()
public p256dh: string; public p256dh: string;
@Column() @Column({ unique: true })
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);
} }

View File

@@ -3,10 +3,7 @@ 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 = {

View File

@@ -193,34 +193,14 @@ 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.debug(`Cleared ${files[0]} from cache 'avatar'`, { logger.info(`Cleared ${files[0]} from cache 'avatar'`, {
label: 'Image Cache', label: 'Image Cache',
}); });
} catch (e) { } catch (e) {

View File

@@ -47,10 +47,11 @@ class GotifyAgent
const title = payload.event const title = payload.event
? `${payload.event} - ${payload.subject}` ? `${payload.event} - ${payload.subject}`
: payload.subject; : payload.subject;
let message = payload.message ?? '';
let message = payload.message ? `${payload.message} \n\n` : '';
if (payload.request) { if (payload.request) {
message += `\n\nRequested By: ${payload.request.requestedBy.displayName}`; message += `\n**Requested By:** ${payload.request.requestedBy.displayName} `;
let status = ''; let status = '';
switch (type) { switch (type) {
@@ -73,16 +74,18 @@ class GotifyAgent
} }
if (status) { if (status) {
message += `\nRequest Status: ${status}`; message += `\n**Request Status:** ${status} `;
} }
} else if (payload.comment) { } else if (payload.comment) {
message += `\nComment from ${payload.comment.user.displayName}:\n${payload.comment.message}`; message += `\nComment from ${payload.comment.user.displayName}:\n${payload.comment.message} `;
} else if (payload.issue) { } else if (payload.issue) {
message += `\n\nReported By: ${payload.issue.createdBy.displayName}`; message += `\n\n**Reported By:** ${payload.issue.createdBy.displayName} `;
message += `\nIssue Type: ${IssueTypeName[payload.issue.issueType]}`; message += `\n**Issue Type:** ${
message += `\nIssue Status: ${ IssueTypeName[payload.issue.issueType]
} `;
message += `\n**Issue Status:** ${
payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved' payload.issue.status === IssueStatus.OPEN ? 'Open' : 'Resolved'
}`; } `;
if (type == Notification.ISSUE_CREATED) { if (type == Notification.ISSUE_CREATED) {
priority = 1; priority = 1;
@@ -90,12 +93,14 @@ class GotifyAgent
} }
for (const extra of payload.extra ?? []) { for (const extra of payload.extra ?? []) {
message += `\n\n**${extra.name}**\n${extra.value}`; message += `\n\n**${extra.name}**\n${extra.value} `;
} }
if (applicationUrl && payload.media) { if (applicationUrl && payload.media) {
const actionUrl = `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`; const actionUrl = `${applicationUrl}/${payload.media.mediaType}/${payload.media.tmdbId}`;
message += `\n\nOpen in ${applicationTitle}(${actionUrl})`; const displayUrl =
actionUrl.length > 40 ? `${actionUrl.slice(0, 41)}...` : actionUrl;
message += `\n\n**Open in ${applicationTitle}:** [${displayUrl}](${actionUrl}) `;
} }
return { return {

View File

@@ -1,29 +0,0 @@
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"`
);
}
}

View File

@@ -1,21 +0,0 @@
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"`);
}
}

View File

@@ -1,203 +0,0 @@
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"`);
}
}

View File

@@ -1,69 +0,0 @@
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"`);
}
}

View File

@@ -10,7 +10,6 @@ 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';
@@ -217,10 +216,6 @@ 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);
@@ -348,12 +343,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 {
@@ -380,7 +375,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 = getUserAvatarUrl(user); user.avatar = `/avatarproxy/${account.User.Id}`;
user.userType = user.userType =
body.serverType === MediaServerType.JELLYFIN body.serverType === MediaServerType.JELLYFIN
? UserType.JELLYFIN ? UserType.JELLYFIN
@@ -427,7 +422,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinUsername: account.User.Name, jellyfinUsername: account.User.Name,
} }
); );
user.avatar = getUserAvatarUrl(user); user.avatar = `/avatarproxy/${account.User.Id}`;
user.jellyfinUsername = account.User.Name; user.jellyfinUsername = account.User.Name;
if (user.username === account.User.Name) { if (user.username === account.User.Name) {
@@ -465,12 +460,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;
@@ -480,26 +475,6 @@ 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;

View File

@@ -8,12 +8,10 @@ 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);
@@ -33,79 +31,6 @@ 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}$/)) {
@@ -121,10 +46,6 @@ 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 },
}); });
@@ -134,7 +55,13 @@ router.get('/:jellyfinUserId', async (req, res) => {
size: 200, size: 200,
}); });
const jellyfinAvatarUrl = getJellyfinAvatarUrl(req.params.jellyfinUserId); const setttings = getSettings();
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,
@@ -146,15 +73,10 @@ 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',
}); });

View File

@@ -237,6 +237,19 @@ 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
@@ -251,6 +264,13 @@ 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);
} }

View File

@@ -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
let mappedRequests = requests.map((r) => { const requestsWithProfileNames = requests.map((r) => {
switch (r.type) { switch (r.type) {
case MediaType.MOVIE: { case MediaType.MOVIE: {
const profileName = radarrServers const profileName = radarrServers
@@ -212,36 +212,6 @@ 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),
@@ -249,7 +219,7 @@ requestRoutes.get<Record<string, unknown>, RequestResultsResponse>(
results: requestCount, results: requestCount,
page: Math.ceil(skip / pageSize) + 1, page: Math.ceil(skip / pageSize) + 1,
}, },
results: mappedRequests, results: requestsWithProfileNames,
}); });
} catch (e) { } catch (e) {
next({ status: 500, message: e.message }); next({ status: 500, message: e.message });

View File

@@ -184,15 +184,13 @@ 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({
relations: { user: true }, where: { auth: req.body.auth },
where: { auth: req.body.auth, user: { id: req.user?.id } },
}); });
if (existingSubs.length > 0) { if (existingSubs.length > 0) {
@@ -207,7 +205,6 @@ 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,
}); });
@@ -222,79 +219,6 @@ 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);

View File

@@ -255,9 +255,7 @@ 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 ${ } flex h-4 w-4 items-center justify-center !px-[9px] !py-[9px] text-[9px]`}
pendingRequestsCount > 99 ? 'w-6' : 'w-4'
} h-4 items-center justify-center !px-[5px] !py-[7px] text-[8px]`}
> >
{pendingRequestsCount > 99 {pendingRequestsCount > 99
? '99+' ? '99+'

View File

@@ -17,10 +17,10 @@ import {
TrashIcon, TrashIcon,
XMarkIcon, XMarkIcon,
} from '@heroicons/react/24/solid'; } from '@heroicons/react/24/solid';
import { MediaRequestStatus } from '@server/constants/media'; import { MediaRequestStatus, MediaType } 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 { RequestResultsResponse } from '@server/interfaces/api/requestInterfaces'; import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
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,11 +292,18 @@ const RequestItemError = ({
}; };
interface RequestItemProps { interface RequestItemProps {
request: RequestResultsResponse['results'][number]; request: NonFunctionProperties<MediaRequest> & { profileName?: string };
revalidateList: () => void; revalidateList: () => void;
radarrData?: RadarrSettings[];
sonarrData?: SonarrSettings[];
} }
const RequestItem = ({ request, revalidateList }: RequestItemProps) => { const RequestItem = ({
request,
revalidateList,
radarrData,
sonarrData,
}: RequestItemProps) => {
const settings = useSettings(); const settings = useSettings();
const { ref, inView } = useInView({ const { ref, inView } = useInView({
triggerOnce: true, triggerOnce: true,
@@ -391,6 +398,23 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
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
@@ -698,30 +722,30 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
)} )}
{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)} className="w-full"
className="w-full" >
> <TrashIcon />
<TrashIcon /> <span>{intl.formatMessage(messages.deleterequest)}</span>
<span>{intl.formatMessage(messages.deleterequest)}</span> </ConfirmButton>
</ConfirmButton> )}
{request.canRemove && ( {hasPermission(Permission.MANAGE_REQUESTS) &&
<ConfirmButton title?.mediaInfo?.serviceId &&
onClick={() => deleteMediaFile()} serviceExists() && (
confirmText={intl.formatMessage(globalMessages.areyousure)} <ConfirmButton
className="w-full" onClick={() => deleteMediaFile()}
> confirmText={intl.formatMessage(globalMessages.areyousure)}
<TrashIcon /> className="w-full"
<span> >
{intl.formatMessage(messages.removearr, { <TrashIcon />
arr: request.type === 'movie' ? 'Radarr' : 'Sonarr', <span>
})} {intl.formatMessage(messages.removearr, {
</span> arr: request.type === 'movie' ? 'Radarr' : 'Sonarr',
</ConfirmButton> })}
)} </span>
</> </ConfirmButton>
)} )}
{requestData.status === MediaRequestStatus.PENDING && {requestData.status === MediaRequestStatus.PENDING &&
hasPermission(Permission.MANAGE_REQUESTS) && ( hasPermission(Permission.MANAGE_REQUESTS) && (

View File

@@ -17,6 +17,8 @@ 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';
@@ -51,7 +53,7 @@ const RequestList = () => {
const { user } = useUser({ const { user } = useUser({
id: Number(router.query.userId), id: Number(router.query.userId),
}); });
const { user: currentUser } = useUser(); const { user: currentUser, hasPermission } = 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] =
@@ -62,6 +64,13 @@ 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,
@@ -245,6 +254,8 @@ const RequestList = () => {
<RequestItem <RequestItem
request={request} request={request}
revalidateList={() => revalidate()} revalidateList={() => revalidate()}
radarrData={radarrData}
sonarrData={sonarrData}
/> />
</div> </div>
); );

View File

@@ -1,9 +1,10 @@
/* 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) {
@@ -14,12 +15,40 @@ 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;
}; };

View File

@@ -141,7 +141,7 @@ const OverrideRuleTiles = ({
} }
setUsers(users); setUsers(users);
})(); })();
}, [rules, users]); }, [rules]);
return ( return (
<> <>

View File

@@ -57,9 +57,9 @@ const PlexImportModal = ({ onCancel, onComplete }: PlexImportProps) => {
}), }),
}); });
if (!res.ok) throw new Error(); if (!res.ok) throw new Error();
const createdUsers = await res.json(); const { data: createdUsers } = await res.json();
if (!Array.isArray(createdUsers) || createdUsers.length === 0) { if (!createdUsers.length) {
throw new Error('No users were imported from Plex.'); throw new Error('No users were imported from Plex.');
} }

View File

@@ -0,0 +1,136 @@
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;

View File

@@ -1,110 +0,0 @@
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;

View File

@@ -1,378 +0,0 @@
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;

View File

@@ -1339,26 +1339,6 @@
"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",
@@ -1398,6 +1378,8 @@
"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",

View File

@@ -242,9 +242,7 @@ 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://${process.env.HOST || 'localhost'}:${ `http://localhost:${process.env.PORT || 5055}/api/v1/settings/public`
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();
@@ -262,9 +260,7 @@ 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://${process.env.HOST || 'localhost'}:${ `http://localhost:${process.env.PORT || 5055}/api/v1/auth/me`,
process.env.PORT || 5055
}/api/v1/auth/me`,
{ {
headers: headers:
ctx.req && ctx.req.headers.cookie ctx.req && ctx.req.headers.cookie

View File

@@ -14,9 +14,9 @@ export const getServerSideProps: GetServerSideProps<
CollectionPageProps CollectionPageProps
> = async (ctx) => { > = async (ctx) => {
const res = await fetch( const res = await fetch(
`http://${process.env.HOST || 'localhost'}:${ `http://localhost:${process.env.PORT || 5055}/api/v1/collection/${
process.env.PORT || 5055 ctx.query.collectionId
}/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 }

View File

@@ -14,9 +14,9 @@ export const getServerSideProps: GetServerSideProps<MoviePageProps> = async (
ctx ctx
) => { ) => {
const res = await fetch( const res = await fetch(
`http://${process.env.HOST || 'localhost'}:${ `http://localhost:${process.env.PORT || 5055}/api/v1/movie/${
process.env.PORT || 5055 ctx.query.movieId
}/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 }

View File

@@ -14,9 +14,7 @@ export const getServerSideProps: GetServerSideProps<TvPageProps> = async (
ctx ctx
) => { ) => {
const res = await fetch( const res = await fetch(
`http://${process.env.HOST || 'localhost'}:${ `http://localhost:${process.env.PORT || 5055}/api/v1/tv/${ctx.query.tvId}`,
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 }