Compare commits
17 Commits
advanced-o
...
preview-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8325986bf | ||
|
|
2123c1d0e9 | ||
|
|
c37ea8d8e2 | ||
|
|
6969f503ab | ||
|
|
86724a3067 | ||
|
|
ad1d8c1b0d | ||
|
|
199c6729a5 | ||
|
|
5b87b6fd8d | ||
|
|
8ab2731736 | ||
|
|
3b8f0076a1 | ||
|
|
d7b5be4bad | ||
|
|
33f1e01650 | ||
|
|
925247833f | ||
|
|
8c20223df6 | ||
|
|
442128a2de | ||
|
|
5d0f9ce30a | ||
|
|
f96dd64bf9 |
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -95,7 +95,7 @@ body:
|
|||||||
id: terms
|
id: terms
|
||||||
attributes:
|
attributes:
|
||||||
label: Code of Conduct
|
label: Code of Conduct
|
||||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/seerr-team/seerr/blob/develop/CODE_OF_CONDUCT.md)
|
description: By submitting this issue, you agree to follow our [Code of Conduct](/../../CODE_OF_CONDUCT.md)
|
||||||
options:
|
options:
|
||||||
- label: I agree to follow Seerr's Code of Conduct
|
- label: I agree to follow Seerr's Code of Conduct
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
#### To-Dos
|
#### To-Dos
|
||||||
|
|
||||||
- [ ] Disclosed any use of AI (see our [policy](https://github.com/seerr-team/seerr/blob/develop/CONTRIBUTING.md#ai-assistance-notice))
|
- [ ] Disclosed any use of AI (see our [policy](../CONTRIBUTING.md#ai-assistance-notice))
|
||||||
- [ ] Successful build `pnpm build`
|
- [ ] Successful build `pnpm build`
|
||||||
- [ ] Translation keys `pnpm i18n:extract`
|
- [ ] Translation keys `pnpm i18n:extract`
|
||||||
- [ ] Database migration (if required)
|
- [ ] Database migration (if required)
|
||||||
|
|||||||
94
.github/cliff.toml
vendored
94
.github/cliff.toml
vendored
@@ -1,94 +0,0 @@
|
|||||||
# git-cliff ~ configuration
|
|
||||||
# https://git-cliff.org/docs/configuration
|
|
||||||
|
|
||||||
[changelog]
|
|
||||||
header = ""
|
|
||||||
body = """
|
|
||||||
{%- macro remote_url() -%}
|
|
||||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
|
||||||
{%- endmacro -%}
|
|
||||||
|
|
||||||
{%- set excluded_users = ["github-actions[bot]", "dependabot[bot]", "renovate[bot]"] -%}
|
|
||||||
|
|
||||||
{% macro print_commit(commit) -%}
|
|
||||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
|
||||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
|
||||||
{{ commit.message | upper_first }} - \
|
|
||||||
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
|
|
||||||
{% endmacro -%}
|
|
||||||
|
|
||||||
{% if version %}\
|
|
||||||
{% if previous.version %}\
|
|
||||||
## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
||||||
{% else %}\
|
|
||||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
||||||
{% endif %}\
|
|
||||||
{% else %}\
|
|
||||||
## [unreleased]
|
|
||||||
{% endif %}\
|
|
||||||
|
|
||||||
{%- for group, commits in commits | group_by(attribute="group") %}
|
|
||||||
### {{ group | striptags | trim | upper_first }}
|
|
||||||
{%- for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
|
|
||||||
{{ self::print_commit(commit=commit) }}
|
|
||||||
{%- endfor %}
|
|
||||||
{%- for commit in commits %}
|
|
||||||
{%- if not commit.scope -%}
|
|
||||||
{{ self::print_commit(commit=commit) }}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
|
|
||||||
{%- set valid_contributors = [] -%}
|
|
||||||
{%- for c in github.contributors | filter(attribute="is_first_time", value=true) %}
|
|
||||||
{%- if c.username and c.username not in excluded_users and c.username not in valid_contributors %}
|
|
||||||
{%- set_global valid_contributors = valid_contributors | concat(with=c.username) %}
|
|
||||||
{%- endif %}
|
|
||||||
{%- endfor %}
|
|
||||||
|
|
||||||
{%- if valid_contributors | length > 0 %}
|
|
||||||
## New Contributors ❤️
|
|
||||||
{%- for username in valid_contributors %}
|
|
||||||
* @{{ username }} made their first contribution
|
|
||||||
{%- endfor %}
|
|
||||||
{%- endif %}
|
|
||||||
"""
|
|
||||||
footer = """
|
|
||||||
<!-- generated by git-cliff -->
|
|
||||||
"""
|
|
||||||
trim = true
|
|
||||||
postprocessors = []
|
|
||||||
|
|
||||||
[git]
|
|
||||||
conventional_commits = true
|
|
||||||
filter_unconventional = true
|
|
||||||
split_commits = false
|
|
||||||
filter_commits = true
|
|
||||||
commit_preprocessors = [
|
|
||||||
{ pattern = '.*\[skip ci\].*', replace = "" },
|
|
||||||
{ pattern = '.*\[ci skip\].*', replace = "" },
|
|
||||||
]
|
|
||||||
commit_parsers = [
|
|
||||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
|
||||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
|
||||||
{ message = "^doc", group = "<!-- 3 -->📖 Documentation" },
|
|
||||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
|
||||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
|
||||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
|
||||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
|
||||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
|
||||||
{ message = "^chore\\(deps.*\\)", skip = true },
|
|
||||||
{ message = "^chore\\(pr\\)", skip = true },
|
|
||||||
{ message = "^chore\\(pull\\)", skip = true },
|
|
||||||
{ message = "^chore\\(git-sync\\)", skip = true },
|
|
||||||
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
|
||||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
|
||||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
|
||||||
{ message = '.*\[skip ci\].*', skip = true },
|
|
||||||
{ message = '.*\[ci skip\].*', skip = true },
|
|
||||||
]
|
|
||||||
protect_breaking_commits = false
|
|
||||||
tag_pattern = "v?[0-9]+\\.[0-9]+\\.[0-9]+.*"
|
|
||||||
skip_tags = "beta|alpha|rc"
|
|
||||||
topo_order = false
|
|
||||||
sort_commits = "newest"
|
|
||||||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -14,9 +14,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
|
||||||
DOCKER_HUB: seerr/seerr
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-${{ github.ref }}
|
group: ci-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -26,7 +23,7 @@ jobs:
|
|||||||
name: Lint & Test Build
|
name: Lint & Test Build
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
container: node:22.20.0-alpine3.22@sha256:dbcedd8aeab47fbc0f4dd4bffa55b7c3c729a707875968d467aaaea42d6225af
|
container: node:22.20.0-alpine3.22@sha256:cb3143549582cc5f74f26f0992cdef4a422b22128cb517f94173a5f910fa4ee7
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -34,7 +31,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Pnpm Setup
|
- name: Pnpm Setup
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
shell: sh
|
shell: sh
|
||||||
@@ -143,7 +140,7 @@ jobs:
|
|||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_HUB }}
|
${{ github.repository }}
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=develop
|
type=raw,value=develop
|
||||||
|
|||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -42,15 +42,15 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-and-quality
|
queries: +security-and-quality
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
uses: github/codeql-action/autobuild@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||||
with:
|
with:
|
||||||
category: '/language:${{ matrix.language }}'
|
category: '/language:${{ matrix.language }}'
|
||||||
|
|||||||
2
.github/workflows/cypress.yml
vendored
2
.github/workflows/cypress.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
|||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
|
|
||||||
- name: Pnpm Setup
|
- name: Pnpm Setup
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
5
.github/workflows/docs-deploy.yml
vendored
5
.github/workflows/docs-deploy.yml
vendored
@@ -3,10 +3,9 @@
|
|||||||
name: Deploy to GitHub Pages
|
name: Deploy to GitHub Pages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- legacy-jellyseerr
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'gen-docs/**'
|
- 'gen-docs/**'
|
||||||
@@ -35,7 +34,7 @@ jobs:
|
|||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
|
|
||||||
- name: Pnpm Setup
|
- name: Pnpm Setup
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
shell: sh
|
shell: sh
|
||||||
|
|||||||
58
.github/workflows/helm.yml
vendored
58
.github/workflows/helm.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
|||||||
# get current version
|
# get current version
|
||||||
current_version=$(grep '^version:' "$chart_path/Chart.yaml" | awk '{print $2}')
|
current_version=$(grep '^version:' "$chart_path/Chart.yaml" | awk '{print $2}')
|
||||||
# try to get current release version
|
# try to get current release version
|
||||||
if oras manifest fetch "ghcr.io/${{ github.repository }}/${chart_name}:${current_version}" >/dev/null 2>&1; then
|
if oras manifest fetch "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:${current_version}" >/dev/null 2>&1; then
|
||||||
echo "No version change for $chart_name. Skipping."
|
echo "No version change for $chart_name. Skipping."
|
||||||
else
|
else
|
||||||
helm dependency build "$chart_path"
|
helm dependency build "$chart_path"
|
||||||
@@ -87,8 +87,8 @@ jobs:
|
|||||||
name: Publish to ghcr.io
|
name: Publish to ghcr.io
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write # needed for pushing to github registry
|
||||||
id-token: write
|
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||||
needs: [package-helm-chart]
|
needs: [package-helm-chart]
|
||||||
if: needs.package-helm-chart.outputs.has_artifacts == 'true'
|
if: needs.package-helm-chart.outputs.has_artifacts == 'true'
|
||||||
steps:
|
steps:
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1.2.4
|
uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1.2.4
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||||
|
|
||||||
- name: Downloads artifacts
|
- name: Downloads artifacts
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
@@ -128,59 +128,17 @@ jobs:
|
|||||||
# push chart to OCI
|
# push chart to OCI
|
||||||
chart_release_file=$(basename "$chart_path")
|
chart_release_file=$(basename "$chart_path")
|
||||||
chart_name=${chart_release_file%-*}
|
chart_name=${chart_release_file%-*}
|
||||||
helm push ${chart_path} oci://ghcr.io/${{ github.repository }} |& tee helm-push-output.log
|
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)
|
chart_digest=$(awk -F "[, ]+" '/Digest/{print $NF}' < helm-push-output.log)
|
||||||
# sign chart
|
# sign chart
|
||||||
cosign sign "ghcr.io/${{ github.repository }}/${chart_name}@${chart_digest}"
|
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}@${chart_digest}"
|
||||||
# push artifacthub-repo.yml to OCI
|
# push artifacthub-repo.yml to OCI
|
||||||
oras push \
|
oras push \
|
||||||
ghcr.io/${{ github.repository }}/${chart_name}:artifacthub.io \
|
ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io \
|
||||||
--config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \
|
--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 \
|
charts/$chart_name/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml \
|
||||||
|& tee oras-push-output.log
|
|& tee oras-push-output.log
|
||||||
artifacthub_digest=$(grep "Digest:" oras-push-output.log | awk '{print $2}')
|
artifacthub_digest=$(grep "Digest:" oras-push-output.log | awk '{print $2}')
|
||||||
# sign artifacthub-repo.yml
|
# sign artifacthub-repo.yml
|
||||||
cosign sign "ghcr.io/${{ github.repository }}/${chart_name}:artifacthub.io@${artifacthub_digest}"
|
cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io@${artifacthub_digest}"
|
||||||
done
|
|
||||||
|
|
||||||
verify:
|
|
||||||
name: Verify signatures for each chart tag
|
|
||||||
needs: [publish]
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
|
||||||
|
|
||||||
- name: Downloads artifacts
|
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: artifacts
|
|
||||||
path: .cr-release-packages/
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Verify signatures for each chart tag
|
|
||||||
run: |
|
|
||||||
for chart_path in $(find .cr-release-packages -name '*.tgz' -print); do
|
|
||||||
chart_release_file=$(basename "$chart_path")
|
|
||||||
chart_name=${chart_release_file%-*}
|
|
||||||
version=${chart_release_file#$chart_name-}
|
|
||||||
version=${version%.tgz}
|
|
||||||
|
|
||||||
cosign verify "ghcr.io/${{ github.repository }}/${chart_name}:${version}" \
|
|
||||||
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
done
|
done
|
||||||
|
|||||||
5
.github/workflows/preview.yml
vendored
5
.github/workflows/preview.yml
vendored
@@ -11,9 +11,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
|
||||||
DOCKER_HUB: seerr/seerr
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: preview-${{ github.ref }}
|
group: preview-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -118,7 +115,7 @@ jobs:
|
|||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_HUB }}
|
${{ github.repository }}
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=preview-${{ steps.ver.outputs.version }}
|
type=raw,value=preview-${{ steps.ver.outputs.version }}
|
||||||
|
|||||||
251
.github/workflows/release.yml
vendored
251
.github/workflows/release.yml
vendored
@@ -3,9 +3,7 @@
|
|||||||
name: Seerr Release
|
name: Seerr Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -14,17 +12,15 @@ concurrency:
|
|||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
|
||||||
DOCKER_HUB: seerr/seerr
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changelog:
|
semantic-release:
|
||||||
name: Generate changelog
|
name: Tag and release latest version
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
env:
|
||||||
contents: read
|
HUSKY: 0
|
||||||
outputs:
|
outputs:
|
||||||
release_body: ${{ steps.git-cliff.outputs.content }}
|
new_release_published: ${{ steps.release.outputs.new_release_published }}
|
||||||
|
new_release_version: ${{ steps.release.outputs.new_release_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -32,36 +28,46 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Generate changelog
|
- name: Set up Node.js
|
||||||
id: git-cliff
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||||
uses: orhun/git-cliff-action@d77b37db2e3f7398432d34b72a12aa3e2ba87e51 # v4.6.0
|
|
||||||
with:
|
with:
|
||||||
config: .github/cliff.toml
|
node-version-file: package.json
|
||||||
args: -vv --current
|
package-manager-cache: false
|
||||||
env:
|
|
||||||
OUTPUT: CHANGELOG.md
|
|
||||||
GITHUB_REPO: ${{ github.repository }}
|
|
||||||
|
|
||||||
create-draft-release:
|
- name: Pnpm Setup
|
||||||
name: Create draft release
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
- name: Get pnpm store directory
|
||||||
contents: write
|
shell: sh
|
||||||
needs: changelog
|
run: |
|
||||||
steps:
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
- name: Draft Release
|
- name: Install dependencies
|
||||||
run: gh release create ${GITHUB_REF_NAME} -t "Release ${GITHUB_REF_NAME}" -n "${RELEASE_BODY}" --draft
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
id: release
|
||||||
|
uses: cycjimmy/semantic-release-action@9cc899c47e6841430bbaedb43de1560a568dfd16 # v5.0.0
|
||||||
|
with:
|
||||||
|
extra_plugins: |
|
||||||
|
@semantic-release/git@10
|
||||||
|
@semantic-release/changelog@6
|
||||||
|
@codedependant/semantic-release-docker@5
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
RELEASE_BODY: ${{ needs.changelog.outputs.release_body }}
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build (${{ matrix.arch }})
|
name: Build (per-arch, native runners)
|
||||||
|
needs: semantic-release
|
||||||
|
if: needs.semantic-release.outputs.new_release_published == 'true'
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
@@ -72,8 +78,6 @@ jobs:
|
|||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
arch: arm64
|
arch: arm64
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
env:
|
|
||||||
VERSION: ${{ github.ref_name }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -87,7 +91,7 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Warm cache [${{ matrix.platform }}]
|
- name: Warm cache (no push) — ${{ matrix.platform }}
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -96,23 +100,21 @@ jobs:
|
|||||||
push: false
|
push: false
|
||||||
build-args: |
|
build-args: |
|
||||||
COMMIT_TAG=${{ github.sha }}
|
COMMIT_TAG=${{ github.sha }}
|
||||||
BUILD_VERSION=${{ env.VERSION }}
|
BUILD_VERSION=${{ needs.semantic-release.outputs.new_release_version }}
|
||||||
SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }}
|
SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }}
|
||||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||||
provenance: false
|
provenance: false
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish multi-arch manifests
|
name: Publish multi-arch image
|
||||||
needs: build
|
needs: [semantic-release, build]
|
||||||
|
if: needs.semantic-release.outputs.new_release_published == 'true'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
id-token: write
|
||||||
packages: write
|
packages: write
|
||||||
outputs:
|
|
||||||
image_digest: ${{ steps.digests.outputs.IMAGE_DIGEST }}
|
|
||||||
env:
|
|
||||||
VERSION: ${{ github.ref_name }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -144,14 +146,14 @@ jobs:
|
|||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_HUB }}
|
${{ github.repository }}
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ env.VERSION }}
|
type=raw,value=${{ needs.semantic-release.outputs.new_release_version }}
|
||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.created=${{ steps.ts.outputs.TIMESTAMP }}
|
org.opencontainers.image.created=${{ steps.ts.outputs.TIMESTAMP }}
|
||||||
|
|
||||||
- name: Build & Push (multi-arch)
|
- name: Build & Push (multi-arch, single tag)
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -160,7 +162,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
COMMIT_TAG=${{ github.sha }}
|
COMMIT_TAG=${{ github.sha }}
|
||||||
BUILD_VERSION=${{ env.VERSION }}
|
BUILD_VERSION=${{ needs.semantic-release.outputs.new_release_version }}
|
||||||
SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }}
|
SOURCE_DATE_EPOCH=${{ steps.ts.outputs.TIMESTAMP }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
@@ -170,158 +172,37 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
provenance: false
|
provenance: false
|
||||||
|
|
||||||
- name: Resolve manifest digest
|
|
||||||
id: digests
|
|
||||||
run: |
|
|
||||||
DIGEST=$(docker buildx imagetools inspect "${{ env.DOCKER_HUB }}:${{ env.VERSION }}" --format '{{json .Manifest.Digest}}' | tr -d '"')
|
|
||||||
echo "IMAGE_DIGEST=$DIGEST" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Also tag :latest (non-pre-release only)
|
- name: Also tag :latest (non-pre-release only)
|
||||||
shell: bash
|
shell: bash
|
||||||
if: ${{ !contains(env.VERSION, '-') }}
|
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools create \
|
VER="${{ needs.semantic-release.outputs.new_release_version }}"
|
||||||
-t ${{ env.DOCKER_HUB }}:latest \
|
if [[ "$VER" != *"-"* ]]; then
|
||||||
${{ env.DOCKER_HUB }}:${{ env.VERSION }}
|
docker buildx imagetools create \
|
||||||
|
-t ${{ github.repository }}:latest \
|
||||||
docker buildx imagetools create \
|
${{ github.repository }}:${VER}
|
||||||
-t ghcr.io/${{ github.repository }}:latest \
|
docker buildx imagetools create \
|
||||||
ghcr.io/${{ github.repository }}:${{ env.VERSION }}
|
-t ghcr.io/${{ github.repository }}:latest \
|
||||||
|
ghcr.io/${{ github.repository }}:${VER}
|
||||||
sign:
|
fi
|
||||||
name: Sign images and create SBOM attestations
|
|
||||||
needs: publish
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
packages: write
|
|
||||||
env:
|
|
||||||
VERSION: ${{ github.ref_name }}
|
|
||||||
COSIGN_YES: 'true'
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
|
||||||
|
|
||||||
- name: Install Trivy
|
|
||||||
uses: aquasecurity/setup-trivy@e6c2c5e321ed9123bda567646e2f96565e34abe1 # v0.2.4
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Sign images
|
|
||||||
run: |
|
|
||||||
cosign sign --recursive "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}"
|
|
||||||
cosign sign --recursive "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}"
|
|
||||||
|
|
||||||
- name: Generate SBOMs
|
|
||||||
run: |
|
|
||||||
trivy image --format cyclonedx --output seerr-ghcr-image-${{ env.VERSION }}.sbom \
|
|
||||||
"ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}"
|
|
||||||
|
|
||||||
trivy image --format cyclonedx --output seerr-dockerhub-image-${{ env.VERSION }}.sbom \
|
|
||||||
"${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}"
|
|
||||||
|
|
||||||
- name: Attest SBOMs
|
|
||||||
run: |
|
|
||||||
cosign attest \
|
|
||||||
--type cyclonedx \
|
|
||||||
--predicate seerr-ghcr-image-${{ env.VERSION }}.sbom \
|
|
||||||
"ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}"
|
|
||||||
|
|
||||||
cosign attest \
|
|
||||||
--type cyclonedx \
|
|
||||||
--predicate seerr-dockerhub-image-${{ env.VERSION }}.sbom \
|
|
||||||
"${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}"
|
|
||||||
|
|
||||||
- name: Upload SBOMs
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: sboms-${{ env.VERSION }}
|
|
||||||
path: '*.sbom'
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
verify:
|
|
||||||
name: Verify signatures and attestations
|
|
||||||
needs: [publish, sign]
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
env:
|
|
||||||
VERSION: ${{ github.ref_name }}
|
|
||||||
steps:
|
|
||||||
- name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
|
||||||
|
|
||||||
- name: Verify signatures
|
|
||||||
run: |
|
|
||||||
cosign verify "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" \
|
|
||||||
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
|
|
||||||
cosign verify "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" \
|
|
||||||
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
|
|
||||||
- name: Verify attestations
|
|
||||||
run: |
|
|
||||||
cosign verify-attestation "ghcr.io/${{ github.repository }}@${{ needs.publish.outputs.image_digest }}" \
|
|
||||||
--type cyclonedx \
|
|
||||||
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" > /dev/null
|
|
||||||
|
|
||||||
cosign verify-attestation "${{ env.DOCKER_HUB }}@${{ needs.publish.outputs.image_digest }}" \
|
|
||||||
--type cyclonedx \
|
|
||||||
--certificate-identity "https://github.com/${{ github.workflow_ref }}" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" > /dev/null
|
|
||||||
|
|
||||||
publish-release:
|
|
||||||
name: Publish release
|
|
||||||
needs: [create-draft-release, verify]
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
env:
|
|
||||||
VERSION: ${{ github.ref_name }}
|
|
||||||
steps:
|
|
||||||
- name: Publish release
|
|
||||||
run: gh release edit "${{ env.VERSION }}" --draft=false --repo "${{ github.repository }}"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
discord:
|
discord:
|
||||||
name: Send Discord Notification
|
name: Send Discord Notification
|
||||||
needs: publish-release
|
needs: publish
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Determine status
|
- name: Determine Workflow Status
|
||||||
id: status
|
id: status
|
||||||
run: |
|
run: |
|
||||||
case "${{ needs.publish-release.result }}" in
|
case "${{ needs.publish.result }}" in
|
||||||
success) echo "status=Success" >> $GITHUB_OUTPUT; echo "colour=3066993" >> $GITHUB_OUTPUT ;;
|
success) echo "status=Success" >> $GITHUB_OUTPUT; echo "colour=3066993" >> $GITHUB_OUTPUT ;;
|
||||||
failure) echo "status=Failure" >> $GITHUB_OUTPUT; echo "colour=15158332" >> $GITHUB_OUTPUT ;;
|
failure) echo "status=Failure" >> $GITHUB_OUTPUT; echo "colour=15158332" >> $GITHUB_OUTPUT ;;
|
||||||
cancelled) echo "status=Cancelled" >> $GITHUB_OUTPUT; echo "colour=10181046" >> $GITHUB_OUTPUT ;;
|
cancelled) echo "status=Cancelled" >> $GITHUB_OUTPUT; echo "colour=10181046" >> $GITHUB_OUTPUT ;;
|
||||||
*) echo "status=Skipped" >> $GITHUB_OUTPUT; echo "colour=9807270" >> $GITHUB_OUTPUT ;;
|
*) echo "status=Skipped" >> $GITHUB_OUTPUT; echo "colour=9807270" >> $GITHUB_OUTPUT ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Send notification
|
- name: Send Discord notification
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
WEBHOOK="${{ secrets.DISCORD_WEBHOOK }}"
|
WEBHOOK="${{ secrets.DISCORD_WEBHOOK }}"
|
||||||
|
|
||||||
@@ -336,7 +217,7 @@ jobs:
|
|||||||
{ "name": "Event", "value": "${{ github.event_name }}", "inline": true },
|
{ "name": "Event", "value": "${{ github.event_name }}", "inline": true },
|
||||||
{ "name": "Triggered by", "value": "${{ github.actor }}", "inline": true },
|
{ "name": "Triggered by", "value": "${{ github.actor }}", "inline": true },
|
||||||
{ "name": "Workflow", "value": "[${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
|
{ "name": "Workflow", "value": "[${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})", "inline": true }
|
||||||
]
|
],
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
181
.github/workflows/renovate-helm-custom-hooks.yml
vendored
181
.github/workflows/renovate-helm-custom-hooks.yml
vendored
@@ -1,181 +0,0 @@
|
|||||||
---
|
|
||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
|
||||||
name: Renovate Helm Hooks
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- 'charts/**'
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: renovate-helm-hooks-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
renovate-post-run:
|
|
||||||
name: Renovate Bump Chart Version
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
if: github.actor == 'renovate[bot]'
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
|
||||||
id: app-token
|
|
||||||
with:
|
|
||||||
app-id: 2138788
|
|
||||||
private-key: ${{ secrets.APP_SEERR_HELM_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
- name: Set up chart-testing
|
|
||||||
uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0
|
|
||||||
|
|
||||||
- name: Run chart-testing (list-changed)
|
|
||||||
id: list-changed
|
|
||||||
run: |
|
|
||||||
changed="$(ct list-changed --target-branch ${TARGET_BRANCH})"
|
|
||||||
if [[ -n "$changed" ]]; then
|
|
||||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "changed_list=${changed//$'\n'/ }" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
TARGET_BRANCH: ${{ github.event.repository.default_branch }}
|
|
||||||
|
|
||||||
- name: Bump chart version
|
|
||||||
if: steps.list-changed.outputs.changed == 'true'
|
|
||||||
env:
|
|
||||||
CHART: ${{ steps.list-changed.outputs.changed_list }}
|
|
||||||
run: |
|
|
||||||
if [[ ! -d "${CHART}" ]]; then
|
|
||||||
echo "${CHART} directory not found"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract current appVersion and chart version from Chart.yaml
|
|
||||||
APP_VERSION=$(grep -e "^appVersion:" "$CHART/Chart.yaml" | cut -d ":" -f 2 | tr -d '[:space:]' | tr -d '"')
|
|
||||||
CHART_VERSION=$(grep -e "^version:" "$CHART/Chart.yaml" | cut -d ":" -f 2 | tr -d '[:space:]' | tr -d '"')
|
|
||||||
|
|
||||||
# Extract major, minor and patch versions of appVersion
|
|
||||||
APP_MAJOR_VERSION=$(printf '%s' "$APP_VERSION" | cut -d "." -f 1)
|
|
||||||
APP_MINOR_VERSION=$(printf '%s' "$APP_VERSION" | cut -d "." -f 2)
|
|
||||||
APP_PATCH_VERSION=$(printf '%s' "$APP_VERSION" | cut -d "." -f 3)
|
|
||||||
|
|
||||||
# Extract major, minor and patch versions of chart version
|
|
||||||
CHART_MAJOR_VERSION=$(printf '%s' "$CHART_VERSION" | cut -d "." -f 1)
|
|
||||||
CHART_MINOR_VERSION=$(printf '%s' "$CHART_VERSION" | cut -d "." -f 2)
|
|
||||||
CHART_PATCH_VERSION=$(printf '%s' "$CHART_VERSION" | cut -d "." -f 3)
|
|
||||||
|
|
||||||
# Get previous appVersion from the base commit of the pull request
|
|
||||||
BASE_COMMIT=$(git merge-base origin/main HEAD)
|
|
||||||
PREV_APP_VERSION=$(git show "$BASE_COMMIT":"$CHART/Chart.yaml" | grep -e "^appVersion:" | cut -d ":" -f 2 | tr -d '[:space:]' | tr -d '"')
|
|
||||||
|
|
||||||
# Extract major, minor and patch versions of previous appVersion
|
|
||||||
PREV_APP_MAJOR_VERSION=$(printf '%s' "$PREV_APP_VERSION" | cut -d "." -f 1)
|
|
||||||
PREV_APP_MINOR_VERSION=$(printf '%s' "$PREV_APP_VERSION" | cut -d "." -f 2)
|
|
||||||
PREV_APP_PATCH_VERSION=$(printf '%s' "$PREV_APP_VERSION" | cut -d "." -f 3)
|
|
||||||
|
|
||||||
# Check if the major, minor, or patch version of appVersion has changed
|
|
||||||
if [[ "$APP_MAJOR_VERSION" != "$PREV_APP_MAJOR_VERSION" ]]; then
|
|
||||||
# Bump major version of the chart and reset minor and patch versions to 0
|
|
||||||
CHART_MAJOR_VERSION=$((CHART_MAJOR_VERSION+1))
|
|
||||||
CHART_MINOR_VERSION=0
|
|
||||||
CHART_PATCH_VERSION=0
|
|
||||||
elif [[ "$APP_MINOR_VERSION" != "$PREV_APP_MINOR_VERSION" ]]; then
|
|
||||||
# Bump minor version of the chart and reset patch version to 0
|
|
||||||
CHART_MINOR_VERSION=$((CHART_MINOR_VERSION+1))
|
|
||||||
CHART_PATCH_VERSION=0
|
|
||||||
elif [[ "$APP_PATCH_VERSION" != "$PREV_APP_PATCH_VERSION" ]]; then
|
|
||||||
# Bump patch version of the chart
|
|
||||||
CHART_PATCH_VERSION=$((CHART_PATCH_VERSION+1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update the chart version in Chart.yaml
|
|
||||||
CHART_NEW_VERSION="${CHART_MAJOR_VERSION}.${CHART_MINOR_VERSION}.${CHART_PATCH_VERSION}"
|
|
||||||
sed -i "s/^version:.*/version: ${CHART_NEW_VERSION}/" "$CHART/Chart.yaml"
|
|
||||||
|
|
||||||
- name: Ensure documentation is updated
|
|
||||||
if: steps.list-changed.outputs.changed == 'true'
|
|
||||||
uses: docker://jnorwood/helm-docs:v1.14.2@sha256:7e562b49ab6b1dbc50c3da8f2dd6ffa8a5c6bba327b1c6335cc15ce29267979c
|
|
||||||
|
|
||||||
- name: Commit changes
|
|
||||||
if: steps.list-changed.outputs.changed == 'true'
|
|
||||||
env:
|
|
||||||
CHART: ${{ steps.list-changed.outputs.changed_list }}
|
|
||||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
||||||
GITHUB_HEAD_REF: ${{ github.head_ref }}
|
|
||||||
run: |
|
|
||||||
# Define the target directory
|
|
||||||
TARGET_DIR="$CHART"
|
|
||||||
|
|
||||||
# Fetch deleted files in the target directory
|
|
||||||
DELETED_FILES=$(git diff --diff-filter=D --name-only HEAD -- "$TARGET_DIR")
|
|
||||||
|
|
||||||
# Fetch added/modified files in the target directory
|
|
||||||
MODIFIED_FILES=$(git diff --diff-filter=ACM --name-only HEAD -- "$TARGET_DIR")
|
|
||||||
|
|
||||||
# Create a temporary file for JSON output
|
|
||||||
FILE_CHANGES_JSON_FILE=$(mktemp)
|
|
||||||
|
|
||||||
# Initialize JSON structure in the file
|
|
||||||
echo '{ "deletions": [], "additions": [] }' > "$FILE_CHANGES_JSON_FILE"
|
|
||||||
|
|
||||||
# Add deletions
|
|
||||||
for file in $DELETED_FILES; do
|
|
||||||
jq --arg path "$file" '.deletions += [{"path": $path}]' "$FILE_CHANGES_JSON_FILE" > "$FILE_CHANGES_JSON_FILE.tmp"
|
|
||||||
mv "$FILE_CHANGES_JSON_FILE.tmp" "$FILE_CHANGES_JSON_FILE"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Add additions (new or modified files)
|
|
||||||
for file in $MODIFIED_FILES; do
|
|
||||||
BASE64_CONTENT=$(base64 -w 0 <"$file") # Encode file content
|
|
||||||
jq --arg path "$file" --arg content "$BASE64_CONTENT" \
|
|
||||||
'.additions += [{"path": $path, "contents": $content}]' "$FILE_CHANGES_JSON_FILE" > "$FILE_CHANGES_JSON_FILE.tmp"
|
|
||||||
mv "$FILE_CHANGES_JSON_FILE.tmp" "$FILE_CHANGES_JSON_FILE"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Create a temporary file for the final JSON payload
|
|
||||||
JSON_PAYLOAD_FILE=$(mktemp)
|
|
||||||
|
|
||||||
# Construct the final JSON using jq and store it in a file
|
|
||||||
jq -n --arg repo "$GITHUB_REPOSITORY" \
|
|
||||||
--arg branch "$GITHUB_HEAD_REF" \
|
|
||||||
--arg message "fix: post upgrade changes from renovate" \
|
|
||||||
--arg expectedOid "$GITHUB_SHA" \
|
|
||||||
--slurpfile fileChanges "$FILE_CHANGES_JSON_FILE" \
|
|
||||||
'{
|
|
||||||
query: "mutation ($input: CreateCommitOnBranchInput!) {
|
|
||||||
createCommitOnBranch(input: $input) {
|
|
||||||
commit {
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}",
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
branch: {
|
|
||||||
repositoryNameWithOwner: $repo,
|
|
||||||
branchName: $branch
|
|
||||||
},
|
|
||||||
message: { headline: $message },
|
|
||||||
fileChanges: $fileChanges[0],
|
|
||||||
expectedHeadOid: $expectedOid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}' > "$JSON_PAYLOAD_FILE"
|
|
||||||
|
|
||||||
# Call GitHub API
|
|
||||||
curl https://api.github.com/graphql -f \
|
|
||||||
-sSf -H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
||||||
--data "@$JSON_PAYLOAD_FILE"
|
|
||||||
|
|
||||||
# Clean up temporary files
|
|
||||||
rm "$FILE_CHANGES_JSON_FILE" "$JSON_PAYLOAD_FILE"
|
|
||||||
111
.github/workflows/seerr-labeller.yml
vendored
111
.github/workflows/seerr-labeller.yml
vendored
@@ -1,111 +0,0 @@
|
|||||||
---
|
|
||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
|
||||||
name: 'Seerr Labeller'
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [labeled, unlabeled, reopened]
|
|
||||||
issues:
|
|
||||||
types: [labeled, unlabeled, reopened]
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ai-generated-support:
|
|
||||||
if: >
|
|
||||||
github.event_name == 'pull_request_target' &&
|
|
||||||
(github.event.label.name == 'ai-generated' || (github.event.action == 'reopened' && contains(github.event.pull_request.labels.*.name, 'ai-generated')))
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
concurrency:
|
|
||||||
group: ai-generated-${{ github.event.pull_request.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GH_REPO: ${{ github.repository }}
|
|
||||||
NUMBER: ${{ github.event.pull_request.number }}
|
|
||||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
|
||||||
steps:
|
|
||||||
- name: Label added, comment and close pull request
|
|
||||||
if: github.event.action == 'labeled' && github.event.label.name == 'ai-generated'
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
BODY: >
|
|
||||||
:wave: @${{ env.PR_AUTHOR }}, thank you for your contribution!
|
|
||||||
|
|
||||||
However, this pull request has been closed because it appears to contain a significant amount of AI-generated code without sufficient human review or supervision.
|
|
||||||
|
|
||||||
AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight.
|
|
||||||
|
|
||||||
This pull request is also too large for effective human review. Please discuss with us on how to break down these changes into smaller, more focused PRs to ensure a thorough and efficient review process.
|
|
||||||
If you'd like to revise and resubmit your changes with careful review and cleanup, we'd be happy to take another look.
|
|
||||||
run: |
|
|
||||||
retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; }
|
|
||||||
retry gh pr comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true
|
|
||||||
retry gh pr close "$NUMBER" -R "$GH_REPO" || true
|
|
||||||
gh pr lock "$NUMBER" -R "$GH_REPO" -r "spam" || true
|
|
||||||
|
|
||||||
- name: Label removed, reopen and unlock pull request
|
|
||||||
if: github.event.action == 'unlabeled' && github.event.label.name == 'ai-generated'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; }
|
|
||||||
retry gh pr reopen "$NUMBER" -R "$GH_REPO" || true
|
|
||||||
gh pr unlock "$NUMBER" -R "$GH_REPO" || true
|
|
||||||
|
|
||||||
- name: Remove AI-generated label on manual reopen
|
|
||||||
if: github.event.action == 'reopened'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
gh pr edit "$NUMBER" -R "$GH_REPO" --remove-label "ai-generated" || true
|
|
||||||
gh pr unlock "$NUMBER" -R "$GH_REPO" || true
|
|
||||||
|
|
||||||
support:
|
|
||||||
if: >
|
|
||||||
github.event_name == 'issues' &&
|
|
||||||
(github.event.label.name == 'support' ||
|
|
||||||
(github.event.action == 'reopened' && contains(github.event.issue.labels.*.name, 'support')))
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
concurrency:
|
|
||||||
group: support-${{ github.event.issue.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GH_REPO: ${{ github.repository }}
|
|
||||||
NUMBER: ${{ github.event.issue.number }}
|
|
||||||
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
|
||||||
steps:
|
|
||||||
- name: Label added, comment and close issue
|
|
||||||
if: github.event.action == 'labeled' && github.event.label.name == 'support'
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
BODY: >
|
|
||||||
:wave: @${{ env.ISSUE_AUTHOR }}, we use the issue tracker exclusively
|
|
||||||
for bug reports and feature requests. However, this issue appears
|
|
||||||
to be a support request. Please use our support channels
|
|
||||||
to get help with Seerr.
|
|
||||||
|
|
||||||
- [Discord](https://discord.gg/seerr)
|
|
||||||
run: |
|
|
||||||
retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; }
|
|
||||||
retry gh issue comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true
|
|
||||||
retry gh issue close "$NUMBER" -R "$GH_REPO" || true
|
|
||||||
gh issue lock "$NUMBER" -R "$GH_REPO" -r "off_topic" || true
|
|
||||||
|
|
||||||
- name: Label removed, reopen and unlock issue
|
|
||||||
if: github.event.action == 'unlabeled' && github.event.label.name == 'support'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; }
|
|
||||||
retry gh issue reopen "$NUMBER" -R "$GH_REPO" || true
|
|
||||||
gh issue unlock "$NUMBER" -R "$GH_REPO" || true
|
|
||||||
|
|
||||||
- name: Remove support label on manual reopen
|
|
||||||
if: github.event.action == 'reopened'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
gh issue edit "$NUMBER" -R "$GH_REPO" --remove-label "support" || true
|
|
||||||
gh issue unlock "$NUMBER" -R "$GH_REPO" || true
|
|
||||||
58
.github/workflows/support.yml
vendored
Normal file
58
.github/workflows/support.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||||
|
name: 'Support requests'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled, unlabeled, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: support-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
support:
|
||||||
|
if: github.event.label.name == 'support' || github.event.action == 'reopened'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
NUMBER: ${{ github.event.issue.number }}
|
||||||
|
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
||||||
|
steps:
|
||||||
|
- name: Label added, comment and close issue
|
||||||
|
if: github.event.action == 'labeled' && github.event.label.name == 'support'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BODY: >
|
||||||
|
:wave: @${{ env.ISSUE_AUTHOR }}, we use the issue tracker exclusively
|
||||||
|
for bug reports and feature requests. However, this issue appears
|
||||||
|
to be a support request. Please use our support channels
|
||||||
|
to get help with Seerr.
|
||||||
|
|
||||||
|
- [Discord](https://discord.gg/seerr)
|
||||||
|
run: |
|
||||||
|
retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; }
|
||||||
|
retry gh issue comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true
|
||||||
|
retry gh issue close "$NUMBER" -R "$GH_REPO" || true
|
||||||
|
gh issue lock "$NUMBER" -R "$GH_REPO" -r "off_topic" || true
|
||||||
|
|
||||||
|
- name: Reopened or label removed, unlock issue
|
||||||
|
if: github.event.action == 'unlabeled' && github.event.label.name == 'support'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; }
|
||||||
|
retry gh issue reopen "$NUMBER" -R "$GH_REPO" || true
|
||||||
|
gh issue unlock "$NUMBER" -R "$GH_REPO" || true
|
||||||
|
|
||||||
|
- name: Remove support label on manual reopen
|
||||||
|
if: github.event.action == 'reopened'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
gh issue edit "$NUMBER" -R "$GH_REPO" --remove-label "support" || true
|
||||||
|
gh issue unlock "$NUMBER" -R "$GH_REPO" || true
|
||||||
2
.github/workflows/test-docs-deploy.yml
vendored
2
.github/workflows/test-docs-deploy.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
|
|
||||||
- name: Pnpm Setup
|
- name: Pnpm Setup
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
- name: Get pnpm store directory
|
||||||
shell: sh
|
shell: sh
|
||||||
|
|||||||
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@@ -56,6 +56,6 @@ jobs:
|
|||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
|
|
||||||
- name: Upload SARIF to code scanning
|
- name: Upload SARIF to code scanning
|
||||||
uses: github/codeql-action/upload-sarif@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||||
with:
|
with:
|
||||||
sarif_file: trivy.sarif
|
sarif_file: trivy.sarif
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -71,6 +71,3 @@ tsconfig.tsbuildinfo
|
|||||||
|
|
||||||
# Config Cache Directory
|
# Config Cache Directory
|
||||||
config/cache
|
config/cache
|
||||||
|
|
||||||
# Docker compose
|
|
||||||
compose.override.yaml
|
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ WORKDIR /app
|
|||||||
FROM base AS prod-deps
|
FROM base AS prod-deps
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store CI=true pnpm install --prod --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store CI=true pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
FROM base AS build
|
FROM base as build
|
||||||
|
|
||||||
ARG COMMIT_TAG
|
|
||||||
ENV COMMIT_TAG=${COMMIT_TAG}
|
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
case "${TARGETPLATFORM}" in \
|
case "${TARGETPLATFORM}" in \
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -7,8 +7,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
||||||
<a href="https://hub.docker.com/r/seerr/seerr"><img src="https://img.shields.io/docker/pulls/seerr/seerr" alt="Docker pulls"></a>
|
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
|
||||||
<a href="http://translate.seerr.dev/engage/seerr/"><img src="http://translate.seerr.dev/widget/seerr/seerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
<a href="http://translate.jellyseerr.dev/engage/jellyseerr/"><img src="http://translate.jellyseerr.dev/widget/jellyseerr/jellyseerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||||
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
|
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
|
||||||
|
|
||||||
**Seerr** 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/)**.
|
**Seerr** 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/)**.
|
||||||
@@ -36,6 +36,12 @@ Check out our documentation for instructions on how to install and run Seerr:
|
|||||||
|
|
||||||
https://docs.seerr.dev/getting-started/
|
https://docs.seerr.dev/getting-started/
|
||||||
|
|
||||||
|
### Packages:
|
||||||
|
|
||||||
|
Archlinux: [AUR](https://aur.archlinux.org/packages/jellyseerr)
|
||||||
|
|
||||||
|
Nix: [Nixpkg](https://search.nixos.org/packages?channel=unstable&show=jellyseerr)
|
||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||
<img src="./public/preview.jpg">
|
<img src="./public/preview.jpg">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ name: seerr-chart
|
|||||||
description: Seerr helm chart for Kubernetes
|
description: Seerr helm chart for Kubernetes
|
||||||
type: application
|
type: application
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
# renovate: image=ghcr.io/seerr-team/seerr
|
# renovate: image=ghcr.io/fallenbagel/jellyseerr
|
||||||
appVersion: '2.7.3'
|
appVersion: '2.7.3'
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: Seerr Team
|
- name: Seerr Team
|
||||||
|
|||||||
@@ -20,15 +20,11 @@ Seerr helm chart for Kubernetes
|
|||||||
|
|
||||||
Kubernetes: `>=1.23.0-0`
|
Kubernetes: `>=1.23.0-0`
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Refer to [https://docs.seerr.dev/getting-started/kubernetes](Seerr kubernetes documentation)
|
|
||||||
|
|
||||||
## Update Notes
|
## Update Notes
|
||||||
|
|
||||||
### Updating to 3.0.0
|
### Updating to 3.0.0
|
||||||
|
|
||||||
Nothing has changed; we just rebranded the `jellyseerr` Helm chart to `seerr` 🥳 refer to our [Migration guide](https://docs.seerr.dev/migration-guide).
|
Nothing has changed; we just rebranded the `jellyseerr` Helm chart to `seerr` 🥳.
|
||||||
|
|
||||||
### Updating to 2.7.0
|
### Updating to 2.7.0
|
||||||
|
|
||||||
@@ -70,20 +66,12 @@ If `replicaCount` value was used - remove it. Helm update should work fine after
|
|||||||
| nodeSelector | object | `{}` | |
|
| nodeSelector | object | `{}` | |
|
||||||
| podAnnotations | object | `{}` | |
|
| podAnnotations | object | `{}` | |
|
||||||
| podLabels | object | `{}` | |
|
| podLabels | object | `{}` | |
|
||||||
| podSecurityContext.fsGroup | int | `1000` | |
|
| podSecurityContext | object | `{}` | |
|
||||||
| podSecurityContext.fsGroupChangePolicy | string | `"OnRootMismatch"` | |
|
|
||||||
| probes.livenessProbe | object | `{}` | Configure liveness probe |
|
| probes.livenessProbe | object | `{}` | Configure liveness probe |
|
||||||
| probes.readinessProbe | object | `{}` | Configure readiness probe |
|
| probes.readinessProbe | object | `{}` | Configure readiness probe |
|
||||||
| probes.startupProbe | string | `nil` | Configure startup probe |
|
| probes.startupProbe | string | `nil` | Configure startup probe |
|
||||||
| resources | object | `{}` | |
|
| resources | object | `{}` | |
|
||||||
| securityContext.allowPrivilegeEscalation | bool | `false` | |
|
| securityContext | object | `{}` | |
|
||||||
| securityContext.capabilities.drop[0] | string | `"ALL"` | |
|
|
||||||
| securityContext.privileged | bool | `false` | |
|
|
||||||
| securityContext.readOnlyRootFilesystem | bool | `false` | |
|
|
||||||
| securityContext.runAsGroup | int | `1000` | |
|
|
||||||
| securityContext.runAsNonRoot | bool | `true` | |
|
|
||||||
| securityContext.runAsUser | int | `1000` | |
|
|
||||||
| securityContext.seccompProfile.type | string | `"RuntimeDefault"` | |
|
|
||||||
| service.port | int | `80` | |
|
| service.port | int | `80` | |
|
||||||
| service.type | string | `"ClusterIP"` | |
|
| service.type | string | `"ClusterIP"` | |
|
||||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||||
|
|||||||
@@ -14,15 +14,11 @@
|
|||||||
|
|
||||||
{{ template "chart.requirementsSection" . }}
|
{{ template "chart.requirementsSection" . }}
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Refer to [https://docs.seerr.dev/getting-started/kubernetes](Seerr kubernetes documentation)
|
|
||||||
|
|
||||||
## Update Notes
|
## Update Notes
|
||||||
|
|
||||||
### Updating to 3.0.0
|
### Updating to 3.0.0
|
||||||
|
|
||||||
Nothing has changed; we just rebranded the `jellyseerr` Helm chart to `seerr` 🥳 refer to our [Migration guide](https://docs.seerr.dev/migration-guide).
|
Nothing change we just rebranded `jellyseerr` helm-chart to `seerr` :)
|
||||||
|
|
||||||
### Updating to 2.7.0
|
### Updating to 2.7.0
|
||||||
|
|
||||||
|
|||||||
@@ -50,22 +50,16 @@ serviceAccount:
|
|||||||
podAnnotations: {}
|
podAnnotations: {}
|
||||||
podLabels: {}
|
podLabels: {}
|
||||||
|
|
||||||
podSecurityContext:
|
podSecurityContext: {}
|
||||||
fsGroup: 1000
|
# fsGroup: 2000
|
||||||
fsGroupChangePolicy: OnRootMismatch
|
|
||||||
|
|
||||||
securityContext:
|
securityContext: {}
|
||||||
allowPrivilegeEscalation: false
|
# capabilities:
|
||||||
capabilities:
|
# drop:
|
||||||
drop:
|
# - ALL
|
||||||
- ALL
|
# readOnlyRootFilesystem: true
|
||||||
readOnlyRootFilesystem: false
|
# runAsNonRoot: true
|
||||||
runAsNonRoot: true
|
# runAsUser: 1000
|
||||||
privileged: false
|
|
||||||
runAsUser: 1000
|
|
||||||
runAsGroup: 1000
|
|
||||||
seccompProfile:
|
|
||||||
type: RuntimeDefault
|
|
||||||
|
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '5432:5432'
|
- '5432:5432'
|
||||||
volumes:
|
volumes:
|
||||||
- postgres:/var/lib/postgresql
|
- postgres:var/lib/postgresql/18/docker
|
||||||
volumes:
|
volumes:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ DB_LOG_QUERIES="false" # (optional) Whether to log the DB queries for debugging.
|
|||||||
## PostgreSQL Options
|
## PostgreSQL Options
|
||||||
|
|
||||||
:::caution
|
:::caution
|
||||||
When migrating Postgres from version 17 to 18 in Docker, note that the data mount point has changed. Instead of using `/var/lib/postgresql/data`, the correct mount path is now `/var/lib/postgresql`.
|
When migrating Postgres from version 17 to 18 in Docker, note that the data mount point has changed. Instead of using `/var/lib/postgresql/data`, the correct mount path is now `/var/lib/postgresql/18/docker`.
|
||||||
Refer to the [PostgreSQL Docker documentation](https://hub.docker.com/_/postgres/#pgdata) to learn how to migrate or opt out of this change.
|
Refer to the [PostgreSQL Docker documentation](https://hub.docker.com/_/postgres/#pgdata) to learn how to migrate or opt out of this change.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
@@ -266,36 +266,3 @@ Add the following Location block to your existing Server configuration.
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## HAProxy (v3)
|
|
||||||
|
|
||||||
:::warning
|
|
||||||
This is a third-party documentation maintained by the community. We can't provide support for this setup and are unable to test it.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Add the following frontend and backend configurations for your seerr instance:
|
|
||||||
```haproxy
|
|
||||||
frontend seerr-frontend
|
|
||||||
bind 0.0.0.0:80
|
|
||||||
bind 0.0.0.0:443 ssl crt /etc/ssl/private/seerr.example.com.pem
|
|
||||||
mode http
|
|
||||||
log global
|
|
||||||
option httplog
|
|
||||||
option http-keep-alive
|
|
||||||
http-request set-header X-Real-IP %[src]
|
|
||||||
option forwardfor
|
|
||||||
acl seerr hdr(host) -i seerr.example.com
|
|
||||||
redirect scheme https code 301 if !{ ssl_fc }
|
|
||||||
use_backend seerr-backend if seerr
|
|
||||||
|
|
||||||
backend seerr-backend
|
|
||||||
mode http
|
|
||||||
log global
|
|
||||||
option httplog
|
|
||||||
http-response set-header Strict-Transport-Security max-age=15552000
|
|
||||||
option httpchk GET /api/v1/status
|
|
||||||
timeout connect 30000
|
|
||||||
timeout server 30000
|
|
||||||
retries 3
|
|
||||||
server seerr 127.0.0.1:5055 check inter 1000
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
---
|
---
|
||||||
title: AUR (Advanced)
|
title: AUR (Arch User Repository)
|
||||||
description: Install Seerr using the Arch User Repository
|
description: Install Seerr using the Arch User Repository
|
||||||
sidebar_position: 2
|
sidebar_position: 4
|
||||||
---
|
---
|
||||||
|
|
||||||
# AUR
|
# AUR (Arch User Repository)
|
||||||
:::warning
|
|
||||||
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
:::note Disclaimer
|
||||||
|
This AUR package is not maintained by us but by a third party. Please refer to the maintainer for any issues.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::warning
|
:::info
|
||||||
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
|
This method is not recommended for most users. It is intended for advanced users who are using Arch Linux or an Arch-based distribution.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -23,12 +24,12 @@ import TabItem from '@theme/TabItem';
|
|||||||
<Tabs groupId="aur-methods" queryString>
|
<Tabs groupId="aur-methods" queryString>
|
||||||
<TabItem value="yay" label="yay">
|
<TabItem value="yay" label="yay">
|
||||||
```bash
|
```bash
|
||||||
yay -S seerr
|
yay -S jellyseerr
|
||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="paru" label="paru">
|
<TabItem value="paru" label="paru">
|
||||||
```bash
|
```bash
|
||||||
paru -S seerr
|
paru -S jellyseerr
|
||||||
```
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -38,5 +39,5 @@ After installing Seerr, configure it by visiting the web UI at `http://[address]
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
You can find the environment file at `/etc/conf.d/seerr` and the service file at `/etc/systemd/system/seerr.service`.
|
You can find the environment file at `/etc/conf.d/jellyseerr` and the service file at `/etc/systemd/system/jellyseerr.service`.
|
||||||
:::
|
:::
|
||||||
@@ -24,10 +24,10 @@ import TabItem from '@theme/TabItem';
|
|||||||
```bash
|
```bash
|
||||||
sudo mkdir -p /opt/seerr && cd /opt/seerr
|
sudo mkdir -p /opt/seerr && cd /opt/seerr
|
||||||
```
|
```
|
||||||
2. Clone the Seerr repository and checkout the main branch:
|
2. Clone the Seerr repository and checkout the develop branch:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/seerr-team/seerr.git
|
git clone https://github.com/fallenbagel/jellyseerr.git
|
||||||
cd seerr
|
cd jellyseerr
|
||||||
git checkout main
|
git checkout main
|
||||||
```
|
```
|
||||||
3. Install the dependencies:
|
3. Install the dependencies:
|
||||||
@@ -199,9 +199,9 @@ pm2 status seerr
|
|||||||
mkdir C:\seerr
|
mkdir C:\seerr
|
||||||
cd C:\seerr
|
cd C:\seerr
|
||||||
```
|
```
|
||||||
2. Clone the Seerr repository and checkout the main branch:
|
2. Clone the Seerr repository and checkout the develop branch:
|
||||||
```powershell
|
```powershell
|
||||||
git clone https://github.com/seerr-team/seerr.git .
|
git clone https://github.com/fallenbagel/jellyseerr.git .
|
||||||
git checkout main
|
git checkout main
|
||||||
```
|
```
|
||||||
3. Install the dependencies:
|
3. Install the dependencies:
|
||||||
|
|||||||
@@ -11,16 +11,6 @@ Details on how to install Docker can be found on the [official Docker website](h
|
|||||||
Refer to [Configuring Databases](/extending-seerr/database-config#postgresql-options) for details on how to configure your database.
|
Refer to [Configuring Databases](/extending-seerr/database-config#postgresql-options) for details on how to configure your database.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::info
|
|
||||||
An alternative Docker image is available on Docker Hub for this project. You can find it at [Docker Hub Repository Link](https://hub.docker.com/r/seerr/seerr)
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::info
|
|
||||||
All official Seerr images are cryptographically signed and include a verified [Software Bill of Materials (SBOM)](https://cyclonedx.org/).
|
|
||||||
|
|
||||||
To confirm that the container image you are using is authentic and unmodified, please refer to the [Verifying Signed Artifacts](/using-seerr/advanced/verifying-signed-artifacts#verifying-signed-images) guide.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Unix (Linux, macOS)
|
## Unix (Linux, macOS)
|
||||||
:::warning
|
:::warning
|
||||||
Be sure to replace `/path/to/appdata/config` in the below examples with a valid host directory path. If this volume mount is not configured correctly, your Seerr settings/data will not be persisted when the container is recreated (e.g., when updating the image or rebooting your machine).
|
Be sure to replace `/path/to/appdata/config` in the below examples with a valid host directory path. If this volume mount is not configured correctly, your Seerr settings/data will not be persisted when the container is recreated (e.g., when updating the image or rebooting your machine).
|
||||||
@@ -48,7 +38,7 @@ docker run -d \
|
|||||||
-p 5055:5055 \
|
-p 5055:5055 \
|
||||||
-v /path/to/appdata/config:/app/config \
|
-v /path/to/appdata/config:/app/config \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
ghcr.io/seerr-team/seerr:latest
|
ghcr.io/seerr-team/seerr
|
||||||
```
|
```
|
||||||
|
|
||||||
The argument `-e PORT=5055` is optional.
|
The argument `-e PORT=5055` is optional.
|
||||||
@@ -72,7 +62,7 @@ docker stop seerr && docker rm seerr
|
|||||||
```
|
```
|
||||||
Pull the latest image:
|
Pull the latest image:
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/seerr-team/seerr:latest
|
docker pull ghcr.io/seerr-team/seerr
|
||||||
```
|
```
|
||||||
Finally, run the container with the same parameters originally used to create the container:
|
Finally, run the container with the same parameters originally used to create the container:
|
||||||
```bash
|
```bash
|
||||||
@@ -135,6 +125,15 @@ You may alternatively use a third-party mechanism like [dockge](https://github.c
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|
||||||
|
## Unraid
|
||||||
|
|
||||||
|
1. Ensure you have the **Community Applications** plugin installed.
|
||||||
|
2. Inside the **Community Applications** app store, search for **Seerr**.
|
||||||
|
3. Click the **Install Button**.
|
||||||
|
4. On the following **Add Container** screen, make changes to the **Host Port** and **Host Path 1** \(Appdata\) as needed.
|
||||||
|
5. Click apply and access "Seerr" at your `<ServerIP:HostPort>` in a web browser.
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
Please refer to the [Docker Desktop for Windows user manual](https://docs.docker.com/docker-for-windows/) for details on how to install Docker on Windows. There is no need to install a Linux distro if using named volumes like in the example below.
|
Please refer to the [Docker Desktop for Windows user manual](https://docs.docker.com/docker-for-windows/) for details on how to install Docker on Windows. There is no need to install a Linux distro if using named volumes like in the example below.
|
||||||
@@ -166,7 +165,7 @@ docker run -d \
|
|||||||
-p 5055:5055 \
|
-p 5055:5055 \
|
||||||
-v seerr-data:/app/config \
|
-v seerr-data:/app/config \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
ghcr.io/seerr-team/seerr:latest
|
ghcr.io/seerr-team/seerr
|
||||||
```
|
```
|
||||||
|
|
||||||
The argument `-e PORT=5055` is optional.
|
The argument `-e PORT=5055` is optional.
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
---
|
---
|
||||||
title: Kubernetes (Advanced)
|
title: Kubernetes (Advanced)
|
||||||
description: Install Seerr in Kubernetes
|
description: Install Jellyseerr in Kubernetes
|
||||||
sidebar_position: 3
|
sidebar_position: 5
|
||||||
---
|
---
|
||||||
# Kubernetes
|
# Kubernetes
|
||||||
:::warning
|
|
||||||
This method is not recommended for most users. It is intended for advanced users who are using Kubernetes.
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
All official Seerr charts are cryptographically signed and include a verified [Software Bill of Materials (SBOM)](https://cyclonedx.org/).
|
This method is not recommended for most users. It is intended for advanced users who are using Kubernetes.
|
||||||
|
|
||||||
To confirm that the chart you are using is authentic and unmodified, please refer to the [Verifying Signed Artifacts](/using-seerr/advanced/verifying-signed-artifacts#verifying-signed-helm-charts) guide.
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
```console
|
```console
|
||||||
helm install seerr oci://ghcr.io/seerr-team/seerr/seerr-chart
|
helm install seerr oci://ghcr.io/seerr-team/seerr/seerr-chart
|
||||||
```
|
```
|
||||||
Helm values can be found in the Seerr repository under [charts/seerr-chart/README.md](https://github.com/seerr-team/seerr/tree/develop/charts/seerr-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) :
|
Verify the signature with [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) (replace [tag], with the TAG you want to verify) :
|
||||||
```console
|
```console
|
||||||
cosign verify ghcr.io/seerr-team/seerr/seerr-chart:[tag] --certificate-identity=https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main --certificate-oidc-issuer=https://token.actions.githubusercontent.com
|
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
|
||||||
```
|
```
|
||||||
|
|||||||
271
docs/getting-started/nixpkg.mdx
Normal file
271
docs/getting-started/nixpkg.mdx
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
---
|
||||||
|
title: Nix Package Manager (Advanced)
|
||||||
|
description: Install Seerr using Nix
|
||||||
|
sidebar_position: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
import { SeerrVersion, NixpkgVersion } from '@site/src/components/SeerrVersion';
|
||||||
|
import Admonition from '@theme/Admonition';
|
||||||
|
import Tabs from '@theme/Tabs';
|
||||||
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
|
# Nix Package Manager (Advanced)
|
||||||
|
:::info
|
||||||
|
This method is not recommended for most users. It is intended for advanced users who are using Nix as their package manager.
|
||||||
|
:::
|
||||||
|
|
||||||
|
export const VersionMismatchWarning = () => {
|
||||||
|
let seerrVersion = null;
|
||||||
|
let nixpkgVersions = null;
|
||||||
|
try {
|
||||||
|
seerrVersion = SeerrVersion();
|
||||||
|
nixpkgVersions = NixpkgVersion();
|
||||||
|
} catch (err) {
|
||||||
|
return (
|
||||||
|
<Admonition type="error">
|
||||||
|
Failed to load version information. Error: {err.message || JSON.stringify(err)}
|
||||||
|
</Admonition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nixpkgVersions || nixpkgVersions.error) {
|
||||||
|
return (
|
||||||
|
<Admonition type="error">
|
||||||
|
Failed to fetch Nixpkg versions: {nixpkgVersions?.error || 'Unknown error'}
|
||||||
|
</Admonition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUnstableUpToDate = seerrVersion === nixpkgVersions.unstable;
|
||||||
|
const isStableUpToDate = seerrVersion === nixpkgVersions.stable;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!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{seerrVersion}</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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
<VersionMismatchWarning />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
To get up and running with jellyseerr using Nix, you can add the following to your `configuration.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
services.jellyseerr.enable = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
services.jellyseerr = {
|
||||||
|
enable = true;
|
||||||
|
port = 5055;
|
||||||
|
openFirewall = true;
|
||||||
|
package = pkgs.jellyseerr; # Use the unstable package if stable is not up-to-date
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</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
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
cfg = config.services.jellyseerr;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
meta.maintainers = [ maintainers.camillemndn ];
|
||||||
|
|
||||||
|
disabledModules = [ "services/misc/jellyseerr.nix" ];
|
||||||
|
|
||||||
|
options.services.jellyseerr = {
|
||||||
|
enable = mkEnableOption ''Jellyseerr, a requests manager for Jellyfin'';
|
||||||
|
|
||||||
|
openFirewall = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''Open port in the firewall for the Jellyseerr web interface.'';
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 5055;
|
||||||
|
description = ''The port which the Jellyseerr web UI should listen to.'';
|
||||||
|
};
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
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 {
|
||||||
|
systemd.services.jellyseerr = {
|
||||||
|
description = "Jellyseerr, a requests manager for Jellyfin";
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
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";
|
||||||
|
DynamicUser = true;
|
||||||
|
ExecStart = "${cfg.package}/bin/jellyseerr";
|
||||||
|
BindPaths = [ "/var/lib/jellyseerr/:${cfg.package}/libexec/jellyseerr/config/" ];
|
||||||
|
Restart = "on-failure";
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
PrivateTmp = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RemoveIPC = true;
|
||||||
|
PrivateMounts = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.port ]; };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Then, import the module into your `configuration.nix`:
|
||||||
|
```nix
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [ ./modules/jellyseerr.nix ];
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
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 Seerr by visiting `http://localhost:5055` in your web browser.
|
||||||
|
:::
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
title: Third-party Installation Methods
|
|
||||||
---
|
|
||||||
import DocCardList from '@theme/DocCardList';
|
|
||||||
|
|
||||||
:::warning
|
|
||||||
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
|
||||||
:::
|
|
||||||
|
|
||||||
|
|
||||||
<DocCardList />
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
title: Nix Package Manager (Advanced)
|
|
||||||
description: Install Seerr using Nixpkgs
|
|
||||||
sidebar_position: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
import { SeerrVersion, NixpkgVersion } from '@site/src/components/SeerrVersion';
|
|
||||||
import Admonition from '@theme/Admonition';
|
|
||||||
import Tabs from '@theme/Tabs';
|
|
||||||
import TabItem from '@theme/TabItem';
|
|
||||||
|
|
||||||
# Nix Package Manager
|
|
||||||
:::warning
|
|
||||||
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Refer to [NixOS documentation](https://search.nixos.org/options?channel=25.05&query=seerr)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
title: Unraid (Advanced)
|
|
||||||
description: Install Seerr using Unraid
|
|
||||||
sidebar_position: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
# Unraid
|
|
||||||
:::warning
|
|
||||||
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::warning
|
|
||||||
This method is not recommended for most users. It is intended for advanced users who are using Unraid.
|
|
||||||
:::
|
|
||||||
|
|
||||||
1. Ensure you have the **Community Applications** plugin installed.
|
|
||||||
2. Inside the **Community Applications** app store, search for **Seerr**.
|
|
||||||
3. Click the **Install Button**.
|
|
||||||
4. On the following **Add Container** screen, make changes to the **Host Port** and **Host Path 1** \(Appdata\) as needed.
|
|
||||||
5. Click apply and access "Seerr" at your `<ServerIP:HostPort>` in a web browser.
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
---
|
|
||||||
title: Migration guide
|
|
||||||
---
|
|
||||||
|
|
||||||
import Tabs from '@theme/Tabs';
|
|
||||||
import TabItem from '@theme/TabItem';
|
|
||||||
|
|
||||||
Whether you come from Overseerr or Jellyseerr, you don't need to perform any manual migration steps, your instance will automatically be migrated to Seerr.
|
|
||||||
This migration will run automatically the first time you start your instance using the Seerr codebase (Docker image or source build or Kubernetes, etc.).
|
|
||||||
An additional migration will happen for Overseerr users, to migrate their configuration to the new codebase.
|
|
||||||
|
|
||||||
:::warning
|
|
||||||
Before doing anything you should backup your existing instance so that you can rollback in case something goes wrong.
|
|
||||||
See [Backups](/using-seerr/backups) for details on how to properly backup your instance.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
Refer to [Seerr Docker Documentation](/getting-started/docker), all of our examples have been updated to reflect the below change.
|
|
||||||
|
|
||||||
Changes :
|
|
||||||
- Renamed all references from `overseerr` or `jellyseerr` to `seerr`.
|
|
||||||
- The container image reference has been updated.
|
|
||||||
- The container can now be run as a non-root user (`node` user); remove the `user` directive if you have configured it.
|
|
||||||
- The container no longer provides an init process, so you must configure it by adding `init: true` for Docker Compose or `--init` for the Docker CLI.
|
|
||||||
|
|
||||||
:::info
|
|
||||||
**Config folder permissions**: Since the container now runs as the `node` user (UID 1000), you must ensure your config folder has the correct permissions. The `node` user must have read and write access to the `/app/config` directory.
|
|
||||||
|
|
||||||
If you're migrating from a previous installation, you may need to update the ownership of your config folder:
|
|
||||||
```bash
|
|
||||||
sudo chown -R 1000:1000 /path/to/appdata/config
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures the `node` user (UID 1000) owns the config directory and can read and write to it.
|
|
||||||
:::
|
|
||||||
|
|
||||||
### Unix
|
|
||||||
|
|
||||||
Summary of changes :
|
|
||||||
<Tabs groupId="docker-methods" queryString>
|
|
||||||
<TabItem value="docker-compose" label="Docker compose">
|
|
||||||
```yaml {3-6}
|
|
||||||
---
|
|
||||||
services:
|
|
||||||
seerr:
|
|
||||||
image: ghcr.io/seerr-team/seerr:latest
|
|
||||||
init: true
|
|
||||||
container_name: seerr
|
|
||||||
environment:
|
|
||||||
- LOG_LEVEL=debug
|
|
||||||
- TZ=Asia/Tashkent
|
|
||||||
- PORT=5055 #optional
|
|
||||||
ports:
|
|
||||||
- 5055:5055
|
|
||||||
volumes:
|
|
||||||
- /path/to/appdata/config:/app/config
|
|
||||||
healthcheck:
|
|
||||||
test: wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/status || exit 1
|
|
||||||
start_period: 20s
|
|
||||||
timeout: 3s
|
|
||||||
interval: 15s
|
|
||||||
retries: 3
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="docker-cli" label="Docker CLI">
|
|
||||||
```bash {2-3,10}
|
|
||||||
docker run -d \
|
|
||||||
--name seerr \
|
|
||||||
--init \
|
|
||||||
-e LOG_LEVEL=debug \
|
|
||||||
-e TZ=Asia/Tashkent \
|
|
||||||
-e PORT=5055 \
|
|
||||||
-p 5055:5055 \
|
|
||||||
-v /path/to/appdata/config:/app/config \
|
|
||||||
--restart unless-stopped \
|
|
||||||
ghcr.io/seerr-team/seerr:latest
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
Summary of changes :
|
|
||||||
<Tabs groupId="docker-methods" queryString>
|
|
||||||
<TabItem value="docker-compose" label="Docker compose">
|
|
||||||
```yaml {3-6,13,23}
|
|
||||||
---
|
|
||||||
services:
|
|
||||||
seerr:
|
|
||||||
image: ghcr.io/seerr-team/seerr:latest
|
|
||||||
init: true
|
|
||||||
container_name: seerr
|
|
||||||
environment:
|
|
||||||
- LOG_LEVEL=debug
|
|
||||||
- TZ=Asia/Tashkent
|
|
||||||
ports:
|
|
||||||
- 5055:5055
|
|
||||||
volumes:
|
|
||||||
- seerr-data:/app/config
|
|
||||||
healthcheck:
|
|
||||||
test: wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/status || exit 1
|
|
||||||
start_period: 20s
|
|
||||||
timeout: 3s
|
|
||||||
interval: 15s
|
|
||||||
retries: 3
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
seerr-data:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="docker-cli" label="Docker CLI">
|
|
||||||
```bash {2-3,8,10}
|
|
||||||
docker run -d \
|
|
||||||
--name seerr \
|
|
||||||
--init \
|
|
||||||
-e LOG_LEVEL=debug \
|
|
||||||
-e TZ=Asia/Tashkent \
|
|
||||||
-e PORT=5055 \
|
|
||||||
-p 5055:5055 \
|
|
||||||
-v seerr-data:/app/config \
|
|
||||||
--restart unless-stopped \
|
|
||||||
ghcr.io/seerr-team/seerr:latest
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Kubernetes
|
|
||||||
Refer to [Seerr Kubernetes Documentation](/getting-started/kubernetes), all of our examples have been updated to reflect the below change.
|
|
||||||
|
|
||||||
Changes :
|
|
||||||
- All references to `jellyseerr` have been renamed to `seerr` in the manifests.
|
|
||||||
- The container image reference has been updated.
|
|
||||||
- The default `securityContext` and `podSecurityContext` have been updated to support running the container without root permissions.
|
|
||||||
|
|
||||||
Summary of changes :
|
|
||||||
<Tabs groupId="kubernetes-values" queryString>
|
|
||||||
<TabItem value="old" label="Old values">
|
|
||||||
```yaml
|
|
||||||
image:
|
|
||||||
repository: fallenbagel/jellyseerr
|
|
||||||
podSecurityContext: {}
|
|
||||||
securityContext: {}
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="new" label="New values">
|
|
||||||
```yaml
|
|
||||||
image:
|
|
||||||
repository: seerr-team/seerr
|
|
||||||
podSecurityContext:
|
|
||||||
fsGroup: 1000
|
|
||||||
fsGroupChangePolicy: OnRootMismatch
|
|
||||||
securityContext:
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
capabilities:
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
readOnlyRootFilesystem: false
|
|
||||||
runAsNonRoot: true
|
|
||||||
privileged: false
|
|
||||||
runAsUser: 1000
|
|
||||||
runAsGroup: 1000
|
|
||||||
seccompProfile:
|
|
||||||
type: RuntimeDefault
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
@@ -103,7 +103,7 @@ If you can't change your DNS servers or force IPV4 resolution, you can use Seerr
|
|||||||
|
|
||||||
In some places (like China), the ISP blocks not only the DNS resolution but also the connection to the TMDB API.
|
In some places (like China), the ISP blocks not only the DNS resolution but also the connection to the TMDB API.
|
||||||
|
|
||||||
You can configure Seerr to use a proxy with the [HTTP(S) Proxy](/using-seerr/settings/general#enable-proxy-support) setting.
|
You can configure Seerr to use a proxy with the [HTTP(S) Proxy](/using-seerr/settings/general#https-proxy) setting.
|
||||||
|
|
||||||
### Option 3: Force IPV4 resolution first
|
### Option 3: Force IPV4 resolution first
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
title: Advanced Features
|
|
||||||
description: Advanced configuration and use cases.
|
|
||||||
sidebar_position: 6
|
|
||||||
---
|
|
||||||
|
|
||||||
# Advanced Features
|
|
||||||
|
|
||||||
## Advanced Configuration and Use Cases
|
|
||||||
|
|
||||||
Seerr currently offers advanced features for power users and specific use cases:
|
|
||||||
|
|
||||||
import DocCardList from '@theme/DocCardList';
|
|
||||||
|
|
||||||
<DocCardList />
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
---
|
|
||||||
id: verifying-signed-artifacts
|
|
||||||
title: Verifying Signed Artifacts
|
|
||||||
sidebar_label: Verify Signed Artifacts
|
|
||||||
description: Learn how to verify Seerr's signed artifacts and SBOM attestations.
|
|
||||||
---
|
|
||||||
|
|
||||||
import Tabs from '@theme/Tabs';
|
|
||||||
import TabItem from '@theme/TabItem';
|
|
||||||
|
|
||||||
# Verifying Signed Artifacts
|
|
||||||
|
|
||||||
These artifacts are cryptographically signed using [Sigstore Cosign](https://docs.sigstore.dev/quickstart/quickstart-cosign/):
|
|
||||||
- Container images
|
|
||||||
- Helm charts
|
|
||||||
|
|
||||||
This ensures that the images you pull are authentic, tamper-proof, and built by the official Seerr release pipeline.
|
|
||||||
|
|
||||||
Additionally each container image also includes a CycloneDX SBOM (Software Bill of Materials) attestation, generated with [Trivy](https://aquasecurity.github.io/trivy/), providing transparency about all dependencies included in the image.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
You will need the following tools installed:
|
|
||||||
|
|
||||||
- [Cosign](https://docs.sigstore.dev/cosign/system_config/installation/)
|
|
||||||
|
|
||||||
To verify images:
|
|
||||||
|
|
||||||
- [Docker](https://docs.docker.com/get-docker/) **or** [Podman](https://podman.io/getting-started/installation) (including [Skopeo](https://github.com/containers/skopeo/blob/main/install.md))
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verifying Signed Images
|
|
||||||
|
|
||||||
### Image Locations
|
|
||||||
|
|
||||||
Official Seerr images are available from:
|
|
||||||
|
|
||||||
- GitHub Container Registry (GHCR): `ghcr.io/seerr-team/seerr:<tag>`
|
|
||||||
- Docker Hub: `seerr/seerr:<tag>`
|
|
||||||
|
|
||||||
You can view all available tags on the [Seerr Releases page](https://github.com/seerr-team/seerr/releases).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Verifying a Specific Release Tag
|
|
||||||
|
|
||||||
Each tagged release (for example `v2.7.4`) is immutable and cryptographically signed.
|
|
||||||
Verification should always be performed using the image digest (SHA256).
|
|
||||||
|
|
||||||
#### Retrieve the Image Digest
|
|
||||||
|
|
||||||
<Tabs groupId="verify-methods">
|
|
||||||
<TabItem value="docker" label="Docker">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker buildx imagetools inspect ghcr.io/seerr-team/seerr:v2.7.4 --format '{{json .Manifest.Digest}}' | tr -d '"'
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="podman" label="Podman / Skopeo">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
skopeo inspect docker://ghcr.io/seerr-team/seerr:v2.7.4 --format '{{.Digest}}'
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
|
|
||||||
```
|
|
||||||
sha256:abcd1234...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Verify the Image Signature
|
|
||||||
|
|
||||||
<Tabs groupId="registry-methods">
|
|
||||||
<TabItem value="ghcr" label="GitHub Container Registry (GHCR)">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cosign verify ghcr.io/seerr-team/seerr@sha256:abcd1234... \
|
|
||||||
--certificate-identity "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v2.7.4" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="dockerhub" label="Docker Hub">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cosign verify seerr/seerr@sha256:abcd1234... \
|
|
||||||
--certificate-identity "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v2.7.4" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
:::info Successful Verification Example
|
|
||||||
Verification for `ghcr.io/seerr-team/seerr@sha256:abcd1234...`
|
|
||||||
|
|
||||||
The following checks were performed:
|
|
||||||
|
|
||||||
- Cosign claims validated
|
|
||||||
- Signatures verified against the transparency log
|
|
||||||
- Certificate issued by Fulcio to the expected workflow identity
|
|
||||||
:::
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Verifying the `latest` Tag
|
|
||||||
|
|
||||||
:::warning Latest Tag Warning
|
|
||||||
The `latest` tag is **mutable**, meaning it will change with each new release.
|
|
||||||
Always verify the digest that `latest` currently points to.
|
|
||||||
:::
|
|
||||||
|
|
||||||
#### Retrieve the Digest for `latest`
|
|
||||||
|
|
||||||
<Tabs groupId="verify-methods">
|
|
||||||
<TabItem value="docker" label="Docker">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker buildx imagetools inspect ghcr.io/seerr-team/seerr:latest --format '{{json .Manifest.Digest}}' | tr -d '"'
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="podman" label="Podman / Skopeo">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
skopeo inspect docker://ghcr.io/seerr-team/seerr:latest --format '{{.Digest}}'
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
|
|
||||||
```
|
|
||||||
sha256:abcd1234...
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Verify the Signature
|
|
||||||
|
|
||||||
<Tabs groupId="registry-methods">
|
|
||||||
<TabItem value="ghcr" label="GHCR">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cosign verify ghcr.io/seerr-team/seerr@sha256:abcd1234... \
|
|
||||||
--certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v.*" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="dockerhub" label="Docker Hub">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cosign verify seerr/seerr@sha256:abcd1234... \
|
|
||||||
--certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v.*" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
:::tip
|
|
||||||
The wildcard `v.*` ensures verification works for any versioned release that `latest` represents.
|
|
||||||
:::
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Verifying SBOM Attestations
|
|
||||||
|
|
||||||
Each image includes a CycloneDX SBOM attestation.
|
|
||||||
|
|
||||||
#### Verify the Attestation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cosign verify-attestation ghcr.io/seerr-team/seerr@sha256:abcd1234... \
|
|
||||||
--type cyclonedx \
|
|
||||||
--certificate-identity "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v2.7.4" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
:::info Successful Verification Example
|
|
||||||
Verification for `ghcr.io/seerr-team/seerr@sha256:abcd1234...`
|
|
||||||
|
|
||||||
The following checks were performed:
|
|
||||||
|
|
||||||
- Cosign claims validated
|
|
||||||
- Signatures verified against the transparency log
|
|
||||||
- Certificate issued by Fulcio to the expected workflow identity
|
|
||||||
:::
|
|
||||||
|
|
||||||
#### Extract the SBOM for Inspection
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cosign verify-attestation ghcr.io/seerr-team/seerr@sha256:abcd1234... \
|
|
||||||
--type cyclonedx \
|
|
||||||
--certificate-identity "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v2.7.4" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" | jq -r '.payload | @base64d' > sbom.json
|
|
||||||
```
|
|
||||||
|
|
||||||
You can open `sbom.json` in a CycloneDX viewer or analyse it with [Trivy](https://aquasecurity.github.io/trivy/) or [Dependency-Track](https://dependencytrack.org/).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Expected Certificate Identity
|
|
||||||
|
|
||||||
The expected certificate identity for all signed Seerr images is:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v*
|
|
||||||
```
|
|
||||||
|
|
||||||
This confirms that the image was:
|
|
||||||
|
|
||||||
- Built by the official Seerr Release workflow
|
|
||||||
- Produced from the seerr-team/seerr repository
|
|
||||||
- Signed using GitHub’s OIDC identity via Sigstore Fulcio
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Example: Full Verification Flow
|
|
||||||
|
|
||||||
<Tabs groupId="verify-examples">
|
|
||||||
<TabItem value="docker" label="Docker">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DIGEST=$(docker buildx imagetools inspect ghcr.io/seerr-team/seerr:latest --format '{{json .Manifest.Digest}}' | tr -d '"')
|
|
||||||
|
|
||||||
cosign verify ghcr.io/seerr-team/seerr@"$DIGEST" \
|
|
||||||
--certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v.*" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
|
|
||||||
cosign verify-attestation ghcr.io/seerr-team/seerr@"$DIGEST" \
|
|
||||||
--type cyclonedx \
|
|
||||||
--certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v.*" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="podman" label="Podman / Skopeo">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DIGEST=$(skopeo inspect docker://ghcr.io/seerr-team/seerr:latest --format '{{.Digest}}')
|
|
||||||
|
|
||||||
cosign verify ghcr.io/seerr-team/seerr@"$DIGEST" \
|
|
||||||
--certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/release.yml@refs/tags/v.*" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Verifying Signed Helm charts
|
|
||||||
|
|
||||||
### Helm Chart Locations
|
|
||||||
|
|
||||||
Official Seerr helm charts are available from:
|
|
||||||
|
|
||||||
- GitHub Container Registry (GHCR): `ghcr.io/seerr-team/seerr/seerr-chart/seerr-chart:<tag>`
|
|
||||||
|
|
||||||
You can view all available tags on the [Seerr Releases page](https://github.com/seerr-team/seerr/pkgs/container/seerr%2Fseerr-chart).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Verifying a Specific Release Tag
|
|
||||||
|
|
||||||
Each tagged release (for example `3.0.0`) is immutable and cryptographically signed.
|
|
||||||
Verification should always be performed using the image digest (SHA256).
|
|
||||||
|
|
||||||
#### Retrieve the Helm Chart Digest
|
|
||||||
|
|
||||||
<Tabs groupId="verify-methods">
|
|
||||||
<TabItem value="docker" label="Docker">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker buildx imagetools inspect ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{json .Manifest.Digest}}' | tr -d '"'
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="podman" label="Podman / Skopeo">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
skopeo inspect docker://ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{.Digest}}'
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
|
|
||||||
```
|
|
||||||
sha256:abcd1234...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Verify the Helm Chart Signature
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cosign verify ghcr.io/seerr-team/seerr/seerr-chart@sha256:abcd1234... \
|
|
||||||
--certificate-identity "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
:::info Successful Verification Example
|
|
||||||
Verification for `ghcr.io/seerr-team/seerr/seerr-chart@sha256:abcd1234...`
|
|
||||||
|
|
||||||
The following checks were performed:
|
|
||||||
|
|
||||||
- Cosign claims validated
|
|
||||||
- Signatures verified against the transparency log
|
|
||||||
- Certificate issued by Fulcio to the expected workflow identity
|
|
||||||
:::
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Expected Certificate Identity
|
|
||||||
|
|
||||||
The expected certificate identity for all signed Seerr images is:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main
|
|
||||||
```
|
|
||||||
|
|
||||||
This confirms that the image was:
|
|
||||||
|
|
||||||
- Built by the official Seerr Release workflow
|
|
||||||
- Produced from the seerr-team/seerr repository
|
|
||||||
- Signed using GitHub’s OIDC identity via Sigstore Fulcio
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Example: Full Verification Flow
|
|
||||||
|
|
||||||
<Tabs groupId="verify-examples">
|
|
||||||
<TabItem value="docker" label="Docker">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DIGEST=$(docker buildx imagetools inspect ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{json .Manifest.Digest}}' | tr -d '"')
|
|
||||||
|
|
||||||
cosign verify ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \
|
|
||||||
--certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
|
|
||||||
cosign verify-attestation ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \
|
|
||||||
--type cyclonedx \
|
|
||||||
--certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
<TabItem value="podman" label="Podman / Skopeo">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DIGEST=$(skopeo inspect docker://ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{.Digest}}')
|
|
||||||
|
|
||||||
cosign verify ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \
|
|
||||||
--certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \
|
|
||||||
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
|
|
||||||
```
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
| Issue | Likely Cause | Suggested Fix |
|
|
||||||
|-------|---------------|----------------|
|
|
||||||
| `no matching signatures` | Incorrect digest or tag | Retrieve the digest again using Docker or Skopeo |
|
|
||||||
| `certificate identity does not match expected` | Workflow reference changed | Ensure your `--certificate-identity` matches this documentation |
|
|
||||||
| `cosign: command not found` | Cosign not installed | Install Cosign from the official release |
|
|
||||||
| `certificate expired` | Old release | Verify a newer tag or digest |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Further Reading
|
|
||||||
|
|
||||||
- [Sigstore Documentation](https://docs.sigstore.dev)
|
|
||||||
- [Cosign Verification Guide](https://docs.sigstore.dev/cosign/verifying/verify/)
|
|
||||||
- [CycloneDX Specification](https://cyclonedx.org/specification/overview/)
|
|
||||||
- [Trivy Documentation](https://trivy.dev/latest/docs/)
|
|
||||||
- [Skopeo Documentation](https://github.com/containers/skopeo)
|
|
||||||
- [Podman Documentation](https://podman.io/get-started/)
|
|
||||||
- [Docker Documentation](https://docs.docker.com/)
|
|
||||||
- [Seerr GitHub Repository](https://github.com/seerr-team/seerr)
|
|
||||||
@@ -22,4 +22,4 @@ Users can customize their notification preferences in their own user notificatio
|
|||||||
|
|
||||||
## Requesting New Notification Agents
|
## Requesting New Notification Agents
|
||||||
|
|
||||||
If we do not currently support your preferred notification agent, feel free to [submit a feature request on GitHub](https://github.com/seerr-team/seerr/issues). However, please be sure to search first and confirm that there is not already an existing request for the agent!
|
If we do not currently support your preferred notification agent, feel free to [submit a feature request on GitHub](https://github.com/fallenbagel/jellyseerr/issues). However, please be sure to search first and confirm that there is not already an existing request for the agent!
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ User notifications are separate from system notifications, and the available not
|
|||||||
|
|
||||||
### Application/API Token
|
### Application/API Token
|
||||||
|
|
||||||
[Register an application](https://pushover.net/apps/build) and enter the API token in this field. (You can use one of the [official icons in our GitHub repository](https://github.com/seerr-team/seerr/tree/develop/public) when configuring the application.)
|
[Register an application](https://pushover.net/apps/build) and enter the API token in this field. (You can use one of the [official icons in our GitHub repository](https://github.com/fallenbagel/jellyseerr/tree/develop/public) when configuring the application.)
|
||||||
|
|
||||||
For more details on registering applications or the API token, please see the [Pushover API documentation](https://pushover.net/api#registration).
|
For more details on registering applications or the API token, please see the [Pushover API documentation](https://pushover.net/api#registration).
|
||||||
|
|
||||||
|
|||||||
24
gen-docs/blog/2025-09-29-introducing-jellyseerr-blog.md
Normal file
24
gen-docs/blog/2025-09-29-introducing-jellyseerr-blog.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: Welcome to the Jellyseerr Blog
|
||||||
|
description: The official Jellyseerr blog for release notes, technical updates, and community news.
|
||||||
|
slug: welcome
|
||||||
|
authors: [fallenbagel, gauthier-th]
|
||||||
|
tags: [announcement, jellyseerr, blog]
|
||||||
|
image: https://raw.githubusercontent.com/fallenbagel/jellyseerr/refs/heads/develop/gen-docs/static/img/logo.svg
|
||||||
|
hide_table_of_contents: false
|
||||||
|
---
|
||||||
|
|
||||||
|
We are pleased to introduce the official Jellyseerr blog.
|
||||||
|
|
||||||
|
This space will serve as the central place for:
|
||||||
|
|
||||||
|
- Release announcements
|
||||||
|
- Updates on new features and improvements
|
||||||
|
- Technical articles, such as details on our [**DNS caching package**](https://github.com/jellyseerr/dns-caching) and other enhancements
|
||||||
|
- Community-related news
|
||||||
|
|
||||||
|
<!--truncate-->
|
||||||
|
|
||||||
|
Our goal is to keep the community informed and provide deeper insights into the ongoing development of Jellyseerr.
|
||||||
|
|
||||||
|
Thank you for being part of the Jellyseerr project. More updates will follow soon.
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
title: Welcome to the Seerr Blog
|
|
||||||
description: The official Seerr blog for release notes, technical updates, and community news.
|
|
||||||
slug: welcome
|
|
||||||
authors: [fallenbagel, gauthier-th]
|
|
||||||
tags: [announcement, seerr, blog]
|
|
||||||
image: https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/gen-docs/static/img/logo.svg
|
|
||||||
hide_table_of_contents: false
|
|
||||||
---
|
|
||||||
|
|
||||||
We are pleased to introduce the official Seerr blog.
|
|
||||||
|
|
||||||
This space will serve as the central place for:
|
|
||||||
|
|
||||||
- Release announcements
|
|
||||||
- Updates on new features and improvements
|
|
||||||
- Technical articles, such as details on our [**DNS caching package**](https://github.com/seerr/dns-caching) and other enhancements
|
|
||||||
- Community-related news
|
|
||||||
|
|
||||||
<!--truncate-->
|
|
||||||
|
|
||||||
Our goal is to keep the community informed and provide deeper insights into the ongoing development of Seerr.
|
|
||||||
|
|
||||||
Thank you for being part of the Seerr project. More updates will follow soon.
|
|
||||||
@@ -7,7 +7,7 @@ export const SeerrVersion = () => {
|
|||||||
async function fetchVersion() {
|
async function fetchVersion() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://raw.githubusercontent.com/seerr-team/seerr/main/package.json'
|
'https://raw.githubusercontent.com/fallenbagel/jellyseerr/main/package.json'
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
67
package.json
67
package.json
@@ -60,6 +60,7 @@
|
|||||||
"dayjs": "1.11.7",
|
"dayjs": "1.11.7",
|
||||||
"dns-caching": "^0.2.7",
|
"dns-caching": "^0.2.7",
|
||||||
"email-templates": "12.0.1",
|
"email-templates": "12.0.1",
|
||||||
|
"email-validator": "2.0.4",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-openapi-validator": "4.13.8",
|
"express-openapi-validator": "4.13.8",
|
||||||
"express-rate-limit": "6.7.0",
|
"express-rate-limit": "6.7.0",
|
||||||
@@ -106,7 +107,6 @@
|
|||||||
"typeorm": "0.3.12",
|
"typeorm": "0.3.12",
|
||||||
"ua-parser-js": "^1.0.35",
|
"ua-parser-js": "^1.0.35",
|
||||||
"undici": "^7.3.0",
|
"undici": "^7.3.0",
|
||||||
"validator": "^13.15.15",
|
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"wink-jaro-distance": "^2.0.0",
|
"wink-jaro-distance": "^2.0.0",
|
||||||
"winston": "3.8.2",
|
"winston": "3.8.2",
|
||||||
@@ -140,7 +140,6 @@
|
|||||||
"@types/secure-random-password": "0.2.1",
|
"@types/secure-random-password": "0.2.1",
|
||||||
"@types/semver": "7.3.13",
|
"@types/semver": "7.3.13",
|
||||||
"@types/swagger-ui-express": "4.1.3",
|
"@types/swagger-ui-express": "4.1.3",
|
||||||
"@types/validator": "^13.15.3",
|
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/xml2js": "0.4.11",
|
"@types/xml2js": "0.4.11",
|
||||||
"@types/yamljs": "0.2.31",
|
"@types/yamljs": "0.2.31",
|
||||||
@@ -202,6 +201,70 @@
|
|||||||
"@commitlint/config-conventional"
|
"@commitlint/config-conventional"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"release": {
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/npm",
|
||||||
|
[
|
||||||
|
"@codedependant/semantic-release-docker",
|
||||||
|
{
|
||||||
|
"dockerArgs": {
|
||||||
|
"COMMIT_TAG": "${GITHUB_SHA}"
|
||||||
|
},
|
||||||
|
"dockerLogin": false,
|
||||||
|
"dockerProject": "fallenbagel",
|
||||||
|
"dockerImage": "jellyseerr",
|
||||||
|
"dockerTags": [
|
||||||
|
"latest",
|
||||||
|
"{{major}}",
|
||||||
|
"{{major}}.{{minor}}",
|
||||||
|
"{{major}}.{{minor}}.{{patch}}"
|
||||||
|
],
|
||||||
|
"dockerPlatform": [
|
||||||
|
"linux/amd64",
|
||||||
|
"linux/arm64"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@codedependant/semantic-release-docker",
|
||||||
|
{
|
||||||
|
"dockerArgs": {
|
||||||
|
"COMMIT_TAG": "${GITHUB_SHA}"
|
||||||
|
},
|
||||||
|
"dockerLogin": false,
|
||||||
|
"dockerRegistry": "ghcr.io",
|
||||||
|
"dockerProject": "fallenbagel",
|
||||||
|
"dockerImage": "jellyseerr",
|
||||||
|
"dockerTags": [
|
||||||
|
"latest",
|
||||||
|
"{{major}}",
|
||||||
|
"{{major}}.{{minor}}",
|
||||||
|
"{{major}}.{{minor}}.{{patch}}"
|
||||||
|
],
|
||||||
|
"dockerPlatform": [
|
||||||
|
"linux/amd64",
|
||||||
|
"linux/arm64"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"addReleases": "bottom"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"npmPublish": false,
|
||||||
|
"publish": [
|
||||||
|
"@codedependant/semantic-release-docker",
|
||||||
|
"@semantic-release/github"
|
||||||
|
]
|
||||||
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"sqlite3",
|
"sqlite3",
|
||||||
|
|||||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -89,6 +89,9 @@ importers:
|
|||||||
email-templates:
|
email-templates:
|
||||||
specifier: 12.0.1
|
specifier: 12.0.1
|
||||||
version: 12.0.1(@babel/core@7.24.7)(encoding@0.1.13)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7)
|
version: 12.0.1(@babel/core@7.24.7)(encoding@0.1.13)(handlebars@4.7.8)(mustache@4.2.0)(pug@3.0.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.7)
|
||||||
|
email-validator:
|
||||||
|
specifier: 2.0.4
|
||||||
|
version: 2.0.4
|
||||||
express:
|
express:
|
||||||
specifier: 4.21.2
|
specifier: 4.21.2
|
||||||
version: 4.21.2
|
version: 4.21.2
|
||||||
@@ -227,9 +230,6 @@ importers:
|
|||||||
undici:
|
undici:
|
||||||
specifier: ^7.3.0
|
specifier: ^7.3.0
|
||||||
version: 7.3.0
|
version: 7.3.0
|
||||||
validator:
|
|
||||||
specifier: ^13.15.15
|
|
||||||
version: 13.15.15
|
|
||||||
web-push:
|
web-push:
|
||||||
specifier: 3.5.0
|
specifier: 3.5.0
|
||||||
version: 3.5.0
|
version: 3.5.0
|
||||||
@@ -324,9 +324,6 @@ importers:
|
|||||||
'@types/swagger-ui-express':
|
'@types/swagger-ui-express':
|
||||||
specifier: 4.1.3
|
specifier: 4.1.3
|
||||||
version: 4.1.3
|
version: 4.1.3
|
||||||
'@types/validator':
|
|
||||||
specifier: ^13.15.3
|
|
||||||
version: 13.15.3
|
|
||||||
'@types/web-push':
|
'@types/web-push':
|
||||||
specifier: 3.3.2
|
specifier: 3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
@@ -3343,9 +3340,6 @@ packages:
|
|||||||
'@types/unist@2.0.10':
|
'@types/unist@2.0.10':
|
||||||
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
||||||
|
|
||||||
'@types/validator@13.15.3':
|
|
||||||
resolution: {integrity: sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==}
|
|
||||||
|
|
||||||
'@types/web-push@3.3.2':
|
'@types/web-push@3.3.2':
|
||||||
resolution: {integrity: sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==}
|
resolution: {integrity: sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==}
|
||||||
|
|
||||||
@@ -4741,6 +4735,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-849pjBFVUAWWTa3HqhDjxlXHaSWmxf4CZOlZ9iVkrSAbQ8YCYi+7KiKqt35L6F20WhSViWX7lmMjno6zBv2rNQ==}
|
resolution: {integrity: sha512-849pjBFVUAWWTa3HqhDjxlXHaSWmxf4CZOlZ9iVkrSAbQ8YCYi+7KiKqt35L6F20WhSViWX7lmMjno6zBv2rNQ==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
email-validator@2.0.4:
|
||||||
|
resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==}
|
||||||
|
engines: {node: '>4.0'}
|
||||||
|
|
||||||
emoji-regex@10.3.0:
|
emoji-regex@10.3.0:
|
||||||
resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
|
resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==}
|
||||||
|
|
||||||
@@ -9051,10 +9049,6 @@ packages:
|
|||||||
validate-npm-package-license@3.0.4:
|
validate-npm-package-license@3.0.4:
|
||||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||||
|
|
||||||
validator@13.15.15:
|
|
||||||
resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==}
|
|
||||||
engines: {node: '>= 0.10'}
|
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -13262,8 +13256,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/unist@2.0.10': {}
|
'@types/unist@2.0.10': {}
|
||||||
|
|
||||||
'@types/validator@13.15.3': {}
|
|
||||||
|
|
||||||
'@types/web-push@3.3.2':
|
'@types/web-push@3.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.10.5
|
'@types/node': 22.10.5
|
||||||
@@ -14945,6 +14937,8 @@ snapshots:
|
|||||||
- walrus
|
- walrus
|
||||||
- whiskers
|
- whiskers
|
||||||
|
|
||||||
|
email-validator@2.0.4: {}
|
||||||
|
|
||||||
emoji-regex@10.3.0: {}
|
emoji-regex@10.3.0: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
@@ -20054,8 +20048,6 @@ snapshots:
|
|||||||
spdx-correct: 3.2.0
|
spdx-correct: 3.2.0
|
||||||
spdx-expression-parse: 3.0.1
|
spdx-expression-parse: 3.0.1
|
||||||
|
|
||||||
validator@13.15.15: {}
|
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
verror@1.10.0:
|
verror@1.10.0:
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
@@ -2339,12 +2339,8 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
id:
|
userId:
|
||||||
type: string
|
type: integer
|
||||||
thumb:
|
|
||||||
type: string
|
|
||||||
email:
|
|
||||||
type: string
|
|
||||||
/settings/jellyfin/sync:
|
/settings/jellyfin/sync:
|
||||||
get:
|
get:
|
||||||
summary: Get status of full Jellyfin library sync
|
summary: Get status of full Jellyfin library sync
|
||||||
@@ -6912,10 +6908,6 @@ paths:
|
|||||||
is4k:
|
is4k:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
description: |
|
|
||||||
When true, updates the 4K status field (status4k).
|
|
||||||
When false or not provided, updates the regular status field (status).
|
|
||||||
This applies to all status values (available, partial, processing, pending, unknown).
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Returned media
|
description: Returned media
|
||||||
@@ -7755,32 +7747,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/OverrideRule'
|
$ref: '#/components/schemas/OverrideRule'
|
||||||
/overrideRule/advancedRequest:
|
|
||||||
post:
|
|
||||||
summary: Advanced override rule request
|
|
||||||
description: Processes an advanced override rule request.
|
|
||||||
tags:
|
|
||||||
- overriderule
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Advanced override rule request processed
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
rootFolder:
|
|
||||||
type: string
|
|
||||||
nullable: true
|
|
||||||
profileId:
|
|
||||||
type: number
|
|
||||||
nullable: true
|
|
||||||
tags:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: number
|
|
||||||
nullable: true
|
|
||||||
|
|
||||||
security:
|
security:
|
||||||
- cookieAuth: []
|
- cookieAuth: []
|
||||||
- apiKey: []
|
- apiKey: []
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export interface TmdbCertificationResponse {
|
|||||||
interface DiscoverMovieOptions {
|
interface DiscoverMovieOptions {
|
||||||
page?: number;
|
page?: number;
|
||||||
includeAdult?: boolean;
|
includeAdult?: boolean;
|
||||||
includeVideo?: boolean;
|
|
||||||
language?: string;
|
language?: string;
|
||||||
primaryReleaseDateGte?: string;
|
primaryReleaseDateGte?: string;
|
||||||
primaryReleaseDateLte?: string;
|
primaryReleaseDateLte?: string;
|
||||||
@@ -491,7 +490,6 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
|
|||||||
sortBy = 'popularity.desc',
|
sortBy = 'popularity.desc',
|
||||||
page = 1,
|
page = 1,
|
||||||
includeAdult = false,
|
includeAdult = false,
|
||||||
includeVideo = true,
|
|
||||||
language = this.locale,
|
language = this.locale,
|
||||||
primaryReleaseDateGte,
|
primaryReleaseDateGte,
|
||||||
primaryReleaseDateLte,
|
primaryReleaseDateLte,
|
||||||
@@ -529,7 +527,6 @@ class TheMovieDb extends ExternalAPI implements TvShowProvider {
|
|||||||
sort_by: sortBy,
|
sort_by: sortBy,
|
||||||
page,
|
page,
|
||||||
include_adult: includeAdult,
|
include_adult: includeAdult,
|
||||||
include_video: includeVideo,
|
|
||||||
language,
|
language,
|
||||||
region: this.discoverRegion || '',
|
region: this.discoverRegion || '',
|
||||||
with_original_language:
|
with_original_language:
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const postgresDevConfig: DataSourceOptions = {
|
|||||||
database: process.env.DB_NAME ?? 'seerr',
|
database: process.env.DB_NAME ?? 'seerr',
|
||||||
ssl: buildSslConfig(),
|
ssl: buildSslConfig(),
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
migrationsRun: true,
|
migrationsRun: false,
|
||||||
logging: boolFromEnv('DB_LOG_QUERIES'),
|
logging: boolFromEnv('DB_LOG_QUERIES'),
|
||||||
entities: ['server/entity/**/*.ts'],
|
entities: ['server/entity/**/*.ts'],
|
||||||
migrations: ['server/migration/postgres/**/*.ts'],
|
migrations: ['server/migration/postgres/**/*.ts'],
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||||
|
import type { TmdbKeyword } from '@server/api/themoviedb/interfaces';
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
MediaType,
|
MediaType,
|
||||||
} from '@server/constants/media';
|
} from '@server/constants/media';
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
|
import OverrideRule from '@server/entity/OverrideRule';
|
||||||
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||||
import overrideRules from '@server/lib/overrideRules';
|
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
@@ -209,20 +211,121 @@ export class MediaRequest {
|
|||||||
let tags = requestBody.tags;
|
let tags = requestBody.tags;
|
||||||
|
|
||||||
if (useOverrides) {
|
if (useOverrides) {
|
||||||
const overrideRulesResult = await overrideRules({
|
const defaultRadarrId = requestBody.is4k
|
||||||
mediaType: requestBody.mediaType,
|
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
||||||
is4k: requestBody.is4k || false,
|
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
||||||
tmdbMedia,
|
const defaultSonarrId = requestBody.is4k
|
||||||
requestUser,
|
? 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 },
|
||||||
});
|
});
|
||||||
if (overrideRulesResult.rootFolder) {
|
|
||||||
rootFolder = overrideRulesResult.rootFolder;
|
const appliedOverrideRules = overrideRules.filter((rule) => {
|
||||||
}
|
const hasAnimeKeyword =
|
||||||
if (overrideRulesResult.profileId) {
|
'results' in tmdbMedia.keywords &&
|
||||||
profileId = overrideRulesResult.profileId;
|
tmdbMedia.keywords.results.some(
|
||||||
}
|
(keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
if (overrideRulesResult.tags) {
|
);
|
||||||
tags = overrideRulesResult.tags;
|
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ class DownloadTracker {
|
|||||||
|
|
||||||
public async resetDownloadTracker() {
|
public async resetDownloadTracker() {
|
||||||
this.radarrServers = {};
|
this.radarrServers = {};
|
||||||
this.sonarrServers = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateDownloads() {
|
public updateDownloads() {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import type { NotificationAgentEmail } from '@server/lib/settings';
|
|||||||
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
import { getSettings, NotificationAgentKey } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import type { EmailOptions } from 'email-templates';
|
import type { EmailOptions } from 'email-templates';
|
||||||
|
import * as EmailValidator from 'email-validator';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import validator from 'validator';
|
|
||||||
import { Notification, shouldSendAdminNotification } from '..';
|
import { Notification, shouldSendAdminNotification } from '..';
|
||||||
import type { NotificationAgent, NotificationPayload } from './agent';
|
import type { NotificationAgent, NotificationPayload } from './agent';
|
||||||
import { BaseAgent } from './agent';
|
import { BaseAgent } from './agent';
|
||||||
@@ -221,9 +221,7 @@ class EmailAgent
|
|||||||
this.getSettings(),
|
this.getSettings(),
|
||||||
payload.notifyUser.settings?.pgpKey
|
payload.notifyUser.settings?.pgpKey
|
||||||
);
|
);
|
||||||
if (
|
if (EmailValidator.validate(payload.notifyUser.email)) {
|
||||||
validator.isEmail(payload.notifyUser.email, { require_tld: false })
|
|
||||||
) {
|
|
||||||
await email.send(
|
await email.send(
|
||||||
this.buildMessage(
|
this.buildMessage(
|
||||||
type,
|
type,
|
||||||
@@ -285,7 +283,7 @@ class EmailAgent
|
|||||||
this.getSettings(),
|
this.getSettings(),
|
||||||
user.settings?.pgpKey
|
user.settings?.pgpKey
|
||||||
);
|
);
|
||||||
if (validator.isEmail(user.email, { require_tld: false })) {
|
if (EmailValidator.validate(user.email)) {
|
||||||
await email.send(
|
await email.send(
|
||||||
this.buildMessage(type, payload, user.email, user.displayName)
|
this.buildMessage(type, payload, user.email, user.displayName)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
|
||||||
import type {
|
|
||||||
TmdbKeyword,
|
|
||||||
TmdbMovieDetails,
|
|
||||||
TmdbTvDetails,
|
|
||||||
} from '@server/api/themoviedb/interfaces';
|
|
||||||
import { MediaType } from '@server/constants/media';
|
|
||||||
import { getRepository } from '@server/datasource';
|
|
||||||
import OverrideRule from '@server/entity/OverrideRule';
|
|
||||||
import type { User } from '@server/entity/User';
|
|
||||||
import { getSettings } from '@server/lib/settings';
|
|
||||||
import logger from '@server/logger';
|
|
||||||
|
|
||||||
export type OverrideRulesResult = {
|
|
||||||
rootFolder: string | null;
|
|
||||||
profileId: number | null;
|
|
||||||
tags: number[] | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function overrideRules({
|
|
||||||
mediaType,
|
|
||||||
is4k,
|
|
||||||
tmdbMedia,
|
|
||||||
requestUser,
|
|
||||||
}: {
|
|
||||||
mediaType: MediaType;
|
|
||||||
is4k: boolean;
|
|
||||||
tmdbMedia: TmdbMovieDetails | TmdbTvDetails;
|
|
||||||
requestUser: User;
|
|
||||||
}): Promise<OverrideRulesResult> {
|
|
||||||
const settings = getSettings();
|
|
||||||
|
|
||||||
let rootFolder: string | null = null;
|
|
||||||
let profileId: number | null = null;
|
|
||||||
let tags: number[] | null = null;
|
|
||||||
|
|
||||||
const defaultRadarrId = is4k
|
|
||||||
? settings.radarr.findIndex((r) => r.is4k && r.isDefault)
|
|
||||||
: settings.radarr.findIndex((r) => !r.is4k && r.isDefault);
|
|
||||||
const defaultSonarrId = 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:
|
|
||||||
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 (
|
|
||||||
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([
|
|
||||||
...prioritizedRule.tags.split(',').map((tag) => Number(tag)),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('Override rule applied.', {
|
|
||||||
label: 'Media Request',
|
|
||||||
overrides: prioritizedRule,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { rootFolder, profileId, tags };
|
|
||||||
}
|
|
||||||
|
|
||||||
export default overrideRules;
|
|
||||||
@@ -97,13 +97,6 @@ const checkOverseerrMerge = async (): Promise<boolean> => {
|
|||||||
media.status = 7;
|
media.status = 7;
|
||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
}
|
}
|
||||||
const media4kToUpdate = await mediaRepository.find({
|
|
||||||
where: { status4k: 6 },
|
|
||||||
});
|
|
||||||
for (const media of media4kToUpdate) {
|
|
||||||
media.status4k = 7;
|
|
||||||
await mediaRepository.save(media);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to update Media status from Blacklisted to Deleted', {
|
logger.error('Failed to update Media status from Blacklisted to Deleted', {
|
||||||
label: 'Seerr Migration',
|
label: 'Seerr Migration',
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import { ApiError } from '@server/types/error';
|
|||||||
import { getAppVersion } from '@server/utils/appVersion';
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
import { getHostname } from '@server/utils/getHostname';
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import * as EmailValidator from 'email-validator';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import validator from 'validator';
|
|
||||||
|
|
||||||
const authRoutes = Router();
|
const authRoutes = Router();
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
|||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
if (
|
if (
|
||||||
settings.notifications.agents.email.options.userEmailRequired &&
|
settings.notifications.agents.email.options.userEmailRequired &&
|
||||||
!validator.isEmail(user.email, { require_tld: false })
|
!EmailValidator.validate(user.email)
|
||||||
) {
|
) {
|
||||||
user.warnings.push('userEmailRequired');
|
user.warnings.push('userEmailRequired');
|
||||||
logger.warn(`User ${user.username} has no valid email address`);
|
logger.warn(`User ${user.username} has no valid email address`);
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ mediaRoutes.post<
|
|||||||
return next({ status: 404, message: 'Media does not exist.' });
|
return next({ status: 404, message: 'Media does not exist.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const is4k = String(req.body.is4k) === 'true';
|
const is4k = Boolean(req.body.is4k);
|
||||||
|
|
||||||
switch (req.params.status) {
|
switch (req.params.status) {
|
||||||
case 'available':
|
case 'available':
|
||||||
@@ -145,16 +145,16 @@ mediaRoutes.post<
|
|||||||
message: 'Only series can be set to be partially available',
|
message: 'Only series can be set to be partially available',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
media[is4k ? 'status4k' : 'status'] = MediaStatus.PARTIALLY_AVAILABLE;
|
media.status = MediaStatus.PARTIALLY_AVAILABLE;
|
||||||
break;
|
break;
|
||||||
case 'processing':
|
case 'processing':
|
||||||
media[is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
|
media.status = MediaStatus.PROCESSING;
|
||||||
break;
|
break;
|
||||||
case 'pending':
|
case 'pending':
|
||||||
media[is4k ? 'status4k' : 'status'] = MediaStatus.PENDING;
|
media.status = MediaStatus.PENDING;
|
||||||
break;
|
break;
|
||||||
case 'unknown':
|
case 'unknown':
|
||||||
media[is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
media.status = MediaStatus.UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
await mediaRepository.save(media);
|
await mediaRepository.save(media);
|
||||||
@@ -198,7 +198,7 @@ mediaRoutes.delete(
|
|||||||
where: { id: Number(req.params.id) },
|
where: { id: Number(req.params.id) },
|
||||||
});
|
});
|
||||||
|
|
||||||
const is4k = String(req.query.is4k) === 'true';
|
const is4k = req.query.is4k === 'true';
|
||||||
const isMovie = media.mediaType === MediaType.MOVIE;
|
const isMovie = media.mediaType === MediaType.MOVIE;
|
||||||
|
|
||||||
let serviceSettings;
|
let serviceSettings;
|
||||||
@@ -212,19 +212,18 @@ mediaRoutes.delete(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const specificServiceId = is4k ? media.serviceId4k : media.serviceId;
|
|
||||||
if (
|
if (
|
||||||
specificServiceId &&
|
media.serviceId &&
|
||||||
specificServiceId >= 0 &&
|
media.serviceId >= 0 &&
|
||||||
serviceSettings?.id !== specificServiceId
|
serviceSettings?.id !== media.serviceId
|
||||||
) {
|
) {
|
||||||
if (isMovie) {
|
if (isMovie) {
|
||||||
serviceSettings = settings.radarr.find(
|
serviceSettings = settings.radarr.find(
|
||||||
(radarr) => radarr.id === specificServiceId
|
(radarr) => radarr.id === media.serviceId
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
serviceSettings = settings.sonarr.find(
|
serviceSettings = settings.sonarr.find(
|
||||||
(sonarr) => sonarr.id === specificServiceId
|
(sonarr) => sonarr.id === media.serviceId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +257,13 @@ mediaRoutes.delete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMovie) {
|
if (isMovie) {
|
||||||
await (service as RadarrAPI).removeMovie(media.tmdbId);
|
await (service as RadarrAPI).removeMovie(
|
||||||
|
parseInt(
|
||||||
|
is4k
|
||||||
|
? (media.externalServiceSlug4k as string)
|
||||||
|
: (media.externalServiceSlug as string)
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const tmdb = new TheMovieDb();
|
const tmdb = new TheMovieDb();
|
||||||
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
|
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import TheMovieDb from '@server/api/themoviedb';
|
|
||||||
import { MediaType } from '@server/constants/media';
|
|
||||||
import { getRepository } from '@server/datasource';
|
import { getRepository } from '@server/datasource';
|
||||||
import OverrideRule from '@server/entity/OverrideRule';
|
import OverrideRule from '@server/entity/OverrideRule';
|
||||||
import { User } from '@server/entity/User';
|
|
||||||
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
|
import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces';
|
||||||
import overrideRules, {
|
|
||||||
type OverrideRulesResult,
|
|
||||||
} from '@server/lib/overrideRules';
|
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { isAuthenticated } from '@server/middleware/auth';
|
import { isAuthenticated } from '@server/middleware/auth';
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
@@ -67,40 +61,6 @@ overrideRuleRoutes.post<
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
overrideRuleRoutes.post(
|
|
||||||
'/advancedRequest',
|
|
||||||
isAuthenticated(Permission.REQUEST_ADVANCED),
|
|
||||||
async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const tmdb = new TheMovieDb();
|
|
||||||
const tmdbMedia =
|
|
||||||
req.body.mediaType === MediaType.MOVIE
|
|
||||||
? await tmdb.getMovie({ movieId: req.body.tmdbId })
|
|
||||||
: await tmdb.getTvShow({ tvId: req.body.tmdbId });
|
|
||||||
|
|
||||||
const userRepository = getRepository(User);
|
|
||||||
const user = await userRepository.findOne({
|
|
||||||
where: { id: req.body.requestUser },
|
|
||||||
relations: { requests: true },
|
|
||||||
});
|
|
||||||
if (!user) {
|
|
||||||
return next({ status: 404, message: 'User not found.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const overrideRulesResult: OverrideRulesResult = await overrideRules({
|
|
||||||
mediaType: req.body.mediaType,
|
|
||||||
is4k: req.body.is4k,
|
|
||||||
tmdbMedia,
|
|
||||||
requestUser: user,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(overrideRulesResult);
|
|
||||||
} catch {
|
|
||||||
next({ status: 404, message: 'Media not found' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
overrideRuleRoutes.put<
|
overrideRuleRoutes.put<
|
||||||
{ ruleId: string },
|
{ ruleId: string },
|
||||||
OverrideRule,
|
OverrideRule,
|
||||||
|
|||||||
@@ -269,20 +269,16 @@ router.delete<{ userId: number; endpoint: string }>(
|
|||||||
try {
|
try {
|
||||||
const userPushSubRepository = getRepository(UserPushSubscription);
|
const userPushSubRepository = getRepository(UserPushSubscription);
|
||||||
|
|
||||||
const userPushSub = await userPushSubRepository.findOne({
|
const userPushSub = await userPushSubRepository.findOneOrFail({
|
||||||
relations: { user: true },
|
relations: {
|
||||||
|
user: true,
|
||||||
|
},
|
||||||
where: {
|
where: {
|
||||||
user: { id: req.params.userId },
|
user: { id: req.params.userId },
|
||||||
endpoint: req.params.endpoint,
|
endpoint: req.params.endpoint,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// If not found, just return 204 to prevent push disable failure
|
|
||||||
// (rare scenario where user push sub does not exist)
|
|
||||||
if (!userPushSub) {
|
|
||||||
return res.status(204).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
await userPushSubRepository.remove(userPushSub);
|
await userPushSubRepository.remove(userPushSub);
|
||||||
return res.status(204).send();
|
return res.status(204).send();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
|
|||||||
src={
|
src={
|
||||||
title?.posterPath
|
title?.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ const CollectionDetails = ({ collection }: CollectionDetailsProps) => {
|
|||||||
src={
|
src={
|
||||||
data.posterPath
|
data.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ const IssueDetails = () => {
|
|||||||
src={
|
src={
|
||||||
data.posterPath
|
data.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ const IssueItem = ({ issue }: IssueItemProps) => {
|
|||||||
src={
|
src={
|
||||||
title.posterPath
|
title.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Transition } from '@headlessui/react';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Field, Formik } from 'formik';
|
import { Field, Formik } from 'formik';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import validator from 'validator';
|
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
const messages = defineMessages('components.Login', {
|
const messages = defineMessages('components.Login', {
|
||||||
@@ -37,11 +36,7 @@ const AddEmailModal: React.FC<AddEmailModalProps> = ({
|
|||||||
|
|
||||||
const EmailSettingsSchema = Yup.object().shape({
|
const EmailSettingsSchema = Yup.object().shape({
|
||||||
email: Yup.string()
|
email: Yup.string()
|
||||||
.test(
|
.email(intl.formatMessage(messages.validationEmailFormat))
|
||||||
'email',
|
|
||||||
intl.formatMessage(messages.validationEmailFormat),
|
|
||||||
(value) => !value || validator.isEmail(value, { require_tld: false })
|
|
||||||
)
|
|
||||||
.required(intl.formatMessage(messages.validationEmailRequired)),
|
.required(intl.formatMessage(messages.validationEmailRequired)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -150,31 +150,6 @@ const ManageSlideOver = ({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDefault4kService = () => {
|
|
||||||
if (data.mediaInfo) {
|
|
||||||
if (data.mediaInfo.mediaType === MediaType.MOVIE) {
|
|
||||||
return (
|
|
||||||
radarrData?.find(
|
|
||||||
(radarr) =>
|
|
||||||
radarr.isDefault &&
|
|
||||||
radarr.is4k &&
|
|
||||||
radarr.id === data.mediaInfo?.serviceId4k
|
|
||||||
) !== undefined
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
sonarrData?.find(
|
|
||||||
(sonarr) =>
|
|
||||||
sonarr.isDefault &&
|
|
||||||
sonarr.is4k &&
|
|
||||||
sonarr.id === data.mediaInfo?.serviceId4k
|
|
||||||
) !== undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const markAvailable = async (is4k = false) => {
|
const markAvailable = async (is4k = false) => {
|
||||||
if (data.mediaInfo) {
|
if (data.mediaInfo) {
|
||||||
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
|
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
|
||||||
@@ -597,7 +572,7 @@ const ManageSlideOver = ({
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
{isDefault4kService() && (
|
{isDefaultService() && (
|
||||||
<div>
|
<div>
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
onClick={() => deleteMediaFile(true)}
|
onClick={() => deleteMediaFile(true)}
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
src={
|
src={
|
||||||
data.posterPath
|
data.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -292,7 +292,6 @@ const PersonDetails = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:hidden">{mediaTypePicker}</div>
|
|
||||||
{data.biography && (
|
{data.biography && (
|
||||||
<div className="relative text-left">
|
<div className="relative text-left">
|
||||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||||
@@ -315,6 +314,7 @@ const PersonDetails = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="lg:hidden">{mediaTypePicker}</div>
|
||||||
{data.knownForDepartment === 'Acting' ? [cast, crew] : [crew, cast]}
|
{data.knownForDepartment === 'Acting' ? [cast, crew] : [crew, cast]}
|
||||||
{isLoading && <LoadingSpinner />}
|
{isLoading && <LoadingSpinner />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -617,7 +617,7 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
src={
|
src={
|
||||||
title.posterPath
|
title.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
src={
|
src={
|
||||||
title.posterPath
|
title.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${title.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import type {
|
|||||||
ServiceCommonServerWithDetails,
|
ServiceCommonServerWithDetails,
|
||||||
} from '@server/interfaces/api/serviceInterfaces';
|
} from '@server/interfaces/api/serviceInterfaces';
|
||||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||||
import type { OverrideRulesResult } from '@server/lib/overrideRules';
|
|
||||||
import { hasPermission } from '@server/lib/permissions';
|
import { hasPermission } from '@server/lib/permissions';
|
||||||
import axios from 'axios';
|
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -53,7 +51,6 @@ export type RequestOverrides = {
|
|||||||
|
|
||||||
interface AdvancedRequesterProps {
|
interface AdvancedRequesterProps {
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv';
|
||||||
tmdbId?: number;
|
|
||||||
is4k: boolean;
|
is4k: boolean;
|
||||||
isAnime?: boolean;
|
isAnime?: boolean;
|
||||||
defaultOverrides?: RequestOverrides;
|
defaultOverrides?: RequestOverrides;
|
||||||
@@ -63,7 +60,6 @@ interface AdvancedRequesterProps {
|
|||||||
|
|
||||||
const AdvancedRequester = ({
|
const AdvancedRequester = ({
|
||||||
type,
|
type,
|
||||||
tmdbId,
|
|
||||||
is4k = false,
|
is4k = false,
|
||||||
isAnime = false,
|
isAnime = false,
|
||||||
defaultOverrides,
|
defaultOverrides,
|
||||||
@@ -288,35 +284,6 @@ const AdvancedRequester = ({
|
|||||||
selectedTags,
|
selectedTags,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (tmdbId) {
|
|
||||||
try {
|
|
||||||
const { data: override } = await axios.post<OverrideRulesResult>(
|
|
||||||
'/api/v1/overrideRule/advancedRequest',
|
|
||||||
{
|
|
||||||
mediaType: type,
|
|
||||||
is4k,
|
|
||||||
requestUser: requestUser?.id ?? currentUser?.id,
|
|
||||||
tmdbId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (override.rootFolder) {
|
|
||||||
setSelectedFolder(override.rootFolder);
|
|
||||||
}
|
|
||||||
if (override.profileId) {
|
|
||||||
setSelectedProfile(override.profileId);
|
|
||||||
}
|
|
||||||
if (override.tags) {
|
|
||||||
setSelectedTags(override.tags);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [serverData, is4k, requestUser]);
|
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 w-full">
|
<div className="mb-2 w-full">
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ const CollectionRequestModal = ({
|
|||||||
src={
|
src={
|
||||||
part.posterPath
|
part.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${part.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -288,7 +288,6 @@ const MovieRequestModal = ({
|
|||||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||||
<AdvancedRequester
|
<AdvancedRequester
|
||||||
type="movie"
|
type="movie"
|
||||||
tmdbId={tmdbId}
|
|
||||||
is4k={is4k}
|
is4k={is4k}
|
||||||
requestUser={editRequest.requestedBy}
|
requestUser={editRequest.requestedBy}
|
||||||
defaultOverrides={{
|
defaultOverrides={{
|
||||||
@@ -358,7 +357,6 @@ const MovieRequestModal = ({
|
|||||||
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
{(hasPermission(Permission.REQUEST_ADVANCED) ||
|
||||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||||
<AdvancedRequester
|
<AdvancedRequester
|
||||||
tmdbId={tmdbId}
|
|
||||||
type="movie"
|
type="movie"
|
||||||
is4k={is4k}
|
is4k={is4k}
|
||||||
onChange={(overrides) => {
|
onChange={(overrides) => {
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ const SearchByNameModal = ({
|
|||||||
<CachedImage
|
<CachedImage
|
||||||
type="tvdb"
|
type="tvdb"
|
||||||
src={
|
src={
|
||||||
item.remotePoster ?? '/images/seerr_poster_not_found.png'
|
item.remotePoster ??
|
||||||
|
'/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt={item.title}
|
alt={item.title}
|
||||||
className="w-100 h-auto rounded-md"
|
className="w-100 h-auto rounded-md"
|
||||||
|
|||||||
@@ -722,7 +722,6 @@ const TvRequestModal = ({
|
|||||||
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
hasPermission(Permission.MANAGE_REQUESTS)) && (
|
||||||
<AdvancedRequester
|
<AdvancedRequester
|
||||||
type="tv"
|
type="tv"
|
||||||
tmdbId={tmdbId}
|
|
||||||
is4k={is4k}
|
is4k={is4k}
|
||||||
isAnime={data?.keywords.some(
|
isAnime={data?.keywords.some(
|
||||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import validator from 'validator';
|
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
const messages = defineMessages('components.ResetPassword', {
|
const messages = defineMessages('components.ResetPassword', {
|
||||||
@@ -30,11 +29,7 @@ const ResetPassword = () => {
|
|||||||
|
|
||||||
const ResetSchema = Yup.object().shape({
|
const ResetSchema = Yup.object().shape({
|
||||||
email: Yup.string()
|
email: Yup.string()
|
||||||
.test(
|
.email(intl.formatMessage(messages.validationemailrequired))
|
||||||
'email',
|
|
||||||
intl.formatMessage(messages.validationemailrequired),
|
|
||||||
(value) => !value || validator.isEmail(value, { require_tld: false })
|
|
||||||
)
|
|
||||||
.required(intl.formatMessage(messages.validationemailrequired)),
|
.required(intl.formatMessage(messages.validationemailrequired)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useState } from 'react';
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
import validator from 'validator';
|
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
const messages = defineMessages('components.Settings.Notifications', {
|
const messages = defineMessages('components.Settings.Notifications', {
|
||||||
@@ -78,11 +77,7 @@ const NotificationsEmail = () => {
|
|||||||
.required(intl.formatMessage(messages.validationEmail)),
|
.required(intl.formatMessage(messages.validationEmail)),
|
||||||
otherwise: Yup.string().nullable(),
|
otherwise: Yup.string().nullable(),
|
||||||
})
|
})
|
||||||
.test(
|
.email(intl.formatMessage(messages.validationEmail)),
|
||||||
'email',
|
|
||||||
intl.formatMessage(messages.validationEmail),
|
|
||||||
(value) => !value || validator.isEmail(value, { require_tld: false })
|
|
||||||
),
|
|
||||||
smtpHost: Yup.string().when('enabled', {
|
smtpHost: Yup.string().when('enabled', {
|
||||||
is: true,
|
is: true,
|
||||||
then: Yup.string()
|
then: Yup.string()
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ const NotificationsWebhook = () => {
|
|||||||
{values.supportVariables && (
|
{values.supportVariables && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.seerr.dev/using-seerr/notifications/webhook#template-variables"
|
href="https://docs.seerr.dev/using-jellyseerr/notifications/webhook#template-variables"
|
||||||
passHref
|
passHref
|
||||||
legacyBehavior
|
legacyBehavior
|
||||||
>
|
>
|
||||||
@@ -376,7 +376,7 @@ const NotificationsWebhook = () => {
|
|||||||
<span>{intl.formatMessage(messages.resetPayload)}</span>
|
<span>{intl.formatMessage(messages.resetPayload)}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.seerr.dev/using-seerr/notifications/webhook#template-variables"
|
href="https://docs.seerr.dev/using-jellyseerr/notifications/webhook#template-variables"
|
||||||
passHref
|
passHref
|
||||||
legacyBehavior
|
legacyBehavior
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import axios from 'axios';
|
|||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import validator from 'validator';
|
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
const messages = defineMessages('components.Login', {
|
const messages = defineMessages('components.Login', {
|
||||||
@@ -91,11 +90,7 @@ function JellyfinSetup({
|
|||||||
(value) => !value || !value.endsWith('/')
|
(value) => !value || !value.endsWith('/')
|
||||||
),
|
),
|
||||||
email: Yup.string()
|
email: Yup.string()
|
||||||
.test(
|
.email(intl.formatMessage(messages.validationemailformat))
|
||||||
'email',
|
|
||||||
intl.formatMessage(messages.validationemailformat),
|
|
||||||
(value) => !value || validator.isEmail(value, { require_tld: false })
|
|
||||||
)
|
|
||||||
.required(intl.formatMessage(messages.validationemailrequired)),
|
.required(intl.formatMessage(messages.validationemailrequired)),
|
||||||
username: Yup.string().required(
|
username: Yup.string().required(
|
||||||
intl.formatMessage(messages.validationusernamerequired)
|
intl.formatMessage(messages.validationusernamerequired)
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ const TitleCard = ({
|
|||||||
src={
|
src={
|
||||||
image
|
image
|
||||||
? `https://image.tmdb.org/t/p/w300_and_h450_face${image}`
|
? `https://image.tmdb.org/t/p/w300_and_h450_face${image}`
|
||||||
: `/images/seerr_poster_not_found_logo_top.png`
|
: `/images/jellyseerr_poster_not_found_logo_top.png`
|
||||||
}
|
}
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
fill
|
fill
|
||||||
|
|||||||
@@ -532,7 +532,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
src={
|
src={
|
||||||
data.posterPath
|
data.posterPath
|
||||||
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
? `https://image.tmdb.org/t/p/w600_and_h900_bestv2${data.posterPath}`
|
||||||
: '/images/seerr_poster_not_found.png'
|
: '/images/jellyseerr_poster_not_found.png'
|
||||||
}
|
}
|
||||||
alt=""
|
alt=""
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import validator from 'validator';
|
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import JellyfinImportModal from './JellyfinImportModal';
|
import JellyfinImportModal from './JellyfinImportModal';
|
||||||
|
|
||||||
@@ -211,11 +210,7 @@ const UserList = () => {
|
|||||||
),
|
),
|
||||||
email: Yup.string()
|
email: Yup.string()
|
||||||
.required()
|
.required()
|
||||||
.test(
|
.email(intl.formatMessage(messages.validationEmail)),
|
||||||
'email',
|
|
||||||
intl.formatMessage(messages.validationEmail),
|
|
||||||
(value) => !value || validator.isEmail(value, { require_tld: false })
|
|
||||||
),
|
|
||||||
password: Yup.lazy((value) =>
|
password: Yup.lazy((value) =>
|
||||||
!value
|
!value
|
||||||
? Yup.string()
|
? Yup.string()
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import validator from 'validator';
|
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
const messages = defineMessages(
|
const messages = defineMessages(
|
||||||
@@ -106,18 +105,10 @@ const UserGeneralSettings = () => {
|
|||||||
user?.id === 1 ||
|
user?.id === 1 ||
|
||||||
(user?.userType !== UserType.JELLYFIN && user?.userType !== UserType.EMBY)
|
(user?.userType !== UserType.JELLYFIN && user?.userType !== UserType.EMBY)
|
||||||
? Yup.string()
|
? Yup.string()
|
||||||
.test(
|
.email(intl.formatMessage(messages.validationemailformat))
|
||||||
'email',
|
|
||||||
intl.formatMessage(messages.validationemailformat),
|
|
||||||
(value) =>
|
|
||||||
!value || validator.isEmail(value, { require_tld: false })
|
|
||||||
)
|
|
||||||
.required(intl.formatMessage(messages.validationemailrequired))
|
.required(intl.formatMessage(messages.validationemailrequired))
|
||||||
: Yup.string().test(
|
: Yup.string().email(
|
||||||
'email',
|
intl.formatMessage(messages.validationemailformat)
|
||||||
intl.formatMessage(messages.validationemailformat),
|
|
||||||
(value) =>
|
|
||||||
!value || validator.isEmail(value, { require_tld: false })
|
|
||||||
),
|
),
|
||||||
discordId: Yup.string()
|
discordId: Yup.string()
|
||||||
.nullable()
|
.nullable()
|
||||||
|
|||||||
@@ -111,11 +111,6 @@ const UserWebPushSettings = () => {
|
|||||||
try {
|
try {
|
||||||
await unsubscribeToPushNotifications(user?.id, endpoint);
|
await unsubscribeToPushNotifications(user?.id, endpoint);
|
||||||
|
|
||||||
// Delete from backend if endpoint is available
|
|
||||||
if (subEndpoint) {
|
|
||||||
await deletePushSubscriptionFromBackend(subEndpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('pushNotificationsEnabled', 'false');
|
localStorage.setItem('pushNotificationsEnabled', 'false');
|
||||||
setWebPushEnabled(false);
|
setWebPushEnabled(false);
|
||||||
addToast(intl.formatMessage(messages.webpushhasbeendisabled), {
|
addToast(intl.formatMessage(messages.webpushhasbeendisabled), {
|
||||||
|
|||||||
Reference in New Issue
Block a user