mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 10:49:30 -05:00
Compare commits
119 Commits
preview-ov
...
preview-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75afa742ea | ||
|
|
525a538f34 | ||
|
|
0d2273ff6e | ||
|
|
e035cd84ae | ||
|
|
438ccfe9c3 | ||
|
|
c181cee328 | ||
|
|
98a5b05816 | ||
|
|
b29959b063 | ||
|
|
9a2c12e51c | ||
|
|
620135aeac | ||
|
|
2dbd1096d2 | ||
|
|
24d3f523fc | ||
|
|
2b7974fa06 | ||
|
|
907ba6fdea | ||
|
|
efaad21554 | ||
|
|
6ab463285d | ||
|
|
418f0c2eb8 | ||
|
|
002557d2d0 | ||
|
|
62c1a70b37 | ||
|
|
1b325e7c32 | ||
|
|
f247642b76 | ||
|
|
396cd968ef | ||
|
|
ca739315b2 | ||
|
|
9143a6c027 | ||
|
|
d7fc03650f | ||
|
|
80fc5c1a78 | ||
|
|
95737d36e6 | ||
|
|
0fd6ca85a4 | ||
|
|
7cee9b475d | ||
|
|
ff9af866f8 | ||
|
|
5ffe6419ee | ||
|
|
8afcf5a8d8 | ||
|
|
17d93a8cb9 | ||
|
|
549082c53e | ||
|
|
fbef7e2c72 | ||
|
|
93d2e26ae9 | ||
|
|
f09a432635 | ||
|
|
a8f84d4f74 | ||
|
|
88e96fa163 | ||
|
|
2d814c1416 | ||
|
|
2f4b848b2c | ||
|
|
0ee3e69a61 | ||
|
|
7fcc0eb66d | ||
|
|
5d9f613dd8 | ||
|
|
ae1ee777fa | ||
|
|
b9dc9bceb5 | ||
|
|
1ffcea2065 | ||
|
|
ce1b39f73b | ||
|
|
71143ca76b | ||
|
|
bebad2d814 | ||
|
|
1c6f5362d7 | ||
|
|
4f6192476b | ||
|
|
e97b3dfa9e | ||
|
|
221397db71 | ||
|
|
9ecbd98230 | ||
|
|
3cc34b0db6 | ||
|
|
5a323324f8 | ||
|
|
99e2e17b8a | ||
|
|
0f14cd9247 | ||
|
|
50db3ea27b | ||
|
|
131a5a2b0b | ||
|
|
7d08f58c76 | ||
|
|
425f0c854e | ||
|
|
b8dbfaaed0 | ||
|
|
24c6208a3c | ||
|
|
d71ee58302 | ||
|
|
1755877e66 | ||
|
|
f84d752bca | ||
|
|
0b331ca579 | ||
|
|
656cd91c9c | ||
|
|
81d7473c05 | ||
|
|
f718cec23f | ||
|
|
ac908026db | ||
|
|
d67ec571c5 | ||
|
|
f3ebf6028b | ||
|
|
465d42dd60 | ||
|
|
2f0e493257 | ||
|
|
ebe7d11a53 | ||
|
|
7e94ad7210 | ||
|
|
814a7357c0 | ||
|
|
f8a8ebdf76 | ||
|
|
8da4870997 | ||
|
|
c98becf936 | ||
|
|
9739e18949 | ||
|
|
5fc4ae57c0 | ||
|
|
b6e2e6ce61 | ||
|
|
66948b420f | ||
|
|
9a595296db | ||
|
|
8da02d01b2 | ||
|
|
8a097d5195 | ||
|
|
5345207940 | ||
|
|
f5055035b6 | ||
|
|
59c22ccc08 | ||
|
|
13d15d1dcf | ||
|
|
59b7859f7f | ||
|
|
0491a04ef1 | ||
|
|
d76d794411 | ||
|
|
1da2f258a7 | ||
|
|
347a24a97b | ||
|
|
66a5ab41ab | ||
|
|
7c734bc873 | ||
|
|
de6e591bae | ||
|
|
7daea46eaa | ||
|
|
fa443c05be | ||
|
|
b01f98f7e2 | ||
|
|
39dbb7f7e5 | ||
|
|
e96159d3a5 | ||
|
|
59a713d174 | ||
|
|
19450b46ef | ||
|
|
bc755d3ad3 | ||
|
|
44a9221a9d | ||
|
|
89831f7090 | ||
|
|
84fd884052 | ||
|
|
57767156f7 | ||
|
|
9fa47cbba2 | ||
|
|
17418f82af | ||
|
|
01bbeced65 | ||
|
|
27e3d465bd | ||
|
|
ef5e954db1 |
@@ -7,7 +7,7 @@
|
||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "jellyseerr",
|
||||
"projectOwner": "Fallenbagel",
|
||||
"projectOwner": "fallenbagel",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true,
|
||||
@@ -448,6 +448,168 @@
|
||||
"contributions": [
|
||||
"security"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "j0srisk",
|
||||
"name": "Joseph Risk",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18372584?v=4",
|
||||
"profile": "http://josephrisk.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Loetwiek",
|
||||
"name": "Loetwiek",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/79059734?v=4",
|
||||
"profile": "https://github.com/Loetwiek",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Fuochi",
|
||||
"name": "Fuochi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4720478?v=4",
|
||||
"profile": "https://github.com/Fuochi",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "demrich",
|
||||
"name": "David Emrich",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/30092389?v=4",
|
||||
"profile": "https://github.com/demrich",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "maxnatamo",
|
||||
"name": "Max T. Kristiansen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5898152?v=4",
|
||||
"profile": "https://maxtrier.dk",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "DamsDev1",
|
||||
"name": "Damien Fajole",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/60252259?v=4",
|
||||
"profile": "https://damsdev.me",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "AhmedNSidd",
|
||||
"name": "Ahmed Siddiqui",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/36286128?v=4",
|
||||
"profile": "https://github.com/AhmedNSidd",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Zariel",
|
||||
"name": "Chris Bannister",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2213?v=4",
|
||||
"profile": "https://github.com/Zariel",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "C4J3",
|
||||
"name": "Joe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13005453?v=4",
|
||||
"profile": "https://github.com/C4J3",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "guillaumearnx",
|
||||
"name": "Guillaume ARNOUX",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37373941?v=4",
|
||||
"profile": "https://me.garnx.fr",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dr-carrot",
|
||||
"name": "dr-carrot",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17272571?v=4",
|
||||
"profile": "https://github.com/dr-carrot",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gageorsburn",
|
||||
"name": "Gage Orsburn",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4692734?v=4",
|
||||
"profile": "https://github.com/gageorsburn",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "GkhnGRBZ",
|
||||
"name": "GkhnGRBZ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/127258824?v=4",
|
||||
"profile": "https://github.com/GkhnGRBZ",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "benhaney",
|
||||
"name": "Ben Haney",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31331498?v=4",
|
||||
"profile": "http://benhaney.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Wunderharke",
|
||||
"name": "Wunderharke",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5105672?v=4",
|
||||
"profile": "https://github.com/Wunderharke",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "methbkts",
|
||||
"name": "Metin Bektas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/30674934?v=4",
|
||||
"profile": "https://github.com/methbkts",
|
||||
"contributions": [
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "andrewkolda",
|
||||
"name": "andrewkolda",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/158614532?v=4",
|
||||
"profile": "https://github.com/andrewkolda",
|
||||
"contributions": [
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ishanjain28",
|
||||
"name": "Ishan Jain",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7921368?v=4",
|
||||
"profile": "https://ishanjain.me",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/bug.yml
vendored
10
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -55,6 +55,16 @@ body:
|
||||
- tablet
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: database
|
||||
attributes:
|
||||
options:
|
||||
- SQLite (default)
|
||||
- PostgreSQL
|
||||
label: Database
|
||||
description: Which database backend are you using?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
|
||||
87
.github/workflows/ci.yml
vendored
87
.github/workflows/ci.yml
vendored
@@ -12,8 +12,8 @@ jobs:
|
||||
test:
|
||||
name: Lint & Test Build
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-22.04
|
||||
container: node:20-alpine
|
||||
runs-on: ubuntu-24.04
|
||||
container: node:22-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -43,15 +43,23 @@ jobs:
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
build_and_push:
|
||||
build:
|
||||
name: Build & Publish Docker Images
|
||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux/amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
outputs:
|
||||
digest-amd64: ${{ steps.set_outputs.outputs.digest-amd64 }}
|
||||
digest-arm64: ${{ steps.set_outputs.outputs.digest-arm64 }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
@@ -70,24 +78,77 @@ jobs:
|
||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
- name: Build and push
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
fallenbagel/jellyseerr
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=,suffix=,format=short
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
build-args: |
|
||||
COMMIT_TAG=${{ github.sha }}
|
||||
tags: |
|
||||
fallenbagel/jellyseerr:develop
|
||||
ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
|
||||
outputs: |
|
||||
type=image,push-by-digest=true,name=fallenbagel/jellyseerr,push=true
|
||||
type=image,push-by-digest=true,name=ghcr.io/${{ env.OWNER_LC }}/jellyseerr,push=true
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
provenance: false
|
||||
- name: Set outputs
|
||||
id: set_outputs
|
||||
run: |
|
||||
platform="${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}"
|
||||
echo "digest-${platform}=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
|
||||
|
||||
merge_and_push:
|
||||
name: Create and Push Multi-arch Manifest
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set lower case owner name
|
||||
run: |
|
||||
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
|
||||
env:
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
- name: Create and push manifest
|
||||
run: |
|
||||
docker manifest create fallenbagel/jellyseerr:develop \
|
||||
--amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \
|
||||
--amend fallenbagel/jellyseerr@${{ needs.build.outputs.digest-arm64 }}
|
||||
docker manifest push fallenbagel/jellyseerr:develop
|
||||
|
||||
# GHCR manifest
|
||||
docker manifest create ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop \
|
||||
--amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-amd64 }} \
|
||||
--amend ghcr.io/${{ env.OWNER_LC }}/jellyseerr@${{ needs.build.outputs.digest-arm64 }}
|
||||
docker manifest push ghcr.io/${{ env.OWNER_LC }}/jellyseerr:develop
|
||||
|
||||
discord:
|
||||
name: Send Discord Notification
|
||||
needs: build_and_push
|
||||
needs: merge_and_push
|
||||
if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
|
||||
2
.github/workflows/cypress.yml
vendored
2
.github/workflows/cypress.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
- name: Pnpm Setup
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
|
||||
135
.github/workflows/helm.yml
vendored
Normal file
135
.github/workflows/helm.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
name: Release Charts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
package-helm-chart:
|
||||
name: Package helm chart
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
outputs:
|
||||
has_artifacts: ${{ steps.check-artifacts.outputs.has_artifacts }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
- name: Install Oras
|
||||
uses: oras-project/setup-oras@v1
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Package helm charts
|
||||
run: |
|
||||
mkdir -p ./.cr-release-packages
|
||||
for chart_path in ./charts/*; do
|
||||
if [ -d "$chart_path" ] && [ -f "$chart_path/Chart.yaml" ]; then
|
||||
chart_name=$(grep '^name:' "$chart_path/Chart.yaml" | awk '{print $2}')
|
||||
# get current version
|
||||
current_version=$(grep '^version:' "$chart_path/Chart.yaml" | awk '{print $2}')
|
||||
# try to get current release version
|
||||
set +e
|
||||
oras discover ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:${current_version}
|
||||
oras_exit_code=$?
|
||||
set -e
|
||||
|
||||
if [ $oras_exit_code -ne 0 ]; then
|
||||
helm dependency build "$chart_path"
|
||||
helm package "$chart_path" --destination ./.cr-release-packages
|
||||
else
|
||||
echo "No version change for $chart_name. Skipping."
|
||||
fi
|
||||
else
|
||||
echo "Skipping $chart_name: Not a valid Helm chart"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Check if artifacts exist
|
||||
id: check-artifacts
|
||||
run: |
|
||||
if ls .cr-release-packages/* >/dev/null 2>&1; then
|
||||
echo "has_artifacts=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_artifacts=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
if: steps.check-artifacts.outputs.has_artifacts == 'true'
|
||||
with:
|
||||
name: artifacts
|
||||
include-hidden-files: true
|
||||
path: .cr-release-packages/
|
||||
|
||||
publish:
|
||||
name: Publish to ghcr.io
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write # needed for pushing to github registry
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
needs: [package-helm-chart]
|
||||
if: needs.package-helm-chart.outputs.has_artifacts == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
- name: Install Oras
|
||||
uses: oras-project/setup-oras@v1
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
- name: Downloads artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: artifacts
|
||||
path: .cr-release-packages/
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Push charts to GHCR
|
||||
env:
|
||||
COSIGN_YES: true
|
||||
run: |
|
||||
for chart_path in `find .cr-release-packages -name '*.tgz' -print`; do
|
||||
# push chart to OCI
|
||||
chart_release_file=$(basename "$chart_path")
|
||||
chart_name=${chart_release_file%-*}
|
||||
helm push ${chart_path} oci://ghcr.io/${GITHUB_REPOSITORY@L} |& tee helm-push-output.log
|
||||
chart_digest=$(awk -F "[, ]+" '/Digest/{print $NF}' < helm-push-output.log)
|
||||
# sign chart
|
||||
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}@${chart_digest}"
|
||||
# push artifacthub-repo.yml to OCI
|
||||
oras push \
|
||||
ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io \
|
||||
--config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \
|
||||
charts/$chart_name/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml \
|
||||
|& tee oras-push-output.log
|
||||
artifacthub_digest=$(grep "Digest:" oras-push-output.log | awk '{print $2}')
|
||||
# sign artifacthub-repo.yml
|
||||
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io@${artifacthub_digest}"
|
||||
done
|
||||
33
.github/workflows/lint-helm-charts.yml
vendored
Normal file
33
.github/workflows/lint-helm-charts.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Lint and Test Charts
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- '.github/workflows/lint-helm-charts.yml'
|
||||
- 'charts/**'
|
||||
jobs:
|
||||
lint-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4.2.0
|
||||
- name: Ensure documentation is updated
|
||||
uses: docker://jnorwood/helm-docs:v1.14.2
|
||||
- name: Set up chart-testing
|
||||
uses: helm/chart-testing-action@v2.6.1
|
||||
- name: Run chart-testing (list-changed)
|
||||
id: list-changed
|
||||
run: |
|
||||
changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }})
|
||||
if [[ -n "$changed" ]]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Run chart-testing
|
||||
if: steps.list-changed.outputs.changed == 'true'
|
||||
run: ct lint --target-branch ${{ github.event.repository.default_branch }} --validate-maintainers=false
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
@@ -26,6 +26,12 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
- name: Pnpm Setup
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
|
||||
2
.github/workflows/test-docs-deploy.yml
vendored
2
.github/workflows/test-docs-deploy.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
path:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'gen-docs/**'
|
||||
|
||||
|
||||
@@ -9,3 +9,6 @@ pnpm-lock.yaml
|
||||
src/assets/
|
||||
public/
|
||||
docs/
|
||||
|
||||
# helm charts
|
||||
**/charts
|
||||
|
||||
@@ -15,5 +15,11 @@ module.exports = {
|
||||
rangeEnd: 0, // default: Infinity
|
||||
},
|
||||
},
|
||||
{
|
||||
files: 'charts/**',
|
||||
options: {
|
||||
rangeEnd: 0, // default: Infinity
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
||||
|
||||
- HTML/Typescript/Javascript editor
|
||||
- [VSCode](https://code.visualstudio.com/) is recommended. Upon opening the project, a few extensions will be automatically recommended for install.
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 20.x)
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 22.x)
|
||||
- [Pnpm](https://pnpm.io/cli/install)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
@@ -101,6 +101,46 @@ We use [Weblate](https://jellyseerr.borgcube.de/projects/jellyseerr/jellyseerr-f
|
||||
|
||||
<a href="https://jellyseerr.borgcube.de/engage/jellysseerr/"><img src="https://jellyseerr.borgcube.de/widget/jellyseerr/multi-auto.svg" alt="Translation status" /></a>
|
||||
|
||||
## Migrations
|
||||
|
||||
If you are adding a new feature that requires a database migration, you will need to create 2 migrations: one for SQLite and one for PostgreSQL. Here is how you could do it:
|
||||
|
||||
1. Create a PostgreSQL database or use an existing one:
|
||||
|
||||
```bash
|
||||
sudo docker run --name postgres-jellyseerr -e POSTGRES_PASSWORD=postgres -d -p 127.0.0.1:5432:5432/tcp postgres:latest
|
||||
```
|
||||
|
||||
2. Reset the SQLite database and the PostgreSQL database:
|
||||
|
||||
```bash
|
||||
rm config/db/db.*
|
||||
rm config/settings.*
|
||||
PGPASSWORD=postgres sudo docker exec -it postgres-jellyseerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "DROP DATABASE IF EXISTS jellyseerr;"
|
||||
PGPASSWORD=postgres sudo docker exec -it postgres-jellyseerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE jellyseerr;"
|
||||
```
|
||||
|
||||
3. Checkout the `develop` branch and create the original database for SQLite and PostgreSQL so that TypeORM can automatically generate the migrations:
|
||||
|
||||
```bash
|
||||
git checkout develop
|
||||
pnpm i
|
||||
rm -r .next dist; pnpm build
|
||||
pnpm start
|
||||
DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm start
|
||||
```
|
||||
|
||||
(You can shutdown the server once the message "Server ready on 5055" appears)
|
||||
|
||||
4. Let TypeORM generate the migrations:
|
||||
|
||||
```bash
|
||||
git checkout -b your-feature-branch
|
||||
pnpm i
|
||||
pnpm migration:generate server/migration/sqlite/YourMigrationName
|
||||
DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm migration:generate server/migration/postgres/YourMigrationName
|
||||
```
|
||||
|
||||
## Attribution
|
||||
|
||||
This contribution guide was inspired by the [Next.js](https://github.com/vercel/next.js), [Radarr](https://github.com/Radarr/Radarr), and [Overseerr](https://github.com/sct/Overseerr) contribution guides.
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS BUILD_IMAGE
|
||||
FROM node:22-alpine AS BUILD_IMAGE
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -14,7 +14,7 @@ RUN \
|
||||
;; \
|
||||
esac
|
||||
|
||||
RUN npm install --global pnpm
|
||||
RUN npm install --global pnpm@9
|
||||
|
||||
COPY package.json pnpm-lock.yaml postinstall-win.js ./
|
||||
RUN CYPRESS_INSTALL_BINARY=0 pnpm install --frozen-lockfile
|
||||
@@ -29,14 +29,14 @@ RUN pnpm build
|
||||
# remove development dependencies
|
||||
RUN pnpm prune --prod --ignore-scripts
|
||||
|
||||
RUN rm -rf src server .next/cache
|
||||
RUN rm -rf src server .next/cache charts gen-docs docs
|
||||
|
||||
RUN touch config/DOCKER
|
||||
|
||||
RUN echo "{\"commitTag\": \"${COMMIT_TAG}\"}" > committag.json
|
||||
|
||||
|
||||
FROM node:20-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
# Metadata for Github Package Registry
|
||||
LABEL org.opencontainers.image.source="https://github.com/Fallenbagel/jellyseerr"
|
||||
@@ -45,7 +45,7 @@ WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache tzdata tini && rm -rf /tmp/*
|
||||
|
||||
RUN npm install -g pnpm
|
||||
RUN npm install -g pnpm@9
|
||||
|
||||
# copy from build image
|
||||
COPY --from=BUILD_IMAGE /app ./
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM node:20-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
Run npm install --global pnpm
|
||||
RUN npm install --global pnpm@9
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
|
||||
113
README.md
113
README.md
@@ -11,17 +11,17 @@
|
||||
<a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-48-orange.svg"/></a>
|
||||
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-66-orange.svg"/></a>
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library.
|
||||
It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring support for [Jellyfin](https://github.com/jellyfin/jellyfin) & [Emby](https://github.com/MediaBrowser/Emby) media servers!
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||
|
||||
## Current Features
|
||||
|
||||
- Full Jellyfin/Emby/Plex integration including authentication with user import & management
|
||||
- Supports Movies, Shows and Mixed Libraries
|
||||
- Ability to change email addresses for smtp purposes
|
||||
- Full Jellyfin/Emby/Plex integration including authentication with user import & management.
|
||||
- Support for **PostgreSQL** and **SQLite** databases.
|
||||
- Supports Movies, Shows and Mixed Libraries.
|
||||
- Ability to change email addresses for SMTP purposes.
|
||||
- Easy integration with your existing services. Currently, Jellyseerr supports Sonarr and Radarr. More to come!
|
||||
- Jellyfin/Emby/Plex library scan, to keep track of the titles which are already available.
|
||||
- Customizable request system, which allows users to request individual seasons or movies in a friendly, easy-to-use interface.
|
||||
@@ -29,8 +29,7 @@ It is a fork of [Overseerr](https://github.com/sct/overseerr) built to bring sup
|
||||
- Granular permission system.
|
||||
- Support for various notification agents.
|
||||
- Mobile-friendly design, for when you need to approve requests on the go!
|
||||
|
||||
(Upcoming Features include: Multiple Server Instances, and much more!)
|
||||
- Support for watchlisting & blacklisting media.
|
||||
|
||||
With more features on the way! Check out our [issue tracker](https://github.com/fallenbagel/jellyseerr/issues) to see the features which have already been requested.
|
||||
|
||||
@@ -87,66 +86,90 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fallenbagel"><img src="https://avatars.githubusercontent.com/u/98979876?v=4?s=100" width="100px;" alt="Fallenbagel"/><br /><sub><b>Fallenbagel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fallenbagel" title="Code">💻</a> <a href="#maintenance-Fallenbagel" title="Maintenance">🚧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/seanzhang98"><img src="https://avatars.githubusercontent.com/u/34902361?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="#translation-seanzhang98" title="Translation">🌍</a> <a href="https://github.com/Fallenbagel/jellyseerr/commits?author=seanzhang98" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notfakie"><img src="https://avatars.githubusercontent.com/u/103784113?v=4?s=100" width="100px;" alt="notfakie"/><br /><sub><b>notfakie</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=notfakie" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jumail"><img src="https://avatars.githubusercontent.com/u/7672055?v=4?s=100" width="100px;" alt="Mohamed Jumail"/><br /><sub><b>Mohamed Jumail</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/pulls?q=is%3Apr+reviewed-by%3AJumail" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.heywhale.com"><img src="https://avatars.githubusercontent.com/u/4048787?v=4?s=100" width="100px;" alt="Shilong Jiang"/><br /><sub><b>Shilong Jiang</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jsl9208" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jinas.me"><img src="https://avatars.githubusercontent.com/u/28459081?v=4?s=100" width="100px;" alt="Boring Dragon"/><br /><sub><b>Boring Dragon</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=boring-dragon" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sambartik"><img src="https://avatars.githubusercontent.com/u/63553146?v=4?s=100" width="100px;" alt="Samuel Bartík"/><br /><sub><b>Samuel Bartík</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=sambartik" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fallenbagel"><img src="https://avatars.githubusercontent.com/u/98979876?v=4?s=100" width="100px;" alt="Fallenbagel"/><br /><sub><b>Fallenbagel</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fallenbagel" title="Code">💻</a> <a href="#maintenance-Fallenbagel" title="Maintenance">🚧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/seanzhang98"><img src="https://avatars.githubusercontent.com/u/34902361?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="#translation-seanzhang98" title="Translation">🌍</a> <a href="https://github.com/fallenbagel/jellyseerr/commits?author=seanzhang98" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notfakie"><img src="https://avatars.githubusercontent.com/u/103784113?v=4?s=100" width="100px;" alt="notfakie"/><br /><sub><b>notfakie</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=notfakie" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jumail"><img src="https://avatars.githubusercontent.com/u/7672055?v=4?s=100" width="100px;" alt="Mohamed Jumail"/><br /><sub><b>Mohamed Jumail</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/pulls?q=is%3Apr+reviewed-by%3AJumail" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.heywhale.com"><img src="https://avatars.githubusercontent.com/u/4048787?v=4?s=100" width="100px;" alt="Shilong Jiang"/><br /><sub><b>Shilong Jiang</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=jsl9208" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jinas.me"><img src="https://avatars.githubusercontent.com/u/28459081?v=4?s=100" width="100px;" alt="Boring Dragon"/><br /><sub><b>Boring Dragon</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=boring-dragon" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sambartik"><img src="https://avatars.githubusercontent.com/u/63553146?v=4?s=100" width="100px;" alt="Samuel Bartík"/><br /><sub><b>Samuel Bartík</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=sambartik" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CyferShepard"><img src="https://avatars.githubusercontent.com/u/24864904?v=4?s=100" width="100px;" alt="Thegan Govender"/><br /><sub><b>Thegan Govender</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=CyferShepard" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jab416171"><img src="https://avatars.githubusercontent.com/u/345752?v=4?s=100" width="100px;" alt="jab416171"/><br /><sub><b>jab416171</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jab416171" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nvds.be"><img src="https://avatars.githubusercontent.com/u/5257222?v=4?s=100" width="100px;" alt="Nicolai Van der Storm"/><br /><sub><b>Nicolai Van der Storm</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=NicolaiVdS" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CyferShepard"><img src="https://avatars.githubusercontent.com/u/24864904?v=4?s=100" width="100px;" alt="Thegan Govender"/><br /><sub><b>Thegan Govender</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=CyferShepard" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jab416171"><img src="https://avatars.githubusercontent.com/u/345752?v=4?s=100" width="100px;" alt="jab416171"/><br /><sub><b>jab416171</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=jab416171" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nvds.be"><img src="https://avatars.githubusercontent.com/u/5257222?v=4?s=100" width="100px;" alt="Nicolai Van der Storm"/><br /><sub><b>Nicolai Van der Storm</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=NicolaiVdS" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Smexhy"><img src="https://avatars.githubusercontent.com/u/4880625?v=4?s=100" width="100px;" alt="Smexhy"/><br /><sub><b>Smexhy</b></sub></a><br /><a href="#translation-Smexhy" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dd06-dev.fr"><img src="https://avatars.githubusercontent.com/u/58089504?v=4?s=100" width="100px;" alt="dd060606"/><br /><sub><b>dd060606</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=dd060606" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://qwer.tz"><img src="https://avatars.githubusercontent.com/u/71837281?v=4?s=100" width="100px;" alt="Daniel"/><br /><sub><b>Daniel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=darmiel" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dd06-dev.fr"><img src="https://avatars.githubusercontent.com/u/58089504?v=4?s=100" width="100px;" alt="dd060606"/><br /><sub><b>dd060606</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=dd060606" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://qwer.tz"><img src="https://avatars.githubusercontent.com/u/71837281?v=4?s=100" width="100px;" alt="Daniel"/><br /><sub><b>Daniel</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=darmiel" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/undone37"><img src="https://avatars.githubusercontent.com/u/10513808?v=4?s=100" width="100px;" alt="undone37"/><br /><sub><b>undone37</b></sub></a><br /><a href="#translation-undone37" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CheChu10"><img src="https://avatars.githubusercontent.com/u/32913133?v=4?s=100" width="100px;" alt="Chechu García"/><br /><sub><b>Chechu García</b></sub></a><br /><a href="#translation-CheChu10" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DimitriDR"><img src="https://avatars.githubusercontent.com/u/56969769?v=4?s=100" width="100px;" alt="Dimitri"/><br /><sub><b>Dimitri</b></sub></a><br /><a href="#translation-DimitriDR" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrey4korop"><img src="https://avatars.githubusercontent.com/u/24610708?v=4?s=100" width="100px;" alt="andrey4korop"/><br /><sub><b>andrey4korop</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=andrey4korop" title="Code">💻</a> <a href="#translation-andrey4korop" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrey4korop"><img src="https://avatars.githubusercontent.com/u/24610708?v=4?s=100" width="100px;" alt="andrey4korop"/><br /><sub><b>andrey4korop</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=andrey4korop" title="Code">💻</a> <a href="#translation-andrey4korop" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://geoffrey-coulaud.fr"><img src="https://avatars.githubusercontent.com/u/20744730?v=4?s=100" width="100px;" alt="Geoffrey Coulaud"/><br /><sub><b>Geoffrey Coulaud</b></sub></a><br /><a href="#translation-GeoffreyCoulaud" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pikachu920"><img src="https://avatars.githubusercontent.com/u/28607612?v=4?s=100" width="100px;" alt="Pikachu920"/><br /><sub><b>Pikachu920</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Pikachu920" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yalagin"><img src="https://avatars.githubusercontent.com/u/12879142?v=4?s=100" width="100px;" alt="Maxim Yalagin"/><br /><sub><b>Maxim Yalagin</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=yalagin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jeaboswell"><img src="https://avatars.githubusercontent.com/u/11653068?v=4?s=100" width="100px;" alt="Jesse Boswell"/><br /><sub><b>Jesse Boswell</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=jeaboswell" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pikachu920"><img src="https://avatars.githubusercontent.com/u/28607612?v=4?s=100" width="100px;" alt="Pikachu920"/><br /><sub><b>Pikachu920</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Pikachu920" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yalagin"><img src="https://avatars.githubusercontent.com/u/12879142?v=4?s=100" width="100px;" alt="Maxim Yalagin"/><br /><sub><b>Maxim Yalagin</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=yalagin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jeaboswell"><img src="https://avatars.githubusercontent.com/u/11653068?v=4?s=100" width="100px;" alt="Jesse Boswell"/><br /><sub><b>Jesse Boswell</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=jeaboswell" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/d-fendrich"><img src="https://avatars.githubusercontent.com/u/27904138?v=4?s=100" width="100px;" alt="d-fendrich"/><br /><sub><b>d-fendrich</b></sub></a><br /><a href="#translation-d-fendrich" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/davidfdezalcoba"><img src="https://avatars.githubusercontent.com/u/15996018?v=4?s=100" width="100px;" alt="David Fernández Alcoba"/><br /><sub><b>David Fernández Alcoba</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=davidfdezalcoba" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/davidfdezalcoba"><img src="https://avatars.githubusercontent.com/u/15996018?v=4?s=100" width="100px;" alt="David Fernández Alcoba"/><br /><sub><b>David Fernández Alcoba</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=davidfdezalcoba" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Gauvino"><img src="https://avatars.githubusercontent.com/u/68083474?v=4?s=100" width="100px;" alt="Gauvino"/><br /><sub><b>Gauvino</b></sub></a><br /><a href="#translation-Gauvino" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EthanArmbrust"><img src="https://avatars.githubusercontent.com/u/22754714?v=4?s=100" width="100px;" alt="EthanArmbrust"/><br /><sub><b>EthanArmbrust</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=EthanArmbrust" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.piribisoft.com"><img src="https://avatars.githubusercontent.com/u/854646?v=4?s=100" width="100px;" alt="Eduardo"/><br /><sub><b>Eduardo</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=SirMartin" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RickLuiken"><img src="https://avatars.githubusercontent.com/u/34110371?v=4?s=100" width="100px;" alt="RickLuiken"/><br /><sub><b>RickLuiken</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=RickLuiken" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EthanArmbrust"><img src="https://avatars.githubusercontent.com/u/22754714?v=4?s=100" width="100px;" alt="EthanArmbrust"/><br /><sub><b>EthanArmbrust</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=EthanArmbrust" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.piribisoft.com"><img src="https://avatars.githubusercontent.com/u/854646?v=4?s=100" width="100px;" alt="Eduardo"/><br /><sub><b>Eduardo</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=SirMartin" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RickLuiken"><img src="https://avatars.githubusercontent.com/u/34110371?v=4?s=100" width="100px;" alt="RickLuiken"/><br /><sub><b>RickLuiken</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=RickLuiken" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Br33ce"><img src="https://avatars.githubusercontent.com/u/124933490?v=4?s=100" width="100px;" alt="Br33ce"/><br /><sub><b>Br33ce</b></sub></a><br /><a href="#translation-Br33ce" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://athfan.com"><img src="https://avatars.githubusercontent.com/u/13810742?v=4?s=100" width="100px;" alt="Athfan Khaleel"/><br /><sub><b>Athfan Khaleel</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=athphane" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://athfan.com"><img src="https://avatars.githubusercontent.com/u/13810742?v=4?s=100" width="100px;" alt="Athfan Khaleel"/><br /><sub><b>Athfan Khaleel</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=athphane" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mdll23"><img src="https://avatars.githubusercontent.com/u/142844478?v=4?s=100" width="100px;" alt="Michael Dallinger"/><br /><sub><b>Michael Dallinger</b></sub></a><br /><a href="#translation-mdll23" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xeruf"><img src="https://avatars.githubusercontent.com/u/13354331?v=4?s=100" width="100px;" alt="Janek"/><br /><sub><b>Janek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=xeruf" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://aleksasiriski.dev"><img src="https://avatars.githubusercontent.com/u/31509435?v=4?s=100" width="100px;" alt="Aleksa Siriški"/><br /><sub><b>Aleksa Siriški</b></sub></a><br /><a href="#infra-aleksasiriski" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://danishhumair.com"><img src="https://avatars.githubusercontent.com/u/121830048?v=4?s=100" width="100px;" alt="Danish Humair"/><br /><sub><b>Danish Humair</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Danish-H" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://arm0.red"><img src="https://avatars.githubusercontent.com/u/16858514?v=4?s=100" width="100px;" alt="Stephen Harris"/><br /><sub><b>Stephen Harris</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=trackmastersteve" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.boniface.me"><img src="https://avatars.githubusercontent.com/u/4031396?v=4?s=100" width="100px;" alt="Joshua M. Boniface"/><br /><sub><b>Joshua M. Boniface</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=joshuaboniface" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://gauthierth.fr/"><img src="https://avatars.githubusercontent.com/u/37781713?v=4?s=100" width="100px;" alt="Gauthier"/><br /><sub><b>Gauthier</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=gauthier-th" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kara-Zor-El"><img src="https://avatars.githubusercontent.com/u/69772087?v=4?s=100" width="100px;" alt="Kara"/><br /><sub><b>Kara</b></sub></a><br /><a href="#infra-Kara-Zor-El" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://joaquinolivero.com"><img src="https://avatars.githubusercontent.com/u/66050823?v=4?s=100" width="100px;" alt="Joaquin Olivero"/><br /><sub><b>Joaquin Olivero</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=JoaquinOlivero" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bretterteig"><img src="https://avatars.githubusercontent.com/u/47298401?v=4?s=100" width="100px;" alt="Julian Behr"/><br /><sub><b>Julian Behr</b></sub></a><br /><a href="#translation-Bretterteig" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThowZzy"><img src="https://avatars.githubusercontent.com/u/61882536?v=4?s=100" width="100px;" alt="ThowZzy"/><br /><sub><b>ThowZzy</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=ThowZzy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://mobihen.com"><img src="https://avatars.githubusercontent.com/u/35529491?v=4?s=100" width="100px;" alt="Nir Israel Hen"/><br /><sub><b>Nir Israel Hen</b></sub></a><br /><a href="#translation-mobihen" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XDark187"><img src="https://avatars.githubusercontent.com/u/39034192?v=4?s=100" width="100px;" alt="Baraa"/><br /><sub><b>Baraa</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=XDark187" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franciscofsales"><img src="https://avatars.githubusercontent.com/u/7977645?v=4?s=100" width="100px;" alt="Francisco Sales"/><br /><sub><b>Francisco Sales</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=franciscofsales" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myselfolli"><img src="https://avatars.githubusercontent.com/u/37535998?v=4?s=100" width="100px;" alt="Oliver Laing"/><br /><sub><b>Oliver Laing</b></sub></a><br /><a href="https://github.com/Fallenbagel/jellyseerr/commits?author=myselfolli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XDark187"><img src="https://avatars.githubusercontent.com/u/39034192?v=4?s=100" width="100px;" alt="Baraa"/><br /><sub><b>Baraa</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=XDark187" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franciscofsales"><img src="https://avatars.githubusercontent.com/u/7977645?v=4?s=100" width="100px;" alt="Francisco Sales"/><br /><sub><b>Francisco Sales</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=franciscofsales" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myselfolli"><img src="https://avatars.githubusercontent.com/u/37535998?v=4?s=100" width="100px;" alt="Oliver Laing"/><br /><sub><b>Oliver Laing</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=myselfolli" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/M0NsTeRRR"><img src="https://avatars.githubusercontent.com/u/37785089?v=4?s=100" width="100px;" alt="Ludovic Ortega"/><br /><sub><b>Ludovic Ortega</b></sub></a><br /><a href="#security-M0NsTeRRR" title="Security">🛡️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=demrich" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=maxnatamo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=DamsDev1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zariel"><img src="https://avatars.githubusercontent.com/u/2213?v=4?s=100" width="100px;" alt="Chris Bannister"/><br /><sub><b>Chris Bannister</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Zariel" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/C4J3"><img src="https://avatars.githubusercontent.com/u/13005453?v=4?s=100" width="100px;" alt="Joe"/><br /><sub><b>Joe</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=C4J3" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://me.garnx.fr"><img src="https://avatars.githubusercontent.com/u/37373941?v=4?s=100" width="100px;" alt="Guillaume ARNOUX"/><br /><sub><b>Guillaume ARNOUX</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=guillaumearnx" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dr-carrot"><img src="https://avatars.githubusercontent.com/u/17272571?v=4?s=100" width="100px;" alt="dr-carrot"/><br /><sub><b>dr-carrot</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=dr-carrot" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gageorsburn"><img src="https://avatars.githubusercontent.com/u/4692734?v=4?s=100" width="100px;" alt="Gage Orsburn"/><br /><sub><b>Gage Orsburn</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=gageorsburn" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GkhnGRBZ"><img src="https://avatars.githubusercontent.com/u/127258824?v=4?s=100" width="100px;" alt="GkhnGRBZ"/><br /><sub><b>GkhnGRBZ</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=GkhnGRBZ" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://benhaney.com"><img src="https://avatars.githubusercontent.com/u/31331498?v=4?s=100" width="100px;" alt="Ben Haney"/><br /><sub><b>Ben Haney</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=benhaney" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wunderharke"><img src="https://avatars.githubusercontent.com/u/5105672?v=4?s=100" width="100px;" alt="Wunderharke"/><br /><sub><b>Wunderharke</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=Wunderharke" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/methbkts"><img src="https://avatars.githubusercontent.com/u/30674934?v=4?s=100" width="100px;" alt="Metin Bektas"/><br /><sub><b>Metin Bektas</b></sub></a><br /><a href="#infra-methbkts" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andrewkolda"><img src="https://avatars.githubusercontent.com/u/158614532?v=4?s=100" width="100px;" alt="andrewkolda"/><br /><sub><b>andrewkolda</b></sub></a><br /><a href="#design-andrewkolda" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://ishanjain.me"><img src="https://avatars.githubusercontent.com/u/7921368?v=4?s=100" width="100px;" alt="Ishan Jain"/><br /><sub><b>Ishan Jain</b></sub></a><br /><a href="https://github.com/fallenbagel/jellyseerr/commits?author=ishanjain28" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -291,6 +314,12 @@ Thanks goes to these wonderful people from Overseerr ([emoji key](https://allcon
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://josephrisk.com"><img src="https://avatars.githubusercontent.com/u/18372584?v=4?s=100" width="100px;" alt="Joseph Risk"/><br /><sub><b>Joseph Risk</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=j0srisk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Loetwiek"><img src="https://avatars.githubusercontent.com/u/79059734?v=4?s=100" width="100px;" alt="Loetwiek"/><br /><sub><b>Loetwiek</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Loetwiek" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Fuochi"><img src="https://avatars.githubusercontent.com/u/4720478?v=4?s=100" width="100px;" alt="Fuochi"/><br /><sub><b>Fuochi</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=Fuochi" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demrich"><img src="https://avatars.githubusercontent.com/u/30092389?v=4?s=100" width="100px;" alt="David Emrich"/><br /><sub><b>David Emrich</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=demrich" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://maxtrier.dk"><img src="https://avatars.githubusercontent.com/u/5898152?v=4?s=100" width="100px;" alt="Max T. Kristiansen"/><br /><sub><b>Max T. Kristiansen</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=maxnatamo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://damsdev.me"><img src="https://avatars.githubusercontent.com/u/60252259?v=4?s=100" width="100px;" alt="Damien Fajole"/><br /><sub><b>Damien Fajole</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=DamsDev1" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedNSidd"><img src="https://avatars.githubusercontent.com/u/36286128?v=4?s=100" width="100px;" alt="Ahmed Siddiqui"/><br /><sub><b>Ahmed Siddiqui</b></sub></a><br /><a href="https://github.com/sct/overseerr/commits?author=AhmedNSidd" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
25
charts/jellyseerr-chart/.helmignore
Normal file
25
charts/jellyseerr-chart/.helmignore
Normal file
@@ -0,0 +1,25 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
# go template
|
||||
*.gotmpl
|
||||
13
charts/jellyseerr-chart/Chart.yaml
Normal file
13
charts/jellyseerr-chart/Chart.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v2
|
||||
kubeVersion: ">=1.23.0-0"
|
||||
name: jellyseerr-chart
|
||||
description: Jellyseerr helm chart for Kubernetes
|
||||
type: application
|
||||
version: 2.1.1
|
||||
appVersion: "2.3.0"
|
||||
maintainers:
|
||||
- name: Jellyseerr
|
||||
url: https://github.com/Fallenbagel/jellyseerr
|
||||
sources:
|
||||
- https://github.com/Fallenbagel/jellyseerr/tree/main/charts/jellyseerr
|
||||
home: https://github.com/Fallenbagel/jellyseerr
|
||||
67
charts/jellyseerr-chart/README.md
Normal file
67
charts/jellyseerr-chart/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# jellyseerr-chart
|
||||
|
||||
  
|
||||
|
||||
Jellyseerr helm chart for Kubernetes
|
||||
|
||||
**Homepage:** <https://github.com/Fallenbagel/jellyseerr>
|
||||
|
||||
## Maintainers
|
||||
|
||||
| Name | Email | Url |
|
||||
| ---- | ------ | --- |
|
||||
| Jellyseerr | | <https://github.com/Fallenbagel/jellyseerr> |
|
||||
|
||||
## Source Code
|
||||
|
||||
* <https://github.com/Fallenbagel/jellyseerr/tree/main/charts/jellyseerr>
|
||||
|
||||
## Requirements
|
||||
|
||||
Kubernetes: `>=1.23.0-0`
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | |
|
||||
| config | object | `{"persistence":{"accessModes":["ReadWriteOnce"],"annotations":{},"name":"","size":"5Gi","volumeName":""}}` | Creating PVC to store configuration |
|
||||
| config.persistence.accessModes | list | `["ReadWriteOnce"]` | Access modes of persistent disk |
|
||||
| config.persistence.annotations | object | `{}` | Annotations for PVCs |
|
||||
| config.persistence.name | string | `""` | Config name |
|
||||
| config.persistence.size | string | `"5Gi"` | Size of persistent disk |
|
||||
| config.persistence.volumeName | string | `""` | Name of the permanent volume to reference in the claim. Can be used to bind to existing volumes. |
|
||||
| extraEnv | list | `[]` | Environment variables to add to the jellyseerr pods |
|
||||
| extraEnvFrom | list | `[]` | Environment variables from secrets or configmaps to add to the jellyseerr pods |
|
||||
| fullnameOverride | string | `""` | |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.registry | string | `"ghcr.io"` | |
|
||||
| image.repository | string | `"fallenbagel/jellyseerr"` | |
|
||||
| image.sha | string | `""` | |
|
||||
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
|
||||
| imagePullSecrets | list | `[]` | |
|
||||
| ingress.annotations | object | `{}` | |
|
||||
| ingress.enabled | bool | `false` | |
|
||||
| ingress.hosts[0].host | string | `"chart-example.local"` | |
|
||||
| ingress.hosts[0].paths[0].path | string | `"/"` | |
|
||||
| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
|
||||
| ingress.ingressClassName | string | `""` | |
|
||||
| ingress.tls | list | `[]` | |
|
||||
| nameOverride | string | `""` | |
|
||||
| nodeSelector | object | `{}` | |
|
||||
| podAnnotations | object | `{}` | |
|
||||
| podLabels | object | `{}` | |
|
||||
| podSecurityContext | object | `{}` | |
|
||||
| replicaCount | int | `1` | |
|
||||
| resources | object | `{}` | |
|
||||
| securityContext | object | `{}` | |
|
||||
| service.port | int | `80` | |
|
||||
| service.type | string | `"ClusterIP"` | |
|
||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||
| serviceAccount.automount | bool | `true` | Automatically mount a ServiceAccount's API credentials? |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template |
|
||||
| strategy | object | `{"type":"Recreate"}` | Deployment strategy |
|
||||
| tolerations | list | `[]` | |
|
||||
| volumeMounts | list | `[]` | Additional volumeMounts on the output Deployment definition. |
|
||||
| volumes | list | `[]` | Additional volumes on the output Deployment definition. |
|
||||
17
charts/jellyseerr-chart/README.md.gotmpl
Normal file
17
charts/jellyseerr-chart/README.md.gotmpl
Normal file
@@ -0,0 +1,17 @@
|
||||
{{ template "chart.header" . }}
|
||||
|
||||
{{ template "chart.deprecationWarning" . }}
|
||||
|
||||
{{ template "chart.badgesSection" . }}
|
||||
|
||||
{{ template "chart.description" . }}
|
||||
|
||||
{{ template "chart.homepageLine" . }}
|
||||
|
||||
{{ template "chart.maintainersSection" . }}
|
||||
|
||||
{{ template "chart.sourcesSection" . }}
|
||||
|
||||
{{ template "chart.requirementsSection" . }}
|
||||
|
||||
{{ template "chart.valuesSection" . }}
|
||||
1
charts/jellyseerr-chart/artifacthub-repo.yml
Normal file
1
charts/jellyseerr-chart/artifacthub-repo.yml
Normal file
@@ -0,0 +1 @@
|
||||
repositoryID: c6b3f2dc-444c-4e37-b397-6a5ff563ee8b
|
||||
5
charts/jellyseerr-chart/templates/NOTES.txt
Normal file
5
charts/jellyseerr-chart/templates/NOTES.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
***********************************************************************
|
||||
Welcome to {{ .Chart.Name }}
|
||||
Chart version: {{ .Chart.Version }}
|
||||
App version: {{ .Chart.AppVersion }}
|
||||
***********************************************************************
|
||||
70
charts/jellyseerr-chart/templates/_helpers.tpl
Normal file
70
charts/jellyseerr-chart/templates/_helpers.tpl
Normal file
@@ -0,0 +1,70 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "jellyseerr.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "jellyseerr.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "jellyseerr.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "jellyseerr.labels" -}}
|
||||
helm.sh/chart: {{ include "jellyseerr.chart" . }}
|
||||
{{ include "jellyseerr.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/part-of: {{ .Chart.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "jellyseerr.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "jellyseerr.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "jellyseerr.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "jellyseerr.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the pvc config to use
|
||||
*/}}
|
||||
{{- define "jellyseerr.configPersistenceName" -}}
|
||||
{{- default (printf "%s-config" (include "jellyseerr.fullname" .)) .Values.config.persistence.name }}
|
||||
{{- end }}
|
||||
89
charts/jellyseerr-chart/templates/deployment.yaml
Normal file
89
charts/jellyseerr-chart/templates/deployment.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
strategy:
|
||||
type: {{ .Values.strategy.type }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "jellyseerr.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "jellyseerr.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
{{- if .Values.image.sha }}
|
||||
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}@sha256:{{ .Values.image.sha }}"
|
||||
{{- else }}
|
||||
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
{{- end }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 5055
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.extraEnv }}
|
||||
env:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.extraEnvFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /app/config
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "jellyseerr.configPersistenceName" . }}
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
41
charts/jellyseerr-chart/templates/ingress.yaml
Normal file
41
charts/jellyseerr-chart/templates/ingress.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.ingressClassName }}
|
||||
ingressClassName: {{ .Values.ingress.ingressClassName }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "jellyseerr.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
20
charts/jellyseerr-chart/templates/persistentvolumeclaim.yaml
Normal file
20
charts/jellyseerr-chart/templates/persistentvolumeclaim.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.configPersistenceName" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- with .Values.config.persistence.accessModes }}
|
||||
accessModes:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.config.persistence.volumeName }}
|
||||
volumeName: {{ .Values.config.persistence.volumeName }}
|
||||
{{- end }}
|
||||
{{- with .Values.config.persistence.storageClass }}
|
||||
storageClassName: {{ if (eq "-" .) }}""{{ else }}{{ . }}{{ end }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: "{{ .Values.config.persistence.size }}"
|
||||
16
charts/jellyseerr-chart/templates/service.yaml
Normal file
16
charts/jellyseerr-chart/templates/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.fullname" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "jellyseerr.selectorLabels" . | nindent 4 }}
|
||||
ipFamilyPolicy: PreferDualStack
|
||||
13
charts/jellyseerr-chart/templates/serviceaccount.yaml
Normal file
13
charts/jellyseerr-chart/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "jellyseerr.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
15
charts/jellyseerr-chart/templates/tests/test-connection.yaml
Normal file
15
charts/jellyseerr-chart/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "jellyseerr.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "jellyseerr.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "jellyseerr.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
114
charts/jellyseerr-chart/values.yaml
Normal file
114
charts/jellyseerr-chart/values.yaml
Normal file
@@ -0,0 +1,114 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
registry: ghcr.io
|
||||
repository: fallenbagel/jellyseerr
|
||||
pullPolicy: IfNotPresent
|
||||
# -- Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
sha: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# -- Deployment strategy
|
||||
strategy:
|
||||
type: Recreate
|
||||
|
||||
# -- Environment variables to add to the jellyseerr pods
|
||||
extraEnv: []
|
||||
# -- Environment variables from secrets or configmaps to add to the jellyseerr pods
|
||||
extraEnvFrom: []
|
||||
|
||||
serviceAccount:
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# -- Automatically mount a ServiceAccount's API credentials?
|
||||
automount: true
|
||||
# -- Annotations to add to the service account
|
||||
annotations: {}
|
||||
# -- The name of the service account to use.
|
||||
# -- If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
# -- Creating PVC to store configuration
|
||||
config:
|
||||
persistence:
|
||||
# -- Size of persistent disk
|
||||
size: 5Gi
|
||||
# -- Annotations for PVCs
|
||||
annotations: {}
|
||||
# -- Access modes of persistent disk
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
# -- Config name
|
||||
name: ""
|
||||
# -- Name of the permanent volume to reference in the claim.
|
||||
# Can be used to bind to existing volumes.
|
||||
volumeName: ""
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
ingressClassName: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# -- Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# -- Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
@@ -16,11 +16,15 @@
|
||||
"hideAvailable": false,
|
||||
"localLogin": true,
|
||||
"newPlexLogin": true,
|
||||
"region": "",
|
||||
"discoverRegion": "",
|
||||
"streamingRegion": "",
|
||||
"originalLanguage": "",
|
||||
"trustProxy": false,
|
||||
"mediaServerType": 1,
|
||||
"partialRequestsEnabled": true,
|
||||
"enableSpecialEpisodes": false,
|
||||
"forceIpv4First": false,
|
||||
"dnsServers": "",
|
||||
"locale": "en"
|
||||
},
|
||||
"plex": {
|
||||
@@ -99,6 +103,7 @@
|
||||
"options": {
|
||||
"botAPI": "",
|
||||
"chatId": "",
|
||||
"messageThreadId": "",
|
||||
"sendSilently": false
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,10 +13,10 @@ describe('General Settings', () => {
|
||||
});
|
||||
|
||||
it('modifies setting that requires restart', () => {
|
||||
cy.visit('/settings');
|
||||
cy.visit('/settings/network');
|
||||
|
||||
cy.get('#trustProxy').click();
|
||||
cy.get('[data-testid=settings-main-form]').submit();
|
||||
cy.get('[data-testid=settings-network-form]').submit();
|
||||
cy.get('[data-testid=modal-title]').should(
|
||||
'contain',
|
||||
'Server Restart Required'
|
||||
@@ -26,7 +26,7 @@ describe('General Settings', () => {
|
||||
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||
|
||||
cy.get('[type=checkbox]#trustProxy').click();
|
||||
cy.get('[data-testid=settings-main-form]').submit();
|
||||
cy.get('[data-testid=settings-network-form]').submit();
|
||||
cy.get('[data-testid=modal-title]').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
38
docker-compose.postgres.yaml
Normal file
38
docker-compose.postgres.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
version: '3.8'
|
||||
services:
|
||||
jellyseerr:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.local
|
||||
ports:
|
||||
- '5055:5055'
|
||||
environment:
|
||||
DB_TYPE: 'postgres' # Which DB engine to use. The default is "sqlite". To use postgres, this needs to be set to "postgres"
|
||||
DB_HOST: 'postgres' # The host (url) of the database
|
||||
DB_PORT: '5432' # The port to connect to
|
||||
DB_USER: 'jellyseerr' # Username used to connect to the database
|
||||
DB_PASS: 'jellyseerr' # Password of the user used to connect to the database
|
||||
DB_NAME: 'jellyseerr' # The name of the database to connect to
|
||||
DB_LOG_QUERIES: 'false' # Whether to log the DB queries for debugging
|
||||
DB_USE_SSL: 'false' # Whether to enable ssl for database connection
|
||||
volumes:
|
||||
- .:/app:rw,cached
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
depends_on:
|
||||
- postgres
|
||||
links:
|
||||
- postgres
|
||||
postgres:
|
||||
image: postgres
|
||||
environment:
|
||||
POSTGRES_USER: jellyseerr
|
||||
POSTGRES_PASSWORD: jellyseerr
|
||||
POSTGRES_DB: jellyseerr
|
||||
ports:
|
||||
- '5432:5432'
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
volumes:
|
||||
postgres:
|
||||
@@ -7,30 +7,34 @@ sidebar_position: 1
|
||||
|
||||
Welcome to the Jellyseerr Documentation.
|
||||
|
||||
**Jellyseerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||
|
||||
## Features
|
||||
|
||||
- **Full Jellyfin/Emby/Plex integration**. Login and manage user access with Jellyfin/Emby/Plex.
|
||||
- **Syncs to your Jellyfin/Emby/Plex library** to show what titles you already have.
|
||||
- Supports Movies, Shows and Mixed Libraries.
|
||||
- **Integrates with Sonarr and Radarr**. With more services to come in the future.
|
||||
- Optionally set **Override rules** for requests to match with your defined conditions.
|
||||
- **Easy to use request system** allowing users to request individual seasons or movies in a friendly, clean UI.
|
||||
- **Simple request management UI**. Don't dig through the app to approve recent requests.
|
||||
- **Mobile-friendly design**, for when you need to approve requests on the go.
|
||||
- Granular permission system.
|
||||
- Localization into other languages.
|
||||
- Support for **PostgreSQL** and **SQLite** databases.
|
||||
- Support for various notification agents.
|
||||
- Easily **Watchlist** or **Blacklist** media.
|
||||
- More features to come!
|
||||
|
||||
## Motivation
|
||||
|
||||
The primary motivation for starting this project was to add support for Jellyfin and Emby to Overseerr. As Overseerr is an incredibly performant and easy-to-use application, we wanted to bring that same experience to Jellyfin and Emby users. Thus, **Jellyseerr** was born.
|
||||
|
||||
This application is designed to be a **one-stop-shop** for all your media requests. It is designed to be a **simple, easy-to-use** application that allows users to request media to be added to your Jellyfin/Emby/Plex server.
|
||||
The primary motivation for starting Jellyseerr was to bring Jellyfin and Emby support to Overseerr. However, over time, **Jellyseerr** has evolved into its own distinct application with unique features. Designed as a one-stop shop for media requests, it offers a simple, easy-to-use experience for managing requests on Jellyfin, Emby, and Plex servers.
|
||||
|
||||
## We need your help!
|
||||
|
||||
[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr, with a heavy focus on Jellyfin and Emby integration.
|
||||
[Overseerr](https://github.com/sct/overseerr) is an ambitious project where the original developers/contributors have already poured a lot of work into, and we wanted to build on top of that.
|
||||
[Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is an ambitious project where developers/contributors poured a lot of work into, and that builds on top of [Overseerr](https://github.com/sct/overseerr). And we have a lot more to do as well.
|
||||
|
||||
We also have poured a lot of work into this project, and we have a lot more to do as well. We need your valuable feedback and help to find and fix bugs. Also, with Jellyseerr being an open-source project, anyone is welcome to contribute. We also encourage you to contribute to Overseerr as well.
|
||||
We value your feedback and support in identifying and fixing bugs to make Jellyseerr even better. As an open-source project, we welcome contributions from everyone. While Jellyseerr has diverged from Overseerr and evolved into its own unique application, we still encourage contributions to Overseerr, as it played a crucial role in inspiring what Jellyseerr has become today.
|
||||
|
||||
Contribution includes building new features, patching bugs, translating the application, or even just writing documentation.
|
||||
|
||||
|
||||
82
docs/extending-jellyseerr/database-config.mdx
Normal file
82
docs/extending-jellyseerr/database-config.mdx
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Configuring the Database (Advanced)
|
||||
description: Configure the database for Jellyseerr
|
||||
sidebar_position: 2
|
||||
---
|
||||
# Configuring the Database
|
||||
|
||||
Jellyseerr supports SQLite and PostgreSQL. The database connection can be configured using the following environment variables:
|
||||
|
||||
## SQLite Options
|
||||
|
||||
If you want to use SQLite, you can simply set the `DB_TYPE` environment variable to `sqlite`. This is the default configuration so even if you don't set any other options, SQLite will be used.
|
||||
|
||||
```dotenv
|
||||
DB_TYPE="sqlite" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||
CONFIG_DIRECTORY="config" # (optional) The path to the config directory where the db file is stored. The default is "config".
|
||||
DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false".
|
||||
```
|
||||
|
||||
## PostgreSQL Options
|
||||
|
||||
### TCP Connection
|
||||
|
||||
If your PostgreSQL server is configured to accept TCP connections, you can specify the host and port using the `DB_HOST` and `DB_PORT` environment variables. This is useful for remote connections where the server uses a network host and port.
|
||||
|
||||
```dotenv
|
||||
DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||
DB_HOST="localhost" # (optional) The host (URL) of the database. The default is "localhost".
|
||||
DB_PORT="5432" # (optional) The port to connect to. The default is "5432".
|
||||
DB_USER= # (required) Username used to connect to the database.
|
||||
DB_PASS= # (required) Password of the user used to connect to the database.
|
||||
DB_NAME="jellyseerr" # (optional) The name of the database to connect to. The default is "jellyseerr".
|
||||
DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false".
|
||||
```
|
||||
|
||||
### Unix Socket Connection
|
||||
|
||||
If your PostgreSQL server is configured to accept Unix socket connections, you can specify the path to the socket directory using the `DB_SOCKET_PATH` environment variable. This is useful for local connections where the server uses a Unix socket.
|
||||
|
||||
```dotenv
|
||||
DB_TYPE="postgres" # Which DB engine to use, either "sqlite" or "postgres". The default is "sqlite".
|
||||
DB_SOCKET_PATH="/var/run/postgresql" # (required) The path to the PostgreSQL Unix socket directory.
|
||||
DB_USER= # (required) Username used to connect to the database.
|
||||
DB_PASS= # (optional) Password of the user used to connect to the database, depending on the server's authentication configuration.
|
||||
DB_NAME="jellyseerr" # (optional) The name of the database to connect to. The default is "jellyseerr".
|
||||
DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging. The default is "false".
|
||||
```
|
||||
|
||||
### SSL configuration
|
||||
|
||||
The following options can be used to further configure ssl. Certificates can be provided as a string or a file path, with the string version taking precedence.
|
||||
|
||||
```dotenv
|
||||
DB_USE_SSL="false" # (optional) Whether to enable ssl for database connection. This must be "true" to use the other ssl options. The default is "false".
|
||||
DB_SSL_REJECT_UNAUTHORIZED="true" # (optional) Whether to reject ssl connections with unverifiable certificates i.e. self-signed certificates without providing the below settings. The default is "true".
|
||||
DB_SSL_CA= # (optional) The CA certificate to verify the connection, provided as a string. The default is "".
|
||||
DB_SSL_CA_FILE= # (optional) The path to a CA certificate to verify the connection. The default is "".
|
||||
DB_SSL_KEY= # (optional) The private key for the connection in PEM format, provided as a string. The default is "".
|
||||
DB_SSL_KEY_FILE= # (optinal) Path to the private key for the connection in PEM format. The default is "".
|
||||
DB_SSL_CERT= # (optional) Certificate chain in pem format for the private key, provided as a string. The default is "".
|
||||
DB_SSL_CERT_FILE= # (optional) Path to certificate chain in pem format for the private key. The default is "".
|
||||
```
|
||||
---
|
||||
|
||||
### Migrating from SQLite to PostgreSQL
|
||||
|
||||
1. Set up your PostgreSQL database and configure Jellyseerr to use it
|
||||
2. Run Jellyseerr to create the tables in the PostgreSQL database
|
||||
3. Stop Jellyseerr
|
||||
4. Run the following command to export the data from the SQLite database and import it into the PostgreSQL database:
|
||||
:::info
|
||||
Edit the postgres connection string to match your setup.
|
||||
|
||||
If you don't have or don't want to use docker, you can build the working pgloader version [in this PR](https://github.com/dimitri/pgloader/pull/1531) from source and use the same options as below.
|
||||
:::
|
||||
:::caution
|
||||
The most recent release of pgloader has an issue quoting the table columns. Use the version in the docker container to avoid this issue.
|
||||
:::
|
||||
```bash
|
||||
docker run --rm -v config/db.sqlite3:/db.sqlite3:ro ghcr.io/ralgar/pgloader:pr-1531 pgloader --with "quote identifiers" --with "data only" /db.sqlite3 postgresql://{{DB_USER}}:{{DB_PASS}}@{{DB_HOST}}:{{DB_PORT}}/{{DB_NAME}}
|
||||
```
|
||||
5. Start Jellyseerr
|
||||
@@ -6,13 +6,15 @@ sidebar_position: 2
|
||||
# Build from Source (Advanced)
|
||||
:::warning
|
||||
This method is not recommended for most users. It is intended for advanced users who are familiar with managing their own server infrastructure.
|
||||
|
||||
Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresql-options) for details on how to configure your database.
|
||||
:::
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
### Prerequisites
|
||||
- [Node.js 20.x](https://nodejs.org/en/download/)
|
||||
- [Node.js 22.x](https://nodejs.org/en/download/)
|
||||
- [Pnpm 9.x](https://pnpm.io/installation)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
@@ -26,7 +28,7 @@ sudo mkdir -p /opt/jellyseerr && cd /opt/jellyseerr
|
||||
```bash
|
||||
git clone https://github.com/Fallenbagel/jellyseerr.git
|
||||
cd jellyseerr
|
||||
git checkout develop # by default, you are on the develop branch so this step is not necessary
|
||||
git checkout main
|
||||
```
|
||||
3. Install the dependencies:
|
||||
```bash
|
||||
@@ -58,9 +60,6 @@ PORT=5055
|
||||
## specify on which interface to listen, by default jellyseerr listens on all interfaces
|
||||
#HOST=127.0.0.1
|
||||
|
||||
## Uncomment if your media server is emby instead of jellyfin.
|
||||
# JELLYFIN_TYPE=emby
|
||||
|
||||
## Uncomment if you want to force Node.js to resolve IPv4 before IPv6 (advanced users only)
|
||||
# FORCE_IPV4_FIRST=true
|
||||
```
|
||||
@@ -203,7 +202,7 @@ cd C:\jellyseerr
|
||||
2. Clone the Jellyseerr repository and checkout the develop branch:
|
||||
```powershell
|
||||
git clone https://github.com/Fallenbagel/jellyseerr.git .
|
||||
git checkout develop # by default, you are on the develop branch so this step is not necessary
|
||||
git checkout main
|
||||
```
|
||||
3. Install the dependencies:
|
||||
```powershell
|
||||
|
||||
@@ -7,6 +7,8 @@ sidebar_position: 1
|
||||
:::info
|
||||
This is the recommended method for most users.
|
||||
Details on how to install Docker can be found on the [official Docker website](https://docs.docker.com/get-docker/).
|
||||
|
||||
Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresql-options) for details on how to configure your database.
|
||||
:::
|
||||
|
||||
## Unix (Linux, macOS)
|
||||
@@ -145,6 +147,16 @@ Then, create and start the Jellyseerr container:
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
```bash
|
||||
docker run -d --name jellyseerr -e LOG_LEVEL=debug -e TZ=Asia/Tashkent -p 5055:5055 -v "jellyseerr-data:/app/config" --restart unless-stopped fallenbagel/jellyseerr:latest
|
||||
```
|
||||
|
||||
#### Updating:
|
||||
Pull the latest image:
|
||||
```bash
|
||||
docker compose pull jellyseerr
|
||||
```
|
||||
Then, restart all services defined in the Compose file:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
@@ -167,6 +179,16 @@ services:
|
||||
volumes:
|
||||
jellyseerr-data:
|
||||
external: true
|
||||
```
|
||||
|
||||
#### Updating:
|
||||
Pull the latest image:
|
||||
```bash
|
||||
docker compose pull jellyseerr
|
||||
```
|
||||
Then, restart all services defined in the Compose file:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
@@ -185,3 +207,6 @@ Docker on Windows works differently than it does on Linux; it runs Docker inside
|
||||
**If you must run Docker on Windows, you should put the `/app/config` directory mount inside the VM and not on the Windows host.** (This also applies to other containers with SQLite databases.)
|
||||
|
||||
Named volumes, like in the example commands above, are automatically mounted inside the VM. Therefore the warning on the setup about the `/app/config` folder being incorrectly mounted page should be ignored.
|
||||
:::
|
||||
|
||||
|
||||
|
||||
21
docs/getting-started/kubernetes.mdx
Normal file
21
docs/getting-started/kubernetes.mdx
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Kubernetes
|
||||
description: Install Jellyseerr in Kubernetes
|
||||
sidebar_position: 5
|
||||
---
|
||||
# Kubernetes
|
||||
:::info
|
||||
This method is not recommended for most users. It is intended for advanced users who are using Kubernetes.
|
||||
:::
|
||||
|
||||
## Installation
|
||||
```console
|
||||
helm install jellyseerr oci://ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart
|
||||
```
|
||||
Helm values can be found in the Jellyseerr repository under [charts/jellyseerr-chart/README.md](https://github.com/Fallenbagel/jellyseerr/tree/develop/charts/jellyseerr-chart).
|
||||
|
||||
Verify the signature with [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) (replace [tag], with the TAG you want to verify) :
|
||||
```console
|
||||
cosign verify ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart:[tag] --certificate-identity=https://github.com/Fallenbagel/jellyseerr/.github/workflows/helm.yml@refs/heads/main --certificate-oidc-issuer=https://token.ac
|
||||
tions.githubusercontent.com
|
||||
```
|
||||
@@ -6,6 +6,8 @@ sidebar_position: 3
|
||||
|
||||
import { JellyseerrVersion, NixpkgVersion } from '@site/src/components/JellyseerrVersion';
|
||||
import Admonition from '@theme/Admonition';
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Nix Package Manager (Advanced)
|
||||
:::info
|
||||
@@ -13,22 +15,55 @@ This method is not recommended for most users. It is intended for advanced users
|
||||
:::
|
||||
|
||||
export const VersionMismatchWarning = () => {
|
||||
const jellyseerrVersion = JellyseerrVersion();
|
||||
const nixpkgVersion = NixpkgVersion();
|
||||
let jellyseerrVersion = null;
|
||||
let nixpkgVersions = null;
|
||||
try {
|
||||
jellyseerrVersion = JellyseerrVersion();
|
||||
nixpkgVersions = NixpkgVersion();
|
||||
} catch (err) {
|
||||
return (
|
||||
<Admonition type="error">
|
||||
Failed to load version information. Error: {err.message || JSON.stringify(err)}
|
||||
</Admonition>
|
||||
);
|
||||
}
|
||||
|
||||
const isUpToDate = jellyseerrVersion === nixpkgVersion;
|
||||
if (!nixpkgVersions || nixpkgVersions.error) {
|
||||
return (
|
||||
<Admonition type="error">
|
||||
Failed to fetch Nixpkg versions: {nixpkgVersions?.error || 'Unknown error'}
|
||||
</Admonition>
|
||||
);
|
||||
}
|
||||
|
||||
const isUnstableUpToDate = jellyseerrVersion === nixpkgVersions.unstable;
|
||||
const isStableUpToDate = jellyseerrVersion === nixpkgVersions.stable;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isUpToDate ? (
|
||||
<Admonition type="warning">
|
||||
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to <a href="#overriding-the-package-derivation">override the package derivation</a>.
|
||||
</Admonition>
|
||||
) : (
|
||||
<Admonition type="success">
|
||||
The <a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/servers/jellyseerr/default.nix#L14">upstream Jellyseerr Nix Package (v{nixpkgVersion})</a> is <b>up-to-date</b> with <b>Jellyseerr v{jellyseerrVersion}</b>.
|
||||
</Admonition>
|
||||
)}
|
||||
<>
|
||||
{!isStableUpToDate ? (
|
||||
<Admonition type="warning">
|
||||
The{' '}
|
||||
<a href="https://github.com/NixOS/nixpkgs/blob/nixos-24.11/pkgs/servers/jellyseerr/default.nix#L14">
|
||||
upstream Jellyseerr Nix Package (v{nixpkgVersions.stable})
|
||||
</a>{' '}
|
||||
is not <b>up-to-date</b>. If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>,{' '}
|
||||
{isUnstableUpToDate ? (
|
||||
<>
|
||||
consider using the{' '}
|
||||
<a href="https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/by-name/je/jellyseerr/package.nix">
|
||||
unstable package
|
||||
</a>{' '}
|
||||
instead.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
you will need to{' '}
|
||||
<a href="#overriding-the-package-derivation">override the package derivation</a>.
|
||||
</>
|
||||
)}
|
||||
</Admonition>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -48,6 +83,8 @@ To get up and running with jellyseerr using Nix, you can add the following to yo
|
||||
|
||||
If you want more advanced configuration options, you can use the following:
|
||||
|
||||
<Tabs groupId="nixpkg-methods" queryString>
|
||||
<TabItem value="default" label="Default Configurations">
|
||||
```nix
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
@@ -56,53 +93,20 @@ If you want more advanced configuration options, you can use the following:
|
||||
enable = true;
|
||||
port = 5055;
|
||||
openFirewall = true;
|
||||
package = pkgs.jellyseerr; # Use the unstable package if stable is not up-to-date
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
After adding the configuration to your `configuration.nix`, you can run the following command to install jellyseerr:
|
||||
|
||||
```bash
|
||||
nixos-rebuild switch
|
||||
```
|
||||
After rebuild is complete jellyseerr should be running, verify that it is with the following command.
|
||||
```bash
|
||||
systemctl status jellyseerr
|
||||
```
|
||||
|
||||
:::info
|
||||
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
||||
:::
|
||||
|
||||
|
||||
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
|
||||
## Overriding the package derivation
|
||||
export const VersionMatch = () => {
|
||||
const jellyseerrVersion = JellyseerrVersion();
|
||||
const nixpkgVersion = NixpkgVersion();
|
||||
|
||||
const code = `{ config, pkgs, ... }:
|
||||
</TabItem>
|
||||
<TabItem value="custom" label="Database Configurations">
|
||||
In order to use postgres, you will need to add override the default module of jellyseerr with the following as the current default module is not compatible with postgres:
|
||||
```nix
|
||||
{
|
||||
nixpkgs.config.packageOverrides = pkgs: {
|
||||
jellyseerr = pkgs.jellyseerr.overrideAttrs (oldAttrs: rec {
|
||||
version = "${jellyseerrVersion}";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
rev = "v\${version}";
|
||||
sha256 = pkgs.lib.fakeSha256;
|
||||
};
|
||||
|
||||
offlineCache = pkgs.fetchYarnDeps {
|
||||
sha256 = pkgs.lib.fakeSha256;
|
||||
};
|
||||
});
|
||||
};
|
||||
}`;
|
||||
|
||||
const module = `{ config, pkgs, lib, ... }:
|
||||
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.jellyseerr;
|
||||
@@ -113,28 +117,65 @@ in
|
||||
disabledModules = [ "services/misc/jellyseerr.nix" ];
|
||||
|
||||
options.services.jellyseerr = {
|
||||
enable = mkEnableOption (mdDoc ''Jellyseerr, a requests manager for Jellyfin'');
|
||||
enable = mkEnableOption ''Jellyseerr, a requests manager for Jellyfin'';
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = mdDoc ''Open port in the firewall for the Jellyseerr web interface.'';
|
||||
description = ''Open port in the firewall for the Jellyseerr web interface.'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 5055;
|
||||
description = mdDoc ''The port which the Jellyseerr web UI should listen to.'';
|
||||
description = ''The port which the Jellyseerr web UI should listen to.'';
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.jellyseerr;
|
||||
defaultText = literalExpression "pkgs.jellyseerr";
|
||||
description = lib.mdDoc ''
|
||||
Jellyseerr package to use.
|
||||
'';
|
||||
type = types.package;
|
||||
default = pkgs.jellyseerr;
|
||||
defaultText = literalExpression "pkgs.jellyseerr";
|
||||
description = ''
|
||||
Jellyseerr package to use.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseConfig = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {
|
||||
type = "sqlite";
|
||||
configDirectory = "config";
|
||||
logQueries = "false";
|
||||
};
|
||||
description = ''
|
||||
Database configuration. For "sqlite", only "type", "configDirectory", and "logQueries" are relevant.
|
||||
For "postgres", include host, port, user, pass, name, and optionally socket.
|
||||
Example:
|
||||
{
|
||||
type = "postgres";
|
||||
socket = "/run/postgresql";
|
||||
user = "jellyseerr";
|
||||
name = "jellyseerr";
|
||||
logQueries = "false";
|
||||
}
|
||||
or
|
||||
{
|
||||
type = "postgres";
|
||||
host = "localhost";
|
||||
port = "5432";
|
||||
user = "dbuser";
|
||||
pass = "password";
|
||||
name = "jellyseerr";
|
||||
logQueries = "false";
|
||||
}
|
||||
or
|
||||
{
|
||||
type = "sqlite";
|
||||
configDirectory = "config";
|
||||
logQueries = "false";
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
@@ -142,14 +183,29 @@ in
|
||||
description = "Jellyseerr, a requests manager for Jellyfin";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment.PORT = toString cfg.port;
|
||||
environment =
|
||||
let
|
||||
dbConfig = cfg.databaseConfig;
|
||||
in
|
||||
{
|
||||
PORT = toString cfg.port;
|
||||
DB_TYPE = toString dbConfig.type;
|
||||
CONFIG_DIRECTORY = toString dbConfig.configDirectory or "";
|
||||
DB_LOG_QUERIES = toString dbConfig.logQueries;
|
||||
DB_HOST = if dbConfig.type == "postgres" && !(hasAttr "socket" dbConfig) then toString dbConfig.host or "" else "";
|
||||
DB_PORT = if dbConfig.type == "postgres" && !(hasAttr "socket" dbConfig) then toString dbConfig.port or "" else "";
|
||||
DB_SOCKET_PATH = if dbConfig.type == "postgres" && hasAttr "socket" dbConfig then toString dbConfig.socket or "" else "";
|
||||
DB_USER = if dbConfig.type == "postgres" then toString dbConfig.user or "" else "";
|
||||
DB_PASS = if dbConfig.type == "postgres" then toString dbConfig.pass or "" else "";
|
||||
DB_NAME = if dbConfig.type == "postgres" then toString dbConfig.name or "" else "";
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "exec";
|
||||
StateDirectory = "jellyseerr";
|
||||
WorkingDirectory = "\${cfg.package}/libexec/jellyseerr/deps/jellyseerr";
|
||||
WorkingDirectory = "${cfg.package}/libexec/jellyseerr";
|
||||
DynamicUser = true;
|
||||
ExecStart = "\${cfg.package}/bin/jellyseerr";
|
||||
BindPaths = [ "/var/lib/jellyseerr/:\${cfg.package}/libexec/jellyseerr/deps/jellyseerr/config/" ];
|
||||
ExecStart = "${cfg.package}/bin/jellyseerr";
|
||||
BindPaths = [ "/var/lib/jellyseerr/:${cfg.package}/libexec/jellyseerr/config/" ];
|
||||
Restart = "on-failure";
|
||||
ProtectHome = true;
|
||||
ProtectSystem = "strict";
|
||||
@@ -169,57 +225,47 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ cfg.port ];
|
||||
};
|
||||
networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };
|
||||
};
|
||||
}`;
|
||||
|
||||
const configuration = `{ config, pkgs, ... }:
|
||||
}
|
||||
```
|
||||
Then, import the module into your `configuration.nix`:
|
||||
```nix
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [ ./jellyseerr-module.nix ]
|
||||
imports = [ ./modules/jellyseerr.nix ];
|
||||
|
||||
services.jellyseerr = {
|
||||
enable = true;
|
||||
port = 5055;
|
||||
openFirewall = true;
|
||||
package = (pkgs.callPackage (import ../../../pkgs/jellyseerr) { });
|
||||
services.jellyseerr = {
|
||||
enable = true;
|
||||
port = 5055;
|
||||
openFirewall = true;
|
||||
package = pkgs.unstable.jellyseerr; # use the unstable package if stable is not up-to-date
|
||||
databaseConfig = {
|
||||
type = "postgres";
|
||||
host = "localhost"; # or socket: "/run/postgresql"
|
||||
port = "5432"; # if using socket, this is not needed
|
||||
user = "jellyseerr";
|
||||
pass = "jellyseerr";
|
||||
name = "jellyseerr";
|
||||
logQueries = "false";
|
||||
};
|
||||
}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
const isUpToDate = jellyseerrVersion === nixpkgVersion;
|
||||
After adding the configuration to your `configuration.nix`, you can run the following command to install jellyseerr:
|
||||
|
||||
return (
|
||||
<>
|
||||
{isUpToDate ? (
|
||||
<>
|
||||
<p>The latest version of Jellyseerr <strong>({jellyseerrVersion})</strong> and the Jellyseerr nixpkg package version <strong>({nixpkgVersion})</strong> is <strong>up-to-date</strong>.</p>
|
||||
<p>There is no need to override the package derivation.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>The latest version of Jellyseerr <strong>({jellyseerrVersion})</strong> and the Jellyseerr nixpkg version <strong>(v{nixpkgVersion})</strong> is <strong>out-of-date</strong>.
|
||||
If you want to use <b>Jellyseerr v{jellyseerrVersion}</b>, you will need to override the package derivation.</p>
|
||||
<p>In order to override the package derivation:</p>
|
||||
<ol>
|
||||
<li style={{ marginBottom: '1rem' }}>Grab the <a href="https://raw.githubusercontent.com/NixOS/nixpkgs/nixos-unstable/pkgs/servers/jellyseerr/default.nix">latest nixpkg derivation for Jellyseerr</a></li>
|
||||
<li style={{ marginBottom: '1rem' }}>Grab the latest <a href="https://raw.githubusercontent.com/Fallenbagel/jellyseerr/main/package.json">package.json</a> for Jellyseerr</li>
|
||||
<li style={{ marginBottom: '1rem' }}>Add it to the same directory as the nixpkg derivation</li>
|
||||
<li style={{ marginBottom: '1rem' }}>Update the `src` and `offlineCache` attributes in the nixpkg derivation:</li>
|
||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{code}</CodeBlock>
|
||||
<Admonition type="tip" style={{ marginBottom: '1rem' }}>You can replace the <b>sha256</b> with the actual hash that <b>nixos-rebuild</b> outputs when you run the command.</Admonition>
|
||||
<li style={{ marginBottom: '1rem' }}>Grab this module and import it in your `configuration.nix`</li>
|
||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{module}</CodeBlock>
|
||||
<Admonition type="tip" style={{ marginBottom: '1rem' }}>We are using a custom module because the upstream module does not have a package option.</Admonition>
|
||||
<li style={{ marginBottom: '1rem' }}>Call the new package in your `configuration.nix`</li>
|
||||
<CodeBlock className="language-nix" style={{ marginBottom: '1rem' }}>{configuration}</CodeBlock>
|
||||
</ol>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
```bash
|
||||
nixos-rebuild switch
|
||||
```
|
||||
After rebuild is complete jellyseerr should be running, verify that it is with the following command.
|
||||
```bash
|
||||
systemctl status jellyseerr
|
||||
```
|
||||
|
||||
};
|
||||
|
||||
<VersionMatch />
|
||||
:::info
|
||||
You can now access Jellyseerr by visiting `http://localhost:5055` in your web browser.
|
||||
:::
|
||||
|
||||
|
||||
158
docs/troubleshooting.mdx
Normal file
158
docs/troubleshooting.mdx
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## [TMDB] failed to retrieve/fetch XXX
|
||||
|
||||
### Option 1: Change your DNS servers
|
||||
|
||||
This error often comes from your Internet Service Provider (ISP) blocking TMDB API. The ISP may block the DNS resolution to the TMDB API hostname.
|
||||
|
||||
To fix this, you can change your DNS servers to a public DNS service like Google's DNS or Cloudflare's DNS:
|
||||
|
||||
<Tabs groupId="methods" queryString>
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
|
||||
Add the following to your `docker run` command to use Google's DNS:
|
||||
```bash
|
||||
--dns=8.8.8.8
|
||||
```
|
||||
or for Cloudflare's DNS:
|
||||
```bash
|
||||
--dns=1.1.1.1
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
Add the following to your `compose.yaml` to use Google's DNS:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
jellyseerr:
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
```
|
||||
or for Cloudflare's DNS:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
jellyseerr:
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="windows" label="Windows">
|
||||
|
||||
1. Open the Control Panel.
|
||||
2. Click on Network and Internet.
|
||||
3. Click on Network and Sharing Center.
|
||||
4. Click on Change adapter settings.
|
||||
5. Right-click the network interface connected to the internet and select Properties.
|
||||
6. Select Internet Protocol Version 4 (TCP/IPv4) and click Properties.
|
||||
7. Select Use the following DNS server addresses and enter `8.8.8.8` for Google's DNS or `1.1.1.1` for Cloudflare's DNS.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="linux" label="Linux">
|
||||
|
||||
1. Open a terminal.
|
||||
2. Edit the `/etc/resolv.conf` file with your favorite text editor.
|
||||
3. Add the following line to use Google's DNS:
|
||||
```bash
|
||||
nameserver 8.8.8.8
|
||||
```
|
||||
or for Cloudflare's DNS:
|
||||
|
||||
```bash
|
||||
nameserver 1.1.1.1
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Option 2: Force IPV4 resolution first
|
||||
|
||||
Sometimes there are configuration issues with IPV6 that prevent the hostname resolution from working correctly.
|
||||
|
||||
You can try to force the resolution to use IPV4 first by setting the `FORCE_IPV4_FIRST` environment variable to `true`:
|
||||
|
||||
<Tabs groupId="methods" queryString>
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
|
||||
Add the following to your `docker run` command:
|
||||
```bash
|
||||
-e "FORCE_IPV4_FIRST=true"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
Add the following to your `compose.yaml`:
|
||||
```yaml
|
||||
---
|
||||
services:
|
||||
jellyseerr:
|
||||
environment:
|
||||
- FORCE_IPV4_FIRST=true
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Option 3: Use Jellyseerr through a proxy
|
||||
|
||||
If you can't change your DNS servers or force IPV4 resolution, you can use Jellyseerr through a proxy.
|
||||
|
||||
In some places (like China), the ISP blocks not only the DNS resolution but also the connection to the TMDB API.
|
||||
|
||||
You can configure Jellyseerr to use a proxy with the [HTTP(S) Proxy](/using-jellyseerr/settings/general#https-proxy) setting.
|
||||
|
||||
### Option 4: Check that your server can reach TMDB API
|
||||
|
||||
Make sure that your server can reach the TMDB API by running the following command:
|
||||
|
||||
<Tabs groupId="methods" queryString>
|
||||
<TabItem value="docker-cli" label="Docker CLI">
|
||||
|
||||
```bash
|
||||
docker exec -it jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
```bash
|
||||
docker compose exec jellyseerr sh -c "apk update && apk add curl && curl -L https://api.themoviedb.org"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="linux" label="Linux">
|
||||
|
||||
In a terminal:
|
||||
```bash
|
||||
curl -L https://api.themoviedb.org
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="windows" label="Windows">
|
||||
|
||||
In a PowerShell window:
|
||||
```powershell
|
||||
(Invoke-WebRequest -Uri "https://api.themoviedb.org" -Method Get).Content
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
If you can't get a response, then your server can't reach the TMDB API.
|
||||
This is usually due to a network configuration issue or a firewall blocking the connection.
|
||||
93
docs/using-jellyseerr/backups.md
Normal file
93
docs/using-jellyseerr/backups.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: Backups
|
||||
description: Understand which data you should back up.
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Which data does Jellyseerr save and where?
|
||||
|
||||
## Settings
|
||||
|
||||
All configurations from the **Settings** panel in the Jellyseerr web UI are saved, including integrations with Radarr, Sonarr, Jellyfin, Plex, and notification settings.
|
||||
These settings are stored in the `settings.json` file located in the Jellyseerr data folder.
|
||||
|
||||
## User Data
|
||||
|
||||
Apart from the settings, all other data—including user accounts, media requests, blacklist etc. are stored in the database (either SQLite or PostgreSQL).
|
||||
|
||||
# Backup
|
||||
|
||||
### SQLite
|
||||
|
||||
If your backup system uses filesystem snapshots (such as Kubernetes with Volsync), you can directly back up the Jellyseerr data folder.
|
||||
Otherwise, you need to stop the Jellyseerr application and back up the `config` folder.
|
||||
|
||||
For advanced users, it's possible to back up the database without stopping the application by using the [SQLite CLI](https://www.sqlite.org/download.html). Run the following command to create a backup:
|
||||
|
||||
```bash
|
||||
sqlite3 db/db.sqlite3 ".backup '/tmp/jellyseerr_db.sqlite3.bak'"
|
||||
```
|
||||
|
||||
Then, copy the `/tmp/jellyseerr_dump.sqlite3.bak` file to your desired backup location.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
You can back up the `config` folder and dump the PostgreSQL database without stopping the Jellyseerr application.
|
||||
|
||||
Install [postgresql-client](https://www.postgresql.org/download/) and run the following command to create a backup (just replace the placeholders):
|
||||
|
||||
:::info
|
||||
Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below.
|
||||
|
||||
-h, --host=HOSTNAME database server host or socket directory
|
||||
|
||||
-p, --port=PORT database server port number
|
||||
:::
|
||||
|
||||
```bash
|
||||
pg_dump -U <database_user> -d <database_name> -f /tmp/jellyseerr_db.sql
|
||||
```
|
||||
|
||||
# Restore
|
||||
|
||||
### SQLite
|
||||
|
||||
After restoring your `db/db.sqlite3` file and, optionally, the `settings.json` file, the `config` folder structure should look like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── cache <-- Optional
|
||||
├── db
|
||||
│ └── db.sqlite3
|
||||
├── logs <-- Optional
|
||||
└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr)
|
||||
```
|
||||
|
||||
Once the files are restored, start the Jellyseerr application.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
Install the [PostgreSQL client](https://www.postgresql.org/download/) and restore the PostgreSQL database using the following command (replace the placeholders accordingly):
|
||||
|
||||
:::info
|
||||
Depending on how your PostgreSQL instance is configured, you may need to add these options to the command below.
|
||||
|
||||
-h, --host=HOSTNAME database server host or socket directory
|
||||
|
||||
-p, --port=PORT database server port number
|
||||
:::
|
||||
|
||||
```bash
|
||||
pg_restore -U <database_user> -d <database_name> /tmp/jellyseerr_db.sql
|
||||
```
|
||||
|
||||
Optionally, restore the `settings.json` file. The `config` folder structure should look like this:
|
||||
|
||||
```
|
||||
.
|
||||
├── cache <-- Optional
|
||||
├── logs <-- Optional
|
||||
└── settings.json <-- Optional (required if you want to avoid reconfiguring Jellyseerr)
|
||||
```
|
||||
|
||||
Once the database and files are restored, start the Jellyseerr application.
|
||||
@@ -58,9 +58,9 @@ You should enable this if you are having issues with loading images directly fro
|
||||
|
||||
Set the default display language for Jellyseerr. Users can override this setting in their user settings.
|
||||
|
||||
## Discover Region & Discover Language
|
||||
## Discover Region, Discover Language & Streaming Region
|
||||
|
||||
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. Users can override these global settings by configuring these same options in their user settings.
|
||||
These settings filter content shown on the "Discover" home page based on regional availability and original language, respectively. The Streaming Region filters the available streaming providers on the media page. Users can override these global settings by configuring these same options in their user settings.
|
||||
|
||||
## Hide Available Media
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Users can override the [global display language](/using-jellyseerr/settings/gene
|
||||
|
||||
### Discover Region & Discover Language
|
||||
|
||||
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region--discover-language) to suit their own preferences.
|
||||
Users can override the [global filter settings](/using-jellyseerr/settings/general#discover-region-discover-language--streaming-region) to suit their own preferences.
|
||||
|
||||
### Movie Request Limit & Series Request Limit
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const config: Config = {
|
||||
baseUrl: '/',
|
||||
trailingSlash: false,
|
||||
|
||||
organizationName: 'Fallenbagel',
|
||||
organizationName: 'fallenbagel',
|
||||
projectName: 'Jellyseerr',
|
||||
deploymentBranch: 'gh-pages',
|
||||
|
||||
@@ -32,7 +32,7 @@ const config: Config = {
|
||||
routeBasePath: '/',
|
||||
path: '../docs',
|
||||
editUrl:
|
||||
'https://github.com/Fallenbagel/jellyseerr/edit/develop/docs/',
|
||||
'https://github.com/fallenbagel/jellyseerr/edit/develop/docs/',
|
||||
},
|
||||
blog: false,
|
||||
pages: false,
|
||||
@@ -70,7 +70,7 @@ const config: Config = {
|
||||
},
|
||||
items: [
|
||||
{
|
||||
href: 'https://github.com/Fallenbagel/jellyseerr',
|
||||
href: 'https://github.com/fallenbagel/jellyseerr',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
|
||||
@@ -47,6 +47,6 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
"node": ">=22.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,25 +26,37 @@ export const JellyseerrVersion = () => {
|
||||
};
|
||||
|
||||
export const NixpkgVersion = () => {
|
||||
const [version, setVersion] = useState(null);
|
||||
const [versions, setVersions] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchVersion = async () => {
|
||||
try {
|
||||
const url =
|
||||
'https://raw.githubusercontent.com/NixOS/nixpkgs/nixos-unstable/pkgs/servers/jellyseerr/default.nix';
|
||||
const response = await fetch(url);
|
||||
const data = await response.text();
|
||||
const unstableUrl =
|
||||
'https://raw.githubusercontent.com/NixOS/nixpkgs/refs/heads/nixos-unstable/pkgs/by-name/je/jellyseerr/package.nix';
|
||||
const stableUrl =
|
||||
'https://raw.githubusercontent.com/NixOS/nixpkgs/refs/heads/nixos-24.11/pkgs/servers/jellyseerr/default.nix';
|
||||
|
||||
const [unstableResponse, stableResponse] = await Promise.all([
|
||||
fetch(unstableUrl),
|
||||
fetch(stableUrl),
|
||||
]);
|
||||
|
||||
const unstableData = await unstableResponse.text();
|
||||
const stableData = await stableResponse.text();
|
||||
|
||||
const versionRegex = /version\s*=\s*"([^"]+)"/;
|
||||
const match = data.match(versionRegex);
|
||||
if (match && match[1]) {
|
||||
setVersion(match[1]);
|
||||
} else {
|
||||
setError('0.0.0');
|
||||
}
|
||||
|
||||
const unstableMatch = unstableData.match(versionRegex);
|
||||
const stableMatch = stableData.match(versionRegex);
|
||||
|
||||
const unstableVersion =
|
||||
unstableMatch && unstableMatch[1] ? unstableMatch[1] : '0.0.0';
|
||||
const stableVersion =
|
||||
stableMatch && stableMatch[1] ? stableMatch[1] : '0.0.0';
|
||||
|
||||
setVersions({ unstable: unstableVersion, stable: stableVersion });
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
@@ -63,5 +75,5 @@ export const NixpkgVersion = () => {
|
||||
return { error };
|
||||
}
|
||||
|
||||
return version;
|
||||
return versions;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
{ hostname: 'gravatar.com' },
|
||||
{ hostname: 'image.tmdb.org' },
|
||||
{ hostname: 'artworks.thetvdb.com' },
|
||||
{ hostname: 'plex.tv' },
|
||||
],
|
||||
},
|
||||
webpack(config) {
|
||||
|
||||
@@ -143,10 +143,12 @@ components:
|
||||
properties:
|
||||
locale:
|
||||
type: string
|
||||
region:
|
||||
discoverRegion:
|
||||
type: string
|
||||
originalLanguage:
|
||||
type: string
|
||||
streamingRegion:
|
||||
type: string
|
||||
MainSettings:
|
||||
type: object
|
||||
properties:
|
||||
@@ -162,12 +164,6 @@ components:
|
||||
applicationUrl:
|
||||
type: string
|
||||
example: https://os.example.com
|
||||
trustProxy:
|
||||
type: boolean
|
||||
example: true
|
||||
csrfProtection:
|
||||
type: boolean
|
||||
example: false
|
||||
hideAvailable:
|
||||
type: boolean
|
||||
example: false
|
||||
@@ -186,6 +182,24 @@ components:
|
||||
defaultPermissions:
|
||||
type: number
|
||||
example: 32
|
||||
enableSpecialEpisodes:
|
||||
type: boolean
|
||||
example: false
|
||||
NetworkSettings:
|
||||
type: object
|
||||
properties:
|
||||
csrfProtection:
|
||||
type: boolean
|
||||
example: false
|
||||
forceIpv4First:
|
||||
type: boolean
|
||||
example: false
|
||||
dnsServers:
|
||||
type: string
|
||||
example: '1.1.1.1'
|
||||
trustProxy:
|
||||
type: boolean
|
||||
example: true
|
||||
PlexLibrary:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1336,6 +1350,8 @@ components:
|
||||
type: string
|
||||
chatId:
|
||||
type: string
|
||||
messageThreadId:
|
||||
type: string
|
||||
sendSilently:
|
||||
type: boolean
|
||||
PushbulletSettings:
|
||||
@@ -1819,6 +1835,9 @@ components:
|
||||
telegramChatId:
|
||||
type: string
|
||||
nullable: true
|
||||
telegramMessageThreadId:
|
||||
type: string
|
||||
nullable: true
|
||||
telegramSendSilently:
|
||||
type: boolean
|
||||
nullable: true
|
||||
@@ -1932,6 +1951,11 @@ components:
|
||||
type: string
|
||||
native_name:
|
||||
type: string
|
||||
OverrideRule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
type: apiKey
|
||||
@@ -2024,6 +2048,37 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MainSettings'
|
||||
/settings/network:
|
||||
get:
|
||||
summary: Get network settings
|
||||
description: Retrieves all network settings in a JSON object.
|
||||
tags:
|
||||
- settings
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/MainSettings'
|
||||
post:
|
||||
summary: Update network settings
|
||||
description: Updates network settings with the provided values.
|
||||
tags:
|
||||
- settings
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NetworkSettings'
|
||||
responses:
|
||||
'200':
|
||||
description: 'Values were sucessfully updated'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NetworkSettings'
|
||||
/settings/main/regenerate:
|
||||
post:
|
||||
summary: Get main settings with newly-generated API key
|
||||
@@ -3755,6 +3810,11 @@ paths:
|
||||
type: string
|
||||
enum: [created, updated, requests, displayname]
|
||||
default: created
|
||||
- in: query
|
||||
name: q
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: A JSON array of all users
|
||||
@@ -3871,7 +3931,7 @@ paths:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
jellyfinIds:
|
||||
jellyfinUserIds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -5436,6 +5496,13 @@ paths:
|
||||
type: string
|
||||
enum: [added, modified]
|
||||
default: added
|
||||
- in: query
|
||||
name: sortDirection
|
||||
schema:
|
||||
type: string
|
||||
enum: [asc, desc]
|
||||
nullable: true
|
||||
default: desc
|
||||
- in: query
|
||||
name: requestedBy
|
||||
schema:
|
||||
@@ -5486,7 +5553,7 @@ paths:
|
||||
- type: array
|
||||
items:
|
||||
type: number
|
||||
minimum: 1
|
||||
minimum: 0
|
||||
- type: string
|
||||
enum: [all]
|
||||
is4k:
|
||||
@@ -5592,7 +5659,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
minimum: 1
|
||||
minimum: 0
|
||||
is4k:
|
||||
type: boolean
|
||||
example: false
|
||||
@@ -6956,6 +7023,74 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/WatchProviderDetails'
|
||||
/overrideRule:
|
||||
get:
|
||||
summary: Get override rules
|
||||
description: Returns a list of all override rules with their conditions and settings
|
||||
tags:
|
||||
- overriderule
|
||||
responses:
|
||||
'200':
|
||||
description: Override rules returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/OverrideRule'
|
||||
post:
|
||||
summary: Create override rule
|
||||
description: Creates a new Override Rule from the request body.
|
||||
tags:
|
||||
- overriderule
|
||||
responses:
|
||||
'200':
|
||||
description: 'Values were successfully created'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/OverrideRule'
|
||||
/overrideRule/{ruleId}:
|
||||
put:
|
||||
summary: Update override rule
|
||||
description: Updates an Override Rule from the request body.
|
||||
tags:
|
||||
- overriderule
|
||||
parameters:
|
||||
- in: path
|
||||
name: ruleId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: 'Values were successfully updated'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/OverrideRule'
|
||||
delete:
|
||||
summary: Delete override rule by ID
|
||||
description: Deletes the override rule with the provided ruleId.
|
||||
tags:
|
||||
- overriderule
|
||||
parameters:
|
||||
- in: path
|
||||
name: ruleId
|
||||
required: true
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: Override rule successfully deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OverrideRule'
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- apiKey: []
|
||||
|
||||
10
package.json
10
package.json
@@ -42,6 +42,7 @@
|
||||
"@supercharge/request-ip": "1.2.0",
|
||||
"@svgr/webpack": "6.5.1",
|
||||
"@tanem/react-nprogress": "5.0.30",
|
||||
"@types/wink-jaro-distance": "^2.0.2",
|
||||
"ace-builds": "1.15.2",
|
||||
"bcrypt": "5.1.0",
|
||||
"bowser": "2.11.0",
|
||||
@@ -69,6 +70,7 @@
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.1",
|
||||
"openpgp": "5.7.0",
|
||||
"pg": "8.11.0",
|
||||
"plex-api": "5.3.2",
|
||||
"pug": "3.0.2",
|
||||
"react": "^18.3.1",
|
||||
@@ -96,6 +98,7 @@
|
||||
"typeorm": "0.3.11",
|
||||
"undici": "^6.20.1",
|
||||
"web-push": "3.5.0",
|
||||
"wink-jaro-distance": "^2.0.0",
|
||||
"winston": "3.8.2",
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"xml2js": "0.4.23",
|
||||
@@ -122,7 +125,7 @@
|
||||
"@types/express-session": "1.17.6",
|
||||
"@types/lodash": "4.14.191",
|
||||
"@types/mime": "3",
|
||||
"@types/node": "20.14.8",
|
||||
"@types/node": "22.10.5",
|
||||
"@types/node-schedule": "2.1.0",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/react": "^18.3.3",
|
||||
@@ -168,7 +171,7 @@
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0",
|
||||
"node": "^22.0.0",
|
||||
"pnpm": "^9.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
@@ -234,7 +237,8 @@
|
||||
"COMMIT_TAG": "$GIT_SHA"
|
||||
},
|
||||
"imageNames": [
|
||||
"fallenbagel/jellyseerr"
|
||||
"fallenbagel/jellyseerr",
|
||||
"ghcr.io/fallenbagel/jellyseerr"
|
||||
],
|
||||
"platforms": [
|
||||
"linux/amd64",
|
||||
|
||||
1951
pnpm-lock.yaml
generated
1951
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -1,3 +1,5 @@
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import type { RateLimitOptions } from '@server/utils/rateLimit';
|
||||
import rateLimit from '@server/utils/rateLimit';
|
||||
import type NodeCache from 'node-cache';
|
||||
@@ -34,6 +36,8 @@ class ExternalAPI {
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
|
||||
const settings = getSettings();
|
||||
|
||||
this.defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
@@ -42,6 +46,9 @@ class ExternalAPI {
|
||||
`${url.username}:${url.password}`
|
||||
).toString('base64')}`,
|
||||
}),
|
||||
...(settings.main.mediaServerType === MediaServerType.EMBY && {
|
||||
'Accept-Encoding': 'gzip',
|
||||
}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
@@ -62,10 +69,13 @@ class ExternalAPI {
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
headers,
|
||||
});
|
||||
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
@@ -74,10 +84,7 @@ class ExternalAPI {
|
||||
const url = this.formatUrl(endpoint, params);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
@@ -104,10 +111,13 @@ class ExternalAPI {
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: { ...this.params, ...params },
|
||||
headers,
|
||||
data,
|
||||
});
|
||||
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
@@ -117,10 +127,7 @@ class ExternalAPI {
|
||||
const response = await this.fetch(url, {
|
||||
method: 'POST',
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
headers,
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -148,10 +155,13 @@ class ExternalAPI {
|
||||
ttl?: number,
|
||||
config?: RequestInit
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
config: { ...this.params, ...params },
|
||||
data,
|
||||
headers,
|
||||
});
|
||||
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
if (cachedItem) {
|
||||
return cachedItem;
|
||||
@@ -161,10 +171,7 @@ class ExternalAPI {
|
||||
const response = await this.fetch(url, {
|
||||
method: 'PUT',
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -220,9 +227,11 @@ class ExternalAPI {
|
||||
config?: RequestInit,
|
||||
overwriteBaseUrl?: string
|
||||
): Promise<T> {
|
||||
const headers = { ...this.defaultHeaders, ...config?.headers };
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...params,
|
||||
headers,
|
||||
});
|
||||
const cachedItem = this.cache?.get<T>(cacheKey);
|
||||
|
||||
@@ -237,10 +246,7 @@ class ExternalAPI {
|
||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||
this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
headers,
|
||||
}).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
@@ -263,10 +269,7 @@ class ExternalAPI {
|
||||
const url = this.formatUrl(endpoint, params, overwriteBaseUrl);
|
||||
const response = await this.fetch(url, {
|
||||
...config,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...config?.headers,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
@@ -286,6 +289,14 @@ class ExternalAPI {
|
||||
return data;
|
||||
}
|
||||
|
||||
protected removeCache(endpoint: string, options?: Record<string, string>) {
|
||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||
...this.params,
|
||||
...options,
|
||||
});
|
||||
this.cache?.del(cacheKey);
|
||||
}
|
||||
|
||||
private formatUrl(
|
||||
endpoint: string,
|
||||
params?: Record<string, string>,
|
||||
@@ -310,13 +321,13 @@ class ExternalAPI {
|
||||
|
||||
private serializeCacheKey(
|
||||
endpoint: string,
|
||||
params?: Record<string, unknown>
|
||||
options?: Record<string, unknown>
|
||||
) {
|
||||
if (!params) {
|
||||
if (!options) {
|
||||
return `${this.baseUrl}${endpoint}`;
|
||||
}
|
||||
|
||||
return `${this.baseUrl}${endpoint}${JSON.stringify(params)}`;
|
||||
return `${this.baseUrl}${endpoint}${JSON.stringify(options)}`;
|
||||
}
|
||||
|
||||
private async getDataFromResponse(response: Response) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import xml2js from 'xml2js';
|
||||
|
||||
interface PlexAccountResponse {
|
||||
@@ -127,6 +128,11 @@ export interface PlexWatchlistItem {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface PlexWatchlistCache {
|
||||
etag: string;
|
||||
response: WatchlistResponse;
|
||||
}
|
||||
|
||||
class PlexTvAPI extends ExternalAPI {
|
||||
private authToken: string;
|
||||
|
||||
@@ -261,6 +267,11 @@ class PlexTvAPI extends ExternalAPI {
|
||||
items: PlexWatchlistItem[];
|
||||
}> {
|
||||
try {
|
||||
const watchlistCache = cacheManager.getCache('plexwatchlist');
|
||||
let cachedWatchlist = watchlistCache.data.get<PlexWatchlistCache>(
|
||||
this.authToken
|
||||
);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
'X-Plex-Container-Start': offset.toString(),
|
||||
'X-Plex-Container-Size': size.toString(),
|
||||
@@ -268,42 +279,62 @@ class PlexTvAPI extends ExternalAPI {
|
||||
const response = await this.fetch(
|
||||
`https://metadata.provider.plex.tv/library/sections/watchlist/all?${params.toString()}`,
|
||||
{
|
||||
headers: this.defaultHeaders,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...(cachedWatchlist?.etag
|
||||
? { 'If-None-Match': cachedWatchlist.etag }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = (await response.json()) as WatchlistResponse;
|
||||
|
||||
// If we don't recieve HTTP 304, the watchlist has been updated and we need to update the cache.
|
||||
if (response.status >= 200 && response.status <= 299) {
|
||||
cachedWatchlist = {
|
||||
etag: response.headers.get('etag') ?? '',
|
||||
response: data,
|
||||
};
|
||||
|
||||
watchlistCache.data.set<PlexWatchlistCache>(
|
||||
this.authToken,
|
||||
cachedWatchlist
|
||||
);
|
||||
}
|
||||
|
||||
const watchlistDetails = await Promise.all(
|
||||
(data.MediaContainer.Metadata ?? []).map(async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
'https://metadata.provider.plex.tv'
|
||||
);
|
||||
(cachedWatchlist?.response.MediaContainer.Metadata ?? []).map(
|
||||
async (watchlistItem) => {
|
||||
const detailedResponse = await this.getRolling<MetadataResponse>(
|
||||
`/library/metadata/${watchlistItem.ratingKey}`,
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
'https://metadata.provider.plex.tv'
|
||||
);
|
||||
|
||||
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||
const metadata = detailedResponse.MediaContainer.Metadata[0];
|
||||
|
||||
const tmdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tmdb')
|
||||
);
|
||||
const tvdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tvdb')
|
||||
);
|
||||
const tmdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tmdb')
|
||||
);
|
||||
const tvdbString = metadata.Guid.find((guid) =>
|
||||
guid.id.startsWith('tvdb')
|
||||
);
|
||||
|
||||
return {
|
||||
ratingKey: metadata.ratingKey,
|
||||
// This should always be set? But I guess it also cannot be?
|
||||
// We will filter out the 0's afterwards
|
||||
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
||||
tvdbId: tvdbString
|
||||
? Number(tvdbString.id.split('//')[1])
|
||||
: undefined,
|
||||
title: metadata.title,
|
||||
type: metadata.type,
|
||||
};
|
||||
})
|
||||
return {
|
||||
ratingKey: metadata.ratingKey,
|
||||
// This should always be set? But I guess it also cannot be?
|
||||
// We will filter out the 0's afterwards
|
||||
tmdbId: tmdbString ? Number(tmdbString.id.split('//')[1]) : 0,
|
||||
tvdbId: tvdbString
|
||||
? Number(tvdbString.id.split('//')[1])
|
||||
: undefined,
|
||||
title: metadata.title,
|
||||
type: metadata.type,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const filteredList = watchlistDetails.filter((detail) => detail.tmdbId);
|
||||
@@ -311,7 +342,7 @@ class PlexTvAPI extends ExternalAPI {
|
||||
return {
|
||||
offset,
|
||||
size,
|
||||
totalSize: data.MediaContainer.totalSize,
|
||||
totalSize: cachedWatchlist?.response.MediaContainer.totalSize ?? 0,
|
||||
items: filteredList,
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -327,6 +358,29 @@ class PlexTvAPI extends ExternalAPI {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async pingToken() {
|
||||
try {
|
||||
const data: { pong: unknown } = await this.get(
|
||||
'/api/v2/ping',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'X-Plex-Client-Identifier': randomUUID(),
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!data?.pong) {
|
||||
throw new Error('No pong response');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Failed to ping token', {
|
||||
label: 'Plex Refresh Token',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PlexTvAPI;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ExternalAPI from '@server/api/externalapi';
|
||||
import cacheManager from '@server/lib/cache';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import jaro from 'wink-jaro-distance';
|
||||
|
||||
interface RTAlgoliaSearchResponse {
|
||||
results: {
|
||||
@@ -15,7 +16,7 @@ interface RTAlgoliaHit {
|
||||
tmsId: string;
|
||||
type: string;
|
||||
title: string;
|
||||
titles: string[];
|
||||
titles?: string[];
|
||||
description: string;
|
||||
releaseYear: number;
|
||||
rating: string;
|
||||
@@ -24,9 +25,9 @@ interface RTAlgoliaHit {
|
||||
isEmsSearchable: boolean;
|
||||
rtId: number;
|
||||
vanity: string;
|
||||
aka: string[];
|
||||
aka?: string[];
|
||||
posterImageUrl: string;
|
||||
rottenTomatoes: {
|
||||
rottenTomatoes?: {
|
||||
audienceScore: number;
|
||||
criticsIconUrl: string;
|
||||
wantToSeeCount: number;
|
||||
@@ -47,6 +48,47 @@ export interface RTRating {
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Tunables
|
||||
const INEXACT_TITLE_FACTOR = 0.25;
|
||||
const ALTERNATE_TITLE_FACTOR = 0.8;
|
||||
const PER_YEAR_PENALTY = 0.4;
|
||||
const MINIMUM_SCORE = 0.175;
|
||||
|
||||
// Normalization for title comparisons.
|
||||
// Lowercase and strip non-alphanumeric (unicode-aware).
|
||||
const norm = (s: string): string =>
|
||||
s.toLowerCase().replace(/[^\p{L}\p{N} ]/gu, '');
|
||||
|
||||
// Title similarity. 1 if exact, quarter-jaro otherwise.
|
||||
const similarity = (a: string, b: string): number =>
|
||||
a === b ? 1 : jaro(a, b).similarity * INEXACT_TITLE_FACTOR;
|
||||
|
||||
// Gets the best similarity score between the searched title and all alternate
|
||||
// titles of the search result. Non-main titles are penalized.
|
||||
const t_score = ({ title, titles, aka }: RTAlgoliaHit, s: string): number => {
|
||||
const f = (t: string, i: number) =>
|
||||
similarity(norm(t), norm(s)) * (i ? ALTERNATE_TITLE_FACTOR : 1);
|
||||
return Math.max(...[title].concat(aka || [], titles || []).map(f));
|
||||
};
|
||||
|
||||
// Year difference to score: 0 -> 1.0, 1 -> 0.6, 2 -> 0.2, 3+ -> 0.0
|
||||
const y_score = (r: RTAlgoliaHit, y?: number): number =>
|
||||
y ? Math.max(0, 1 - Math.abs(r.releaseYear - y) * PER_YEAR_PENALTY) : 1;
|
||||
|
||||
// Cut score in half if result has no ratings.
|
||||
const extra_score = (r: RTAlgoliaHit): number => (r.rottenTomatoes ? 1 : 0.5);
|
||||
|
||||
// Score search result as product of all subscores
|
||||
const score = (r: RTAlgoliaHit, name: string, year?: number): number =>
|
||||
t_score(r, name) * y_score(r, year) * extra_score(r);
|
||||
|
||||
// Score each search result and return the highest scoring result, if any
|
||||
const best = (rs: RTAlgoliaHit[], name: string, year?: number): RTAlgoliaHit =>
|
||||
rs
|
||||
.map((r) => ({ score: score(r, name, year), result: r }))
|
||||
.filter(({ score }) => score > MINIMUM_SCORE)
|
||||
.sort(({ score: a }, { score: b }) => b - a)[0]?.result;
|
||||
|
||||
/**
|
||||
* This is a best-effort API. The Rotten Tomatoes API is technically
|
||||
* private and getting access costs money/requires approval.
|
||||
@@ -90,47 +132,21 @@ class RottenTomatoes extends ExternalAPI {
|
||||
year: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const filters = encodeURIComponent('isEmsSearchable=1 AND type:"movie"');
|
||||
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'content_rt',
|
||||
query: name,
|
||||
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
|
||||
query: name.replace(/\bthe\b ?/gi, ''),
|
||||
params: `filters=${filters}&hitsPerPage=20`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
||||
const movie = best(contentResults?.hits || [], name, year);
|
||||
|
||||
if (!contentResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First, attempt to match exact name and year
|
||||
let movie = contentResults.hits.find(
|
||||
(movie) => movie.releaseYear === year && movie.title === name
|
||||
);
|
||||
|
||||
// If we don't find a movie, try to match partial name and year
|
||||
if (!movie) {
|
||||
movie = contentResults.hits.find(
|
||||
(movie) => movie.releaseYear === year && movie.title.includes(name)
|
||||
);
|
||||
}
|
||||
|
||||
// If we still dont find a movie, try to match just on year
|
||||
if (!movie) {
|
||||
movie = contentResults.hits.find((movie) => movie.releaseYear === year);
|
||||
}
|
||||
|
||||
// One last try, try exact name match only
|
||||
if (!movie) {
|
||||
movie = contentResults.hits.find((movie) => movie.title === name);
|
||||
}
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
if (!movie?.rottenTomatoes) return null;
|
||||
|
||||
return {
|
||||
title: movie.title,
|
||||
@@ -158,33 +174,21 @@ class RottenTomatoes extends ExternalAPI {
|
||||
year?: number
|
||||
): Promise<RTRating | null> {
|
||||
try {
|
||||
const filters = encodeURIComponent('isEmsSearchable=1 AND type:"tv"');
|
||||
const data = await this.post<RTAlgoliaSearchResponse>('/queries', {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'content_rt',
|
||||
query: name,
|
||||
params: 'filters=isEmsSearchable%20%3D%201&hitsPerPage=20',
|
||||
params: `filters=${filters}&hitsPerPage=20`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const contentResults = data.results.find((r) => r.index === 'content_rt');
|
||||
const tvshow = best(contentResults?.hits || [], name, year);
|
||||
|
||||
if (!contentResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let tvshow: RTAlgoliaHit | undefined = contentResults.hits[0];
|
||||
|
||||
if (year) {
|
||||
tvshow = contentResults.hits.find(
|
||||
(series) => series.releaseYear === year
|
||||
);
|
||||
}
|
||||
|
||||
if (!tvshow || !tvshow.rottenTomatoes) {
|
||||
return null;
|
||||
}
|
||||
if (!tvshow?.rottenTomatoes) return null;
|
||||
|
||||
return {
|
||||
title: tvshow.title,
|
||||
|
||||
@@ -230,6 +230,23 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
|
||||
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public clearCache = ({
|
||||
tmdbId,
|
||||
externalId,
|
||||
}: {
|
||||
tmdbId?: number | null;
|
||||
externalId?: number | null;
|
||||
}) => {
|
||||
if (tmdbId) {
|
||||
this.removeCache('/movie/lookup', {
|
||||
term: `tmdb:${tmdbId}`,
|
||||
});
|
||||
}
|
||||
if (externalId) {
|
||||
this.removeCache(`/movie/${externalId}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default RadarrAPI;
|
||||
|
||||
@@ -353,6 +353,30 @@ class SonarrAPI extends ServarrBase<{
|
||||
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
public clearCache = ({
|
||||
tvdbId,
|
||||
externalId,
|
||||
title,
|
||||
}: {
|
||||
tvdbId?: number | null;
|
||||
externalId?: number | null;
|
||||
title?: string | null;
|
||||
}) => {
|
||||
if (tvdbId) {
|
||||
this.removeCache('/series/lookup', {
|
||||
term: `tvdb:${tvdbId}`,
|
||||
});
|
||||
}
|
||||
if (externalId) {
|
||||
this.removeCache(`/series/${externalId}`);
|
||||
}
|
||||
if (title) {
|
||||
this.removeCache('/series/lookup', {
|
||||
term: title,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default SonarrAPI;
|
||||
|
||||
@@ -99,16 +99,16 @@ interface DiscoverTvOptions {
|
||||
}
|
||||
|
||||
class TheMovieDb extends ExternalAPI {
|
||||
private region?: string;
|
||||
private discoverRegion?: string;
|
||||
private originalLanguage?: string;
|
||||
constructor({
|
||||
region,
|
||||
discoverRegion,
|
||||
originalLanguage,
|
||||
}: { region?: string; originalLanguage?: string } = {}) {
|
||||
}: { discoverRegion?: string; originalLanguage?: string } = {}) {
|
||||
super(
|
||||
'https://api.themoviedb.org/3',
|
||||
{
|
||||
api_key: 'db55323b8d3e4154498498a75642b381',
|
||||
api_key: '431a8708161bcd1f1fbe7536137e61ed',
|
||||
},
|
||||
{
|
||||
nodeCache: cacheManager.getCache('tmdb').data,
|
||||
@@ -118,7 +118,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
},
|
||||
}
|
||||
);
|
||||
this.region = region;
|
||||
this.discoverRegion = discoverRegion;
|
||||
this.originalLanguage = originalLanguage;
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
page: page.toString(),
|
||||
include_adult: includeAdult ? 'true' : 'false',
|
||||
language,
|
||||
region: this.region || '',
|
||||
region: this.discoverRegion || '',
|
||||
with_original_language:
|
||||
originalLanguage && originalLanguage !== 'all'
|
||||
? originalLanguage
|
||||
@@ -541,7 +541,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
sort_by: sortBy,
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
region: this.discoverRegion || '',
|
||||
// Set our release date values, but check if one is set and not the other,
|
||||
// so we can force a past date or a future date. TMDB Requires both values if one is set!
|
||||
'first_air_date.gte':
|
||||
@@ -594,7 +594,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
region: this.discoverRegion || '',
|
||||
originalLanguage: this.originalLanguage || '',
|
||||
}
|
||||
);
|
||||
@@ -620,7 +620,7 @@ class TheMovieDb extends ExternalAPI {
|
||||
{
|
||||
page: page.toString(),
|
||||
language,
|
||||
region: this.region || '',
|
||||
region: this.discoverRegion || '',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum ApiErrorCode {
|
||||
InvalidAuthToken = 'INVALID_AUTH_TOKEN',
|
||||
InvalidEmail = 'INVALID_EMAIL',
|
||||
NotAdmin = 'NOT_ADMIN',
|
||||
NoAdminUser = 'NO_ADMIN_USER',
|
||||
SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS',
|
||||
SyncErrorNoLibraries = 'SYNC_ERROR_NO_LIBRARIES',
|
||||
Unknown = 'UNKNOWN',
|
||||
|
||||
@@ -1,7 +1,43 @@
|
||||
import 'reflect-metadata';
|
||||
import fs from 'fs';
|
||||
import type { TlsOptions } from 'tls';
|
||||
import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const DB_SSL_PREFIX = 'DB_SSL_';
|
||||
|
||||
function boolFromEnv(envVar: string, defaultVal = false) {
|
||||
if (process.env[envVar]) {
|
||||
return process.env[envVar]?.toLowerCase() === 'true';
|
||||
}
|
||||
return defaultVal;
|
||||
}
|
||||
|
||||
function stringOrReadFileFromEnv(envVar: string): Buffer | string | undefined {
|
||||
if (process.env[envVar]) {
|
||||
return process.env[envVar];
|
||||
}
|
||||
const filePath = process.env[`${envVar}_FILE`];
|
||||
if (filePath) {
|
||||
return fs.readFileSync(filePath);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildSslConfig(): TlsOptions | undefined {
|
||||
if (process.env.DB_USE_SSL?.toLowerCase() !== 'true') {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
rejectUnauthorized: boolFromEnv(
|
||||
`${DB_SSL_PREFIX}REJECT_UNAUTHORIZED`,
|
||||
true
|
||||
),
|
||||
ca: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}CA`),
|
||||
key: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}KEY`),
|
||||
cert: stringOrReadFileFromEnv(`${DB_SSL_PREFIX}CERT`),
|
||||
};
|
||||
}
|
||||
|
||||
const devConfig: DataSourceOptions = {
|
||||
type: 'sqlite',
|
||||
database: process.env.CONFIG_DIRECTORY
|
||||
@@ -9,10 +45,10 @@ const devConfig: DataSourceOptions = {
|
||||
: 'config/db/db.sqlite3',
|
||||
synchronize: true,
|
||||
migrationsRun: false,
|
||||
logging: false,
|
||||
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||
enableWAL: true,
|
||||
entities: ['server/entity/**/*.ts'],
|
||||
migrations: ['server/migration/**/*.ts'],
|
||||
migrations: ['server/migration/sqlite/**/*.ts'],
|
||||
subscribers: ['server/subscriber/**/*.ts'],
|
||||
};
|
||||
|
||||
@@ -23,16 +59,60 @@ const prodConfig: DataSourceOptions = {
|
||||
: 'config/db/db.sqlite3',
|
||||
synchronize: false,
|
||||
migrationsRun: false,
|
||||
logging: false,
|
||||
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||
enableWAL: true,
|
||||
entities: ['dist/entity/**/*.js'],
|
||||
migrations: ['dist/migration/**/*.js'],
|
||||
migrations: ['dist/migration/sqlite/**/*.js'],
|
||||
subscribers: ['dist/subscriber/**/*.js'],
|
||||
};
|
||||
|
||||
const dataSource = new DataSource(
|
||||
process.env.NODE_ENV !== 'production' ? devConfig : prodConfig
|
||||
);
|
||||
const postgresDevConfig: DataSourceOptions = {
|
||||
type: 'postgres',
|
||||
host: process.env.DB_SOCKET_PATH || process.env.DB_HOST,
|
||||
port: process.env.DB_SOCKET_PATH
|
||||
? undefined
|
||||
: parseInt(process.env.DB_PORT ?? '5432'),
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME ?? 'jellyseerr',
|
||||
ssl: buildSslConfig(),
|
||||
synchronize: false,
|
||||
migrationsRun: true,
|
||||
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||
entities: ['server/entity/**/*.ts'],
|
||||
migrations: ['server/migration/postgres/**/*.ts'],
|
||||
subscribers: ['server/subscriber/**/*.ts'],
|
||||
};
|
||||
|
||||
const postgresProdConfig: DataSourceOptions = {
|
||||
type: 'postgres',
|
||||
host: process.env.DB_SOCKET_PATH || process.env.DB_HOST,
|
||||
port: process.env.DB_SOCKET_PATH
|
||||
? undefined
|
||||
: parseInt(process.env.DB_PORT ?? '5432'),
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME ?? 'jellyseerr',
|
||||
ssl: buildSslConfig(),
|
||||
synchronize: false,
|
||||
migrationsRun: false,
|
||||
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||
entities: ['dist/entity/**/*.js'],
|
||||
migrations: ['dist/migration/postgres/**/*.js'],
|
||||
subscribers: ['dist/subscriber/**/*.js'],
|
||||
};
|
||||
|
||||
export const isPgsql = process.env.DB_TYPE === 'postgres';
|
||||
|
||||
function getDataSource(): DataSourceOptions {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return isPgsql ? postgresProdConfig : prodConfig;
|
||||
} else {
|
||||
return isPgsql ? postgresDevConfig : devConfig;
|
||||
}
|
||||
}
|
||||
|
||||
const dataSource = new DataSource(getDataSource());
|
||||
|
||||
export const getRepository = <Entity extends object>(
|
||||
target: EntityTarget<Entity>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { DownloadingItem } from '@server/lib/downloadtracker';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import logger from '@server/logger';
|
||||
import { DbAwareColumn } from '@server/utils/DbColumnHelper';
|
||||
import { getHostname } from '@server/utils/getHostname';
|
||||
import {
|
||||
AfterLoad,
|
||||
@@ -42,6 +43,10 @@ class Media {
|
||||
finalIds = tmdbIds;
|
||||
}
|
||||
|
||||
if (finalIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const media = await mediaRepository
|
||||
.createQueryBuilder('media')
|
||||
.leftJoinAndSelect(
|
||||
@@ -127,10 +132,23 @@ class Media {
|
||||
@UpdateDateColumn()
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||
/**
|
||||
* The `lastSeasonChange` column stores the date and time when the media was added to the library.
|
||||
* It needs to be database-aware because SQLite supports `datetime` while PostgreSQL supports `timestamp with timezone (timestampz)`.
|
||||
*/
|
||||
@DbAwareColumn({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||
public lastSeasonChange: Date;
|
||||
|
||||
@Column({ type: 'datetime', nullable: true })
|
||||
/**
|
||||
* The `mediaAddedAt` column stores the date and time when the media was added to the library.
|
||||
* It needs to be database-aware because SQLite supports `datetime` while PostgreSQL supports `timestamp with timezone (timestampz)`.
|
||||
* This column is nullable because it can be null when the media is not yet synced to the library.
|
||||
*/
|
||||
@DbAwareColumn({
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
nullable: true,
|
||||
})
|
||||
public mediaAddedAt: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'int' })
|
||||
|
||||
@@ -7,12 +7,14 @@ import type {
|
||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||
import TheMovieDb from '@server/api/themoviedb';
|
||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||
import {
|
||||
MediaRequestStatus,
|
||||
MediaStatus,
|
||||
MediaType,
|
||||
} from '@server/constants/media';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import OverrideRule from '@server/entity/OverrideRule';
|
||||
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||
import { Permission } from '@server/lib/permissions';
|
||||
@@ -57,6 +59,7 @@ export class MediaRequest {
|
||||
const mediaRepository = getRepository(Media);
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const userRepository = getRepository(User);
|
||||
const settings = getSettings();
|
||||
|
||||
let requestUser = user;
|
||||
|
||||
@@ -205,6 +208,134 @@ export class MediaRequest {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply overrides if the user is not an admin or has the "advanced request" permission
|
||||
const useOverrides = !user.hasPermission([Permission.MANAGE_REQUESTS], {
|
||||
type: 'or',
|
||||
});
|
||||
|
||||
let rootFolder = requestBody.rootFolder;
|
||||
let profileId = requestBody.profileId;
|
||||
let tags = requestBody.tags;
|
||||
|
||||
if (useOverrides) {
|
||||
const defaultRadarrId = requestBody.is4k
|
||||
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
||||
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
||||
const defaultSonarrId = requestBody.is4k
|
||||
? settings.sonarr.findIndex((s) => s.is4k && s.isDefault)
|
||||
: settings.sonarr.findIndex((s) => !s.is4k && s.isDefault);
|
||||
|
||||
const overrideRuleRepository = getRepository(OverrideRule);
|
||||
const overrideRules = await overrideRuleRepository.find({
|
||||
where:
|
||||
requestBody.mediaType === MediaType.MOVIE
|
||||
? { radarrServiceId: defaultRadarrId }
|
||||
: { sonarrServiceId: defaultSonarrId },
|
||||
});
|
||||
|
||||
const appliedOverrideRules = overrideRules.filter((rule) => {
|
||||
const hasAnimeKeyword =
|
||||
'results' in tmdbMedia.keywords &&
|
||||
tmdbMedia.keywords.results.some(
|
||||
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
|
||||
);
|
||||
|
||||
// Skip override rules if the media is an anime TV show as anime TV
|
||||
// is handled by default and override rules do not explicitly include
|
||||
// the anime keyword
|
||||
if (
|
||||
requestBody.mediaType === MediaType.TV &&
|
||||
hasAnimeKeyword &&
|
||||
(!rule.keywords ||
|
||||
!rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
rule.users &&
|
||||
!rule.users
|
||||
.split(',')
|
||||
.some((userId) => Number(userId) === requestUser.id)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
rule.genre &&
|
||||
!rule.genre
|
||||
.split(',')
|
||||
.some((genreId) =>
|
||||
tmdbMedia.genres
|
||||
.map((genre) => genre.id)
|
||||
.includes(Number(genreId))
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
rule.language &&
|
||||
!rule.language
|
||||
.split('|')
|
||||
.some((languageId) => languageId === tmdbMedia.original_language)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
rule.keywords &&
|
||||
!rule.keywords.split(',').some((keywordId) => {
|
||||
let keywordList: TmdbKeyword[] = [];
|
||||
|
||||
if ('keywords' in tmdbMedia.keywords) {
|
||||
keywordList = tmdbMedia.keywords.keywords;
|
||||
} else if ('results' in tmdbMedia.keywords) {
|
||||
keywordList = tmdbMedia.keywords.results;
|
||||
}
|
||||
|
||||
return keywordList
|
||||
.map((keyword: TmdbKeyword) => keyword.id)
|
||||
.includes(Number(keywordId));
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// hacky way to prioritize rules
|
||||
// TODO: make this better
|
||||
const prioritizedRule = appliedOverrideRules.sort((a, b) => {
|
||||
const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords'];
|
||||
|
||||
const aSpecificity = keys.filter((key) => a[key] !== null).length;
|
||||
const bSpecificity = keys.filter((key) => b[key] !== null).length;
|
||||
|
||||
// Take the rule with the most specific condition first
|
||||
return bSpecificity - aSpecificity;
|
||||
})[0];
|
||||
|
||||
if (prioritizedRule) {
|
||||
if (prioritizedRule.rootFolder) {
|
||||
rootFolder = prioritizedRule.rootFolder;
|
||||
}
|
||||
if (prioritizedRule.profileId) {
|
||||
profileId = prioritizedRule.profileId;
|
||||
}
|
||||
if (prioritizedRule.tags) {
|
||||
tags = [
|
||||
...new Set([
|
||||
...(tags || []),
|
||||
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
logger.debug('Override rule applied.', {
|
||||
label: 'Media Request',
|
||||
overrides: prioritizedRule,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (requestBody.mediaType === MediaType.MOVIE) {
|
||||
await mediaRepository.save(media);
|
||||
|
||||
@@ -243,9 +374,9 @@ export class MediaRequest {
|
||||
: undefined,
|
||||
is4k: requestBody.is4k,
|
||||
serverId: requestBody.serverId,
|
||||
profileId: requestBody.profileId,
|
||||
rootFolder: requestBody.rootFolder,
|
||||
tags: requestBody.tags,
|
||||
profileId: profileId,
|
||||
rootFolder: rootFolder,
|
||||
tags: tags,
|
||||
isAutoRequest: options.isAutoRequest ?? false,
|
||||
});
|
||||
|
||||
@@ -255,12 +386,14 @@ export class MediaRequest {
|
||||
const tmdbMediaShow = tmdbMedia as Awaited<
|
||||
ReturnType<typeof tmdb.getTvShow>
|
||||
>;
|
||||
const requestedSeasons =
|
||||
let requestedSeasons =
|
||||
requestBody.seasons === 'all'
|
||||
? tmdbMediaShow.seasons
|
||||
.map((season) => season.season_number)
|
||||
.filter((sn) => sn > 0)
|
||||
? tmdbMediaShow.seasons.map((season) => season.season_number)
|
||||
: (requestBody.seasons as number[]);
|
||||
if (!settings.main.enableSpecialEpisodes) {
|
||||
requestedSeasons = requestedSeasons.filter((sn) => sn > 0);
|
||||
}
|
||||
|
||||
let existingSeasons: number[] = [];
|
||||
|
||||
// We need to check existing requests on this title to make sure we don't double up on seasons that were
|
||||
@@ -346,10 +479,10 @@ export class MediaRequest {
|
||||
: undefined,
|
||||
is4k: requestBody.is4k,
|
||||
serverId: requestBody.serverId,
|
||||
profileId: requestBody.profileId,
|
||||
rootFolder: requestBody.rootFolder,
|
||||
profileId: profileId,
|
||||
rootFolder: rootFolder,
|
||||
languageProfileId: requestBody.languageProfileId,
|
||||
tags: requestBody.tags,
|
||||
tags: tags,
|
||||
seasons: finalSeasons.map(
|
||||
(sn) =>
|
||||
new SeasonRequest({
|
||||
@@ -586,18 +719,26 @@ export class MediaRequest {
|
||||
// Do not update the status if the item is already partially available or available
|
||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
|
||||
media[this.is4k ? 'status4k' : 'status'] !==
|
||||
MediaStatus.PARTIALLY_AVAILABLE
|
||||
MediaStatus.PARTIALLY_AVAILABLE &&
|
||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
|
||||
) {
|
||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
|
||||
mediaRepository.save(media);
|
||||
const statusField = this.is4k ? 'status4k' : 'status';
|
||||
|
||||
await mediaRepository.update(
|
||||
{ id: this.media.id },
|
||||
{ [statusField]: MediaStatus.PROCESSING }
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
media.mediaType === MediaType.MOVIE &&
|
||||
this.status === MediaRequestStatus.DECLINED
|
||||
) {
|
||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||
mediaRepository.save(media);
|
||||
const statusField = this.is4k ? 'status4k' : 'status';
|
||||
await mediaRepository.update(
|
||||
{ id: this.media.id },
|
||||
{ [statusField]: MediaStatus.UNKNOWN }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -614,8 +755,11 @@ export class MediaRequest {
|
||||
).length === 0 &&
|
||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING
|
||||
) {
|
||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||
mediaRepository.save(media);
|
||||
const statusField = this.is4k ? 'status4k' : 'status';
|
||||
mediaRepository.update(
|
||||
{ id: this.media.id },
|
||||
{ [statusField]: MediaStatus.UNKNOWN }
|
||||
);
|
||||
}
|
||||
|
||||
// Approve child seasons if parent is approved
|
||||
@@ -817,8 +961,10 @@ export class MediaRequest {
|
||||
});
|
||||
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
this.status = MediaRequestStatus.APPROVED;
|
||||
await requestRepository.save(this);
|
||||
|
||||
await requestRepository.update(this.id, {
|
||||
status: MediaRequestStatus.APPROVED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -848,18 +994,22 @@ export class MediaRequest {
|
||||
throw new Error('Media data not found');
|
||||
}
|
||||
|
||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
radarrMovie.id;
|
||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
radarrMovie.titleSlug;
|
||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = radarrSettings?.id;
|
||||
await mediaRepository.save(media);
|
||||
const updateFields = {
|
||||
[this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
|
||||
radarrMovie.id,
|
||||
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
|
||||
radarrMovie.titleSlug,
|
||||
[this.is4k ? 'serviceId4k' : 'serviceId']: radarrMovie?.id,
|
||||
};
|
||||
|
||||
await mediaRepository.update({ id: this.media.id }, updateFields);
|
||||
})
|
||||
.catch(async () => {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
this.status = MediaRequestStatus.FAILED;
|
||||
requestRepository.save(this);
|
||||
await requestRepository.update(this.id, {
|
||||
status: MediaRequestStatus.FAILED,
|
||||
});
|
||||
|
||||
logger.warn(
|
||||
'Something went wrong sending movie request to Radarr, marking status as FAILED',
|
||||
@@ -872,6 +1022,14 @@ export class MediaRequest {
|
||||
);
|
||||
|
||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
||||
})
|
||||
.finally(() => {
|
||||
radarr.clearCache({
|
||||
tmdbId: movie.id,
|
||||
externalId: this.is4k
|
||||
? media.externalServiceId4k
|
||||
: media.externalServiceId,
|
||||
});
|
||||
});
|
||||
logger.info('Sent request to Radarr', {
|
||||
label: 'Media Request',
|
||||
@@ -967,8 +1125,9 @@ export class MediaRequest {
|
||||
});
|
||||
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
this.status = MediaRequestStatus.APPROVED;
|
||||
await requestRepository.save(this);
|
||||
await requestRepository.update(this.id, {
|
||||
status: MediaRequestStatus.APPROVED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1129,18 +1288,23 @@ export class MediaRequest {
|
||||
throw new Error('Media data not found');
|
||||
}
|
||||
|
||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||
sonarrSeries.id;
|
||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
||||
sonarrSeries.titleSlug;
|
||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
|
||||
await mediaRepository.save(media);
|
||||
const updateFields = {
|
||||
[this.is4k ? 'externalServiceId4k' : 'externalServiceId']:
|
||||
sonarrSeries.id,
|
||||
[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug']:
|
||||
sonarrSeries.titleSlug,
|
||||
[this.is4k ? 'serviceId4k' : 'serviceId']: sonarrSettings?.id,
|
||||
};
|
||||
|
||||
await mediaRepository.update({ id: this.media.id }, updateFields);
|
||||
})
|
||||
.catch(async () => {
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
|
||||
this.status = MediaRequestStatus.FAILED;
|
||||
requestRepository.save(this);
|
||||
await requestRepository.update(
|
||||
{ id: this.id },
|
||||
{ status: MediaRequestStatus.FAILED }
|
||||
);
|
||||
|
||||
logger.warn(
|
||||
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
||||
@@ -1153,6 +1317,15 @@ export class MediaRequest {
|
||||
);
|
||||
|
||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
||||
})
|
||||
.finally(() => {
|
||||
sonarr.clearCache({
|
||||
tvdbId,
|
||||
externalId: this.is4k
|
||||
? media.externalServiceId4k
|
||||
: media.externalServiceId,
|
||||
title: series.name,
|
||||
});
|
||||
});
|
||||
logger.info('Sent request to Sonarr', {
|
||||
label: 'Media Request',
|
||||
|
||||
52
server/entity/OverrideRule.ts
Normal file
52
server/entity/OverrideRule.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
class OverrideRule {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
public radarrServiceId?: number;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
public sonarrServiceId?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public users?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public genre?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public language?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public keywords?: string;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
public profileId?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public rootFolder?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public tags?: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
public createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
public updatedAt: Date;
|
||||
|
||||
constructor(init?: Partial<OverrideRule>) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
export default OverrideRule;
|
||||
@@ -23,7 +23,9 @@ class Season {
|
||||
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
||||
public status4k: MediaStatus;
|
||||
|
||||
@ManyToOne(() => Media, (media) => media.seasons, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => Media, (media) => media.seasons, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
public media: Promise<Media>;
|
||||
|
||||
@CreateDateColumn()
|
||||
|
||||
@@ -83,13 +83,13 @@ export class User {
|
||||
@Column({ nullable: true })
|
||||
public jellyfinUserId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({ nullable: true, select: false })
|
||||
public jellyfinDeviceId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({ nullable: true, select: false })
|
||||
public jellyfinAuthToken?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Column({ nullable: true, select: false })
|
||||
public plexToken?: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
|
||||
@@ -31,7 +31,10 @@ export class UserSettings {
|
||||
public locale?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public region?: string;
|
||||
public discoverRegion?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public streamingRegion?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public originalLanguage?: string;
|
||||
@@ -57,6 +60,9 @@ export class UserSettings {
|
||||
@Column({ nullable: true })
|
||||
public telegramChatId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public telegramMessageThreadId?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public telegramSendSilently?: boolean;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PlexAPI from '@server/api/plexapi';
|
||||
import dataSource, { getRepository } from '@server/datasource';
|
||||
import dataSource, { getRepository, isPgsql } from '@server/datasource';
|
||||
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
||||
import { Session } from '@server/entity/Session';
|
||||
import { User } from '@server/entity/User';
|
||||
@@ -41,11 +41,6 @@ import path from 'path';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import YAML from 'yamljs';
|
||||
|
||||
if (process.env.forceIpv4First === 'true') {
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
net.setDefaultAutoSelectFamily(false);
|
||||
}
|
||||
|
||||
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
||||
|
||||
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
||||
@@ -66,18 +61,37 @@ app
|
||||
|
||||
// Run migrations in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
await dbConnection.query('PRAGMA foreign_keys=OFF');
|
||||
await dbConnection.runMigrations();
|
||||
await dbConnection.query('PRAGMA foreign_keys=ON');
|
||||
if (isPgsql) {
|
||||
await dbConnection.runMigrations();
|
||||
} else {
|
||||
await dbConnection.query('PRAGMA foreign_keys=OFF');
|
||||
await dbConnection.runMigrations();
|
||||
await dbConnection.query('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
}
|
||||
|
||||
// Load Settings
|
||||
const settings = await getSettings().load();
|
||||
restartFlag.initializeSettings(settings.main);
|
||||
restartFlag.initializeSettings(settings);
|
||||
|
||||
// Check if we force IPv4 first
|
||||
if (
|
||||
process.env.forceIpv4First === 'true' ||
|
||||
settings.network.forceIpv4First
|
||||
) {
|
||||
dns.setDefaultResultOrder('ipv4first');
|
||||
net.setDefaultAutoSelectFamily(false);
|
||||
}
|
||||
|
||||
if (settings.network.dnsServers.trim() !== '') {
|
||||
dns.setServers(
|
||||
settings.network.dnsServers.split(',').map((server) => server.trim())
|
||||
);
|
||||
}
|
||||
|
||||
// Register HTTP proxy
|
||||
if (settings.main.proxy.enabled) {
|
||||
await createCustomProxyAgent(settings.main.proxy);
|
||||
if (settings.network.proxy.enabled) {
|
||||
await createCustomProxyAgent(settings.network.proxy);
|
||||
}
|
||||
|
||||
// Migrate library types
|
||||
@@ -132,7 +146,7 @@ app
|
||||
await DiscoverSlider.bootstrapSliders();
|
||||
|
||||
const server = express();
|
||||
if (settings.main.trustProxy) {
|
||||
if (settings.network.trustProxy) {
|
||||
server.enable('trust proxy');
|
||||
}
|
||||
server.use(cookieParser());
|
||||
@@ -153,7 +167,7 @@ app
|
||||
next();
|
||||
}
|
||||
});
|
||||
if (settings.main.csrfProtection) {
|
||||
if (settings.network.csrfProtection) {
|
||||
server.use(
|
||||
csurf({
|
||||
cookie: {
|
||||
@@ -183,7 +197,7 @@ app
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30,
|
||||
httpOnly: true,
|
||||
sameSite: settings.main.csrfProtection ? 'strict' : 'lax',
|
||||
sameSite: settings.network.csrfProtection ? 'strict' : 'lax',
|
||||
secure: 'auto',
|
||||
},
|
||||
store: new TypeormStore({
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface GenreSliderItem {
|
||||
}
|
||||
|
||||
export interface WatchlistItem {
|
||||
id: number;
|
||||
ratingKey: string;
|
||||
tmdbId: number;
|
||||
mediaType: 'movie' | 'tv';
|
||||
|
||||
3
server/interfaces/api/overrideRuleInterfaces.ts
Normal file
3
server/interfaces/api/overrideRuleInterfaces.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type OverrideRule from '@server/entity/OverrideRule';
|
||||
|
||||
export type OverrideRuleResultsResponse = OverrideRule[];
|
||||
@@ -32,10 +32,12 @@ export interface PublicSettingsResponse {
|
||||
localLogin: boolean;
|
||||
movie4kEnabled: boolean;
|
||||
series4kEnabled: boolean;
|
||||
region: string;
|
||||
discoverRegion: string;
|
||||
streamingRegion: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
cacheImages: boolean;
|
||||
vapidPublic: string;
|
||||
enablePushRegistration: boolean;
|
||||
|
||||
@@ -5,7 +5,8 @@ export interface UserSettingsGeneralResponse {
|
||||
email?: string;
|
||||
discordId?: string;
|
||||
locale?: string;
|
||||
region?: string;
|
||||
discoverRegion?: string;
|
||||
streamingRegion?: string;
|
||||
originalLanguage?: string;
|
||||
movieQuotaLimit?: number;
|
||||
movieQuotaDays?: number;
|
||||
@@ -33,6 +34,7 @@ export interface UserSettingsNotificationsResponse {
|
||||
telegramEnabled?: boolean;
|
||||
telegramBotUsername?: string;
|
||||
telegramChatId?: string;
|
||||
telegramMessageThreadId?: string;
|
||||
telegramSendSilently?: boolean;
|
||||
webPushEnabled?: boolean;
|
||||
notificationTypes: Partial<NotificationAgentTypes>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MediaServerType } from '@server/constants/server';
|
||||
import availabilitySync from '@server/lib/availabilitySync';
|
||||
import downloadTracker from '@server/lib/downloadtracker';
|
||||
import ImageProxy from '@server/lib/imageproxy';
|
||||
import refreshToken from '@server/lib/refreshToken';
|
||||
import {
|
||||
jellyfinFullScanner,
|
||||
jellyfinRecentScanner,
|
||||
@@ -13,7 +14,6 @@ import type { JobId } from '@server/lib/settings';
|
||||
import { getSettings } from '@server/lib/settings';
|
||||
import watchlistSync from '@server/lib/watchlistsync';
|
||||
import logger from '@server/logger';
|
||||
import random from 'lodash/random';
|
||||
import schedule from 'node-schedule';
|
||||
|
||||
interface ScheduledJob {
|
||||
@@ -70,6 +70,35 @@ export const startJobs = (): void => {
|
||||
running: () => plexFullScanner.status().running,
|
||||
cancelFn: () => plexFullScanner.cancel(),
|
||||
});
|
||||
|
||||
scheduledJobs.push({
|
||||
id: 'plex-refresh-token',
|
||||
name: 'Plex Refresh Token',
|
||||
type: 'process',
|
||||
interval: 'fixed',
|
||||
cronSchedule: jobs['plex-refresh-token'].schedule,
|
||||
job: schedule.scheduleJob(jobs['plex-refresh-token'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Plex Refresh Token', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
refreshToken.run();
|
||||
}),
|
||||
});
|
||||
|
||||
// Watchlist Sync
|
||||
scheduledJobs.push({
|
||||
id: 'plex-watchlist-sync',
|
||||
name: 'Plex Watchlist Sync',
|
||||
type: 'process',
|
||||
interval: 'seconds',
|
||||
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
||||
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
|
||||
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
watchlistSync.syncWatchlist();
|
||||
}),
|
||||
});
|
||||
} else if (
|
||||
mediaServerType === MediaServerType.JELLYFIN ||
|
||||
mediaServerType === MediaServerType.EMBY
|
||||
@@ -112,31 +141,6 @@ export const startJobs = (): void => {
|
||||
});
|
||||
}
|
||||
|
||||
// Watchlist Sync
|
||||
const watchlistSyncJob: ScheduledJob = {
|
||||
id: 'plex-watchlist-sync',
|
||||
name: 'Plex Watchlist Sync',
|
||||
type: 'process',
|
||||
interval: 'fixed',
|
||||
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
||||
job: schedule.scheduleJob(new Date(Date.now() + 1000 * 60 * 20), () => {
|
||||
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
||||
label: 'Jobs',
|
||||
});
|
||||
watchlistSync.syncWatchlist();
|
||||
}),
|
||||
};
|
||||
|
||||
// To help alleviate load on Plex's servers, we will add some fuzziness to the next schedule
|
||||
// after each run
|
||||
watchlistSyncJob.job.on('run', () => {
|
||||
watchlistSyncJob.job.schedule(
|
||||
new Date(Math.floor(Date.now() + 1000 * 60 * random(14, 24, true)))
|
||||
);
|
||||
});
|
||||
|
||||
scheduledJobs.push(watchlistSyncJob);
|
||||
|
||||
// Run full radarr scan every 24 hours
|
||||
scheduledJobs.push({
|
||||
id: 'radarr-scan',
|
||||
|
||||
@@ -8,7 +8,8 @@ export type AvailableCacheIds =
|
||||
| 'imdb'
|
||||
| 'github'
|
||||
| 'plexguid'
|
||||
| 'plextv';
|
||||
| 'plextv'
|
||||
| 'plexwatchlist';
|
||||
|
||||
const DEFAULT_TTL = 300;
|
||||
const DEFAULT_CHECK_PERIOD = 120;
|
||||
@@ -68,6 +69,7 @@ class CacheManager {
|
||||
stdTtl: 86400 * 7, // 1 week cache
|
||||
checkPeriod: 60,
|
||||
}),
|
||||
plexwatchlist: new Cache('plexwatchlist', 'Plex Watchlist'),
|
||||
};
|
||||
|
||||
public getCache(id: AvailableCacheIds): Cache {
|
||||
|
||||
@@ -17,6 +17,7 @@ interface TelegramMessagePayload {
|
||||
text: string;
|
||||
parse_mode: string;
|
||||
chat_id: string;
|
||||
message_thread_id: string;
|
||||
disable_notification: boolean;
|
||||
}
|
||||
|
||||
@@ -25,6 +26,7 @@ interface TelegramPhotoPayload {
|
||||
caption: string;
|
||||
parse_mode: string;
|
||||
chat_id: string;
|
||||
message_thread_id: string;
|
||||
disable_notification: boolean;
|
||||
}
|
||||
|
||||
@@ -182,6 +184,7 @@ class TelegramAgent
|
||||
body: JSON.stringify({
|
||||
...notificationPayload,
|
||||
chat_id: settings.options.chatId,
|
||||
message_thread_id: settings.options.messageThreadId,
|
||||
disable_notification: !!settings.options.sendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
||||
});
|
||||
@@ -233,6 +236,8 @@ class TelegramAgent
|
||||
body: JSON.stringify({
|
||||
...notificationPayload,
|
||||
chat_id: payload.notifyUser.settings.telegramChatId,
|
||||
message_thread_id:
|
||||
payload.notifyUser.settings.telegramMessageThreadId,
|
||||
disable_notification:
|
||||
!!payload.notifyUser.settings.telegramSendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
||||
@@ -296,6 +301,7 @@ class TelegramAgent
|
||||
body: JSON.stringify({
|
||||
...notificationPayload,
|
||||
chat_id: user.settings.telegramChatId,
|
||||
message_thread_id: user.settings.telegramMessageThreadId,
|
||||
disable_notification: !!user.settings?.telegramSendSilently,
|
||||
} as TelegramMessagePayload | TelegramPhotoPayload),
|
||||
});
|
||||
|
||||
37
server/lib/refreshToken.ts
Normal file
37
server/lib/refreshToken.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import PlexTvAPI from '@server/api/plextv';
|
||||
import { getRepository } from '@server/datasource';
|
||||
import { User } from '@server/entity/User';
|
||||
import logger from '@server/logger';
|
||||
|
||||
class RefreshToken {
|
||||
public async run() {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
const users = await userRepository
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.plexToken')
|
||||
.where("user.plexToken != ''")
|
||||
.getMany();
|
||||
|
||||
for (const user of users) {
|
||||
await this.refreshUserToken(user);
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshUserToken(user: User) {
|
||||
if (!user.plexToken) {
|
||||
logger.warn('Skipping user refresh token for user without plex token', {
|
||||
label: 'Plex Refresh Token',
|
||||
user: user.displayName,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const plexTvApi = new PlexTvAPI(user.plexToken);
|
||||
plexTvApi.pingToken();
|
||||
}
|
||||
}
|
||||
|
||||
const refreshToken = new RefreshToken();
|
||||
|
||||
export default refreshToken;
|
||||
@@ -210,14 +210,27 @@ class JellyfinScanner {
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.ProviderIds.Tvdb) {
|
||||
tvShow = await this.tmdb.getShowByTvdbId({
|
||||
tvdbId: Number(metadata.ProviderIds.Tvdb),
|
||||
});
|
||||
} else if (metadata.ProviderIds.Tmdb) {
|
||||
tvShow = await this.tmdb.getTvShow({
|
||||
tvId: Number(metadata.ProviderIds.Tmdb),
|
||||
});
|
||||
if (metadata.ProviderIds.Tmdb) {
|
||||
try {
|
||||
tvShow = await this.tmdb.getTvShow({
|
||||
tvId: Number(metadata.ProviderIds.Tmdb),
|
||||
});
|
||||
} catch {
|
||||
this.log('Unable to find TMDb ID for this title.', 'debug', {
|
||||
jellyfinitem,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!tvShow && metadata.ProviderIds.Tvdb) {
|
||||
try {
|
||||
tvShow = await this.tmdb.getShowByTvdbId({
|
||||
tvdbId: Number(metadata.ProviderIds.Tvdb),
|
||||
});
|
||||
} catch {
|
||||
this.log('Unable to find TVDb ID for this title.', 'debug', {
|
||||
jellyfinitem,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tvShow) {
|
||||
@@ -491,7 +504,13 @@ class JellyfinScanner {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.log(`failed show: ${metadata.Name}`);
|
||||
this.log(
|
||||
`No information found for the show: ${metadata.Name}`,
|
||||
'debug',
|
||||
{
|
||||
jellyfinitem,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.log(
|
||||
|
||||
@@ -277,8 +277,11 @@ class PlexScanner
|
||||
|
||||
const seasons = tvShow.seasons;
|
||||
const processableSeasons: ProcessableSeason[] = [];
|
||||
const settings = getSettings();
|
||||
|
||||
const filteredSeasons = seasons.filter((sn) => sn.season_number !== 0);
|
||||
const filteredSeasons = settings.main.enableSpecialEpisodes
|
||||
? seasons
|
||||
: seasons.filter((sn) => sn.season_number !== 0);
|
||||
|
||||
for (const season of filteredSeasons) {
|
||||
const matchedPlexSeason = metadata.Children?.Metadata.find(
|
||||
|
||||
@@ -102,11 +102,12 @@ class SonarrScanner
|
||||
}
|
||||
|
||||
const tmdbId = tvShow.id;
|
||||
const settings = getSettings();
|
||||
|
||||
const filteredSeasons = sonarrSeries.seasons.filter(
|
||||
(sn) =>
|
||||
sn.seasonNumber !== 0 &&
|
||||
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
|
||||
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) &&
|
||||
(!settings.main.enableSpecialEpisodes ? sn.seasonNumber !== 0 : true)
|
||||
);
|
||||
|
||||
for (const season of filteredSeasons) {
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface DVRSettings {
|
||||
syncEnabled: boolean;
|
||||
preventSearch: boolean;
|
||||
tagRequests: boolean;
|
||||
overrideRule: number[];
|
||||
}
|
||||
|
||||
export interface RadarrSettings extends DVRSettings {
|
||||
@@ -114,7 +115,6 @@ export interface MainSettings {
|
||||
apiKey: string;
|
||||
applicationTitle: string;
|
||||
applicationUrl: string;
|
||||
csrfProtection: boolean;
|
||||
cacheImages: boolean;
|
||||
defaultPermissions: number;
|
||||
defaultQuotas: {
|
||||
@@ -124,12 +124,20 @@ export interface MainSettings {
|
||||
hideAvailable: boolean;
|
||||
localLogin: boolean;
|
||||
newPlexLogin: boolean;
|
||||
region: string;
|
||||
discoverRegion: string;
|
||||
streamingRegion: string;
|
||||
originalLanguage: string;
|
||||
trustProxy: boolean;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export interface NetworkSettings {
|
||||
csrfProtection: boolean;
|
||||
forceIpv4First: boolean;
|
||||
dnsServers: string;
|
||||
trustProxy: boolean;
|
||||
proxy: ProxySettings;
|
||||
}
|
||||
|
||||
@@ -144,13 +152,15 @@ interface FullPublicSettings extends PublicSettings {
|
||||
localLogin: boolean;
|
||||
movie4kEnabled: boolean;
|
||||
series4kEnabled: boolean;
|
||||
region: string;
|
||||
discoverRegion: string;
|
||||
streamingRegion: string;
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
jellyfinExternalHost?: string;
|
||||
jellyfinForgotPasswordUrl?: string;
|
||||
jellyfinServerName?: string;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
cacheImages: boolean;
|
||||
vapidPublic: string;
|
||||
enablePushRegistration: boolean;
|
||||
@@ -211,6 +221,7 @@ export interface NotificationAgentTelegram extends NotificationAgentConfig {
|
||||
botUsername?: string;
|
||||
botAPI: string;
|
||||
chatId: string;
|
||||
messageThreadId: string;
|
||||
sendSilently: boolean;
|
||||
};
|
||||
}
|
||||
@@ -282,6 +293,7 @@ export type JobId =
|
||||
| 'plex-recently-added-scan'
|
||||
| 'plex-full-scan'
|
||||
| 'plex-watchlist-sync'
|
||||
| 'plex-refresh-token'
|
||||
| 'radarr-scan'
|
||||
| 'sonarr-scan'
|
||||
| 'download-sync'
|
||||
@@ -304,6 +316,7 @@ export interface AllSettings {
|
||||
public: PublicSettings;
|
||||
notifications: NotificationSettings;
|
||||
jobs: Record<JobId, JobSettings>;
|
||||
network: NetworkSettings;
|
||||
}
|
||||
|
||||
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
||||
@@ -322,7 +335,6 @@ class Settings {
|
||||
apiKey: '',
|
||||
applicationTitle: 'Jellyseerr',
|
||||
applicationUrl: '',
|
||||
csrfProtection: false,
|
||||
cacheImages: false,
|
||||
defaultPermissions: Permission.REQUEST,
|
||||
defaultQuotas: {
|
||||
@@ -332,22 +344,13 @@ class Settings {
|
||||
hideAvailable: false,
|
||||
localLogin: true,
|
||||
newPlexLogin: true,
|
||||
region: '',
|
||||
discoverRegion: '',
|
||||
streamingRegion: '',
|
||||
originalLanguage: '',
|
||||
trustProxy: false,
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
enableSpecialEpisodes: false,
|
||||
locale: 'en',
|
||||
proxy: {
|
||||
enabled: false,
|
||||
hostname: '',
|
||||
port: 8080,
|
||||
useSsl: false,
|
||||
user: '',
|
||||
password: '',
|
||||
bypassFilter: '',
|
||||
bypassLocalAddresses: true,
|
||||
},
|
||||
},
|
||||
plex: {
|
||||
name: '',
|
||||
@@ -419,6 +422,7 @@ class Settings {
|
||||
options: {
|
||||
botAPI: '',
|
||||
chatId: '',
|
||||
messageThreadId: '',
|
||||
sendSilently: false,
|
||||
},
|
||||
},
|
||||
@@ -469,7 +473,10 @@ class Settings {
|
||||
schedule: '0 0 3 * * *',
|
||||
},
|
||||
'plex-watchlist-sync': {
|
||||
schedule: '0 */10 * * * *',
|
||||
schedule: '0 */3 * * * *',
|
||||
},
|
||||
'plex-refresh-token': {
|
||||
schedule: '0 0 5 * * *',
|
||||
},
|
||||
'radarr-scan': {
|
||||
schedule: '0 0 4 * * *',
|
||||
@@ -496,6 +503,22 @@ class Settings {
|
||||
schedule: '0 0 5 * * *',
|
||||
},
|
||||
},
|
||||
network: {
|
||||
csrfProtection: false,
|
||||
trustProxy: false,
|
||||
forceIpv4First: false,
|
||||
dnsServers: '',
|
||||
proxy: {
|
||||
enabled: false,
|
||||
hostname: '',
|
||||
port: 8080,
|
||||
useSsl: false,
|
||||
user: '',
|
||||
password: '',
|
||||
bypassFilter: '',
|
||||
bypassLocalAddresses: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (initialSettings) {
|
||||
this.data = merge(this.data, initialSettings);
|
||||
@@ -572,10 +595,12 @@ class Settings {
|
||||
series4kEnabled: this.data.sonarr.some(
|
||||
(sonarr) => sonarr.is4k && sonarr.isDefault
|
||||
),
|
||||
region: this.data.main.region,
|
||||
discoverRegion: this.data.main.discoverRegion,
|
||||
streamingRegion: this.data.main.streamingRegion,
|
||||
originalLanguage: this.data.main.originalLanguage,
|
||||
mediaServerType: this.main.mediaServerType,
|
||||
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
||||
enableSpecialEpisodes: this.data.main.enableSpecialEpisodes,
|
||||
cacheImages: this.data.main.cacheImages,
|
||||
vapidPublic: this.vapidPublic,
|
||||
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
||||
@@ -603,6 +628,14 @@ class Settings {
|
||||
this.data.jobs = data;
|
||||
}
|
||||
|
||||
get network(): NetworkSettings {
|
||||
return this.data.network;
|
||||
}
|
||||
|
||||
set network(data: NetworkSettings) {
|
||||
this.data.network = data;
|
||||
}
|
||||
|
||||
get clientId(): string {
|
||||
return this.data.clientId;
|
||||
}
|
||||
@@ -684,10 +717,9 @@ class Settings {
|
||||
}
|
||||
|
||||
public async save(): Promise<void> {
|
||||
await fs.writeFile(
|
||||
SETTINGS_PATH,
|
||||
JSON.stringify(this.data, undefined, ' ')
|
||||
);
|
||||
const tmp = SETTINGS_PATH + '.tmp';
|
||||
await fs.writeFile(tmp, JSON.stringify(this.data, undefined, ' '));
|
||||
await fs.rename(tmp, SETTINGS_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateRegionSetting = (settings: any): AllSettings => {
|
||||
if (
|
||||
settings.main.discoverRegion !== undefined &&
|
||||
settings.main.streamingRegion !== undefined
|
||||
) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
const oldRegion = settings.main.region;
|
||||
if (oldRegion) {
|
||||
settings.main.discoverRegion = oldRegion;
|
||||
settings.main.streamingRegion = oldRegion;
|
||||
} else {
|
||||
settings.main.discoverRegion = '';
|
||||
settings.main.streamingRegion = 'US';
|
||||
}
|
||||
delete settings.main.region;
|
||||
|
||||
return settings;
|
||||
};
|
||||
|
||||
export default migrateRegionSetting;
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { AllSettings } from '@server/lib/settings';
|
||||
|
||||
const migrateNetworkSettings = (settings: any): AllSettings => {
|
||||
if (settings.network) {
|
||||
return settings;
|
||||
}
|
||||
const newSettings = { ...settings };
|
||||
newSettings.network = {
|
||||
...settings.network,
|
||||
csrfProtection: settings.main.csrfProtection ?? false,
|
||||
trustProxy: settings.main.trustProxy ?? false,
|
||||
forceIpv4First: settings.main.forceIpv4First ?? false,
|
||||
dnsServers: settings.main.dnsServers ?? '',
|
||||
proxy: settings.main.proxy ?? {
|
||||
enabled: false,
|
||||
hostname: '',
|
||||
port: 8080,
|
||||
useSsl: false,
|
||||
user: '',
|
||||
password: '',
|
||||
bypassFilter: '',
|
||||
bypassLocalAddresses: true,
|
||||
},
|
||||
};
|
||||
delete settings.main.csrfProtection;
|
||||
delete settings.main.trustProxy;
|
||||
delete settings.main.forceIpv4First;
|
||||
delete settings.main.dnsServers;
|
||||
delete settings.main.proxy;
|
||||
return newSettings;
|
||||
};
|
||||
|
||||
export default migrateNetworkSettings;
|
||||
@@ -62,7 +62,7 @@ class WatchlistSync {
|
||||
|
||||
const plexTvApi = new PlexTvAPI(user.plexToken);
|
||||
|
||||
const response = await plexTvApi.getWatchlist({ size: 200 });
|
||||
const response = await plexTvApi.getWatchlist({ size: 20 });
|
||||
|
||||
const mediaItems = await Media.getRelatedMedia(
|
||||
user,
|
||||
|
||||
195
server/migration/postgres/1734786061496-InitialMigration.ts
Normal file
195
server/migration/postgres/1734786061496-InitialMigration.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class InitialMigration1734786061496 implements MigrationInterface {
|
||||
name = 'InitialMigration1734786061496';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "blacklist" ("id" SERIAL NOT NULL, "mediaType" character varying NOT NULL, "title" character varying, "tmdbId" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "mediaId" integer, CONSTRAINT "UQ_6bbafa28411e6046421991ea21c" UNIQUE ("tmdbId"), CONSTRAINT "REL_62b7ade94540f9f8d8bede54b9" UNIQUE ("mediaId"), CONSTRAINT "PK_04dc42a96bf0914cda31b579702" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "season_request" ("id" SERIAL NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "requestId" integer, CONSTRAINT "PK_4811e502081543bf620f1fa4328" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "media_request" ("id" SERIAL NOT NULL, "status" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" character varying NOT NULL, "is4k" boolean NOT NULL DEFAULT false, "serverId" integer, "profileId" integer, "rootFolder" character varying, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT false, "mediaId" integer NOT NULL, "requestedById" integer, "modifiedById" integer, CONSTRAINT "PK_f8334500e8e12db87536558c66c" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "season" ("id" SERIAL NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "status4k" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "mediaId" integer NOT NULL, CONSTRAINT "PK_8ac0d081dbdb7ab02d166bcda9f" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "media" ("id" SERIAL NOT NULL, "mediaType" character varying NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" character varying, "status" integer NOT NULL DEFAULT '1', "status4k" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "lastSeasonChange" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "mediaAddedAt" TIMESTAMP WITH TIME ZONE DEFAULT now(), "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" character varying, "externalServiceSlug4k" character varying, "ratingKey" character varying, "ratingKey4k" character varying, "jellyfinMediaId" character varying, "jellyfinMediaId4k" character varying, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"), CONSTRAINT "PK_f4e0fcac36e050de337b670d8bd" PRIMARY KEY ("id"))`
|
||||
);
|
||||
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(
|
||||
`CREATE TABLE "watchlist" ("id" SERIAL NOT NULL, "ratingKey" character varying NOT NULL, "mediaType" character varying NOT NULL, "title" character varying NOT NULL, "tmdbId" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "requestedById" integer, "mediaId" integer NOT NULL, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "PK_0c8c0dbcc8d379117138e71ad5b" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_push_subscription" ("id" SERIAL NOT NULL, "endpoint" character varying NOT NULL, "p256dh" character varying NOT NULL, "auth" character varying NOT NULL, "userId" integer, CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "PK_397020e7be9a4086cc798e0bb63" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_settings" ("id" SERIAL NOT NULL, "locale" character varying NOT NULL DEFAULT '', "discoverRegion" character varying, "streamingRegion" character varying, "originalLanguage" character varying, "pgpKey" character varying, "discordId" character varying, "pushbulletAccessToken" character varying, "pushoverApplicationToken" character varying, "pushoverUserKey" character varying, "pushoverSound" character varying, "telegramChatId" character varying, "telegramSendSilently" boolean, "watchlistSyncMovies" boolean, "watchlistSyncTv" boolean, "notificationTypes" text, "userId" integer, CONSTRAINT "REL_986a2b6d3c05eb4091bb8066f7" UNIQUE ("userId"), CONSTRAINT "PK_00f004f5922a0744d174530d639" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying NOT NULL, "plexUsername" character varying, "jellyfinUsername" character varying, "username" character varying, "password" character varying, "resetPasswordGuid" character varying, "recoveryLinkExpirationDate" date, "userType" integer NOT NULL DEFAULT '1', "plexId" integer, "jellyfinUserId" character varying, "jellyfinDeviceId" character varying, "jellyfinAuthToken" character varying, "plexToken" character varying, "permissions" integer NOT NULL DEFAULT '0', "avatar" character varying NOT NULL, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "issue_comment" ("id" SERIAL NOT NULL, "message" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "userId" integer, "issueId" integer, CONSTRAINT "PK_2ad05784e2ae661fa409e5e0248" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "issue" ("id" SERIAL NOT NULL, "issueType" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "problemSeason" integer NOT NULL DEFAULT '0', "problemEpisode" integer NOT NULL DEFAULT '0', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "mediaId" integer, "createdById" integer, "modifiedById" integer, CONSTRAINT "PK_f80e086c249b9f3f3ff2fd321b7" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "discover_slider" ("id" SERIAL NOT NULL, "type" integer NOT NULL, "order" integer NOT NULL, "isBuiltIn" boolean NOT NULL DEFAULT false, "enabled" boolean NOT NULL DEFAULT true, "title" character varying, "data" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_20a71a098d04bae448e4d51db23" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "session" ("expiredAt" bigint NOT NULL, "id" character varying(255) NOT NULL, "json" text NOT NULL, CONSTRAINT "PK_f55da76ac1c3ac420f444d2ff11" PRIMARY KEY ("id"))`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session" ("expiredAt") `
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" ADD CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" ADD CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season_request" ADD CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a" FOREIGN KEY ("requestId") REFERENCES "media_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ADD CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_push_subscription" ADD CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" ADD CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue_comment" ADD CONSTRAINT "FK_707b033c2d0653f75213614789d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue_comment" ADD CONSTRAINT "FK_180710fead1c94ca499c57a7d42" FOREIGN KEY ("issueId") REFERENCES "issue"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" ADD CONSTRAINT "FK_276e20d053f3cff1645803c95d8" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" ADD CONSTRAINT "FK_10b17b49d1ee77e7184216001e0" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" ADD CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5" FOREIGN KEY ("modifiedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" DROP CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" DROP CONSTRAINT "FK_10b17b49d1ee77e7184216001e0"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue" DROP CONSTRAINT "FK_276e20d053f3cff1645803c95d8"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue_comment" DROP CONSTRAINT "FK_180710fead1c94ca499c57a7d42"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "issue_comment" DROP CONSTRAINT "FK_707b033c2d0653f75213614789d"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" DROP CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_push_subscription" DROP CONSTRAINT "FK_03f7958328e311761b0de675fbe"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" DROP CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_6997bee94720f1ecb7f31137095"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season_request" DROP CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" DROP CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "blacklist" DROP CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_28c5d1d16da7908c97c9bc2f74"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "session"`);
|
||||
await queryRunner.query(`DROP TABLE "discover_slider"`);
|
||||
await queryRunner.query(`DROP TABLE "issue"`);
|
||||
await queryRunner.query(`DROP TABLE "issue_comment"`);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`DROP TABLE "user_settings"`);
|
||||
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_939f205946256cc0d2a1ac51a8"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "watchlist"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7ff2d11f6a83cb52386eaebe74"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_41a289eb1fa489c1bc6f38d9c3"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_7157aad07c73f6a6ae3bbd5ef5"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "media"`);
|
||||
await queryRunner.query(`DROP TABLE "season"`);
|
||||
await queryRunner.query(`DROP TABLE "media_request"`);
|
||||
await queryRunner.query(`DROP TABLE "season_request"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_6bbafa28411e6046421991ea21"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "blacklist"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddTelegramMessageThreadId1734786596045
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddTelegramMessageThreadId1734786596045';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" ADD "telegramMessageThreadId" character varying`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_settings" DROP COLUMN "telegramMessageThreadId"`
|
||||
);
|
||||
}
|
||||
}
|
||||
15
server/migration/postgres/1734805738349-AddOverrideRules.ts
Normal file
15
server/migration/postgres/1734805738349-AddOverrideRules.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddOverrideRules1734805738349 implements MigrationInterface {
|
||||
name = 'AddOverrideRules1734805738349';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "override_rule" ("id" SERIAL NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" character varying, "genre" character varying, "language" character varying, "keywords" character varying, "profileId" integer, "rootFolder" character varying, "tags" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_657f810c7b20a4fce45aee8f182" PRIMARY KEY ("id"))`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "override_rule"`);
|
||||
}
|
||||
}
|
||||
65
server/migration/postgres/1734809898562-FixNullFields.ts
Normal file
65
server/migration/postgres/1734809898562-FixNullFields.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class FixNullFields1734809898562 implements MigrationInterface {
|
||||
name = 'FixNullFields1734809898562';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ALTER COLUMN "mediaId" DROP NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ALTER COLUMN "mediaId" DROP NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ALTER COLUMN "mediaId" DROP NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" DROP CONSTRAINT "FK_087099b39600be695591da9a49c"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" DROP CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" DROP CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc"`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ALTER COLUMN "mediaId" SET NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "season" ADD CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ALTER COLUMN "mediaId" SET NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "media_request" ADD CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ALTER COLUMN "mediaId" SET NOT NULL`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "watchlist" ADD CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user