Compare commits
1 Commits
develop
...
fix-retry-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de1bf4caff |
3
.github/ISSUE_TEMPLATE/bug.yml
vendored
3
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
name: 🐛 Bug Report
|
name: 🐛 Bug Report
|
||||||
description: Report a problem
|
description: Report a problem
|
||||||
labels: ['awaiting triage']
|
labels: ['bug', 'awaiting triage']
|
||||||
type: bug
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
63
.github/ISSUE_TEMPLATE/documentation.yml
vendored
63
.github/ISSUE_TEMPLATE/documentation.yml
vendored
@@ -1,63 +0,0 @@
|
|||||||
name: 📚 Documentation
|
|
||||||
description: Report a docs problem or suggest a docs improvement
|
|
||||||
title: "[Docs]: "
|
|
||||||
labels: ["documentation", "awaiting triage"]
|
|
||||||
type: task
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for helping improve the docs!
|
|
||||||
|
|
||||||
Use this template for documentation issues (typos, unclear steps, missing info, outdated screenshots).
|
|
||||||
For app bugs or feature ideas, please use the other templates.
|
|
||||||
- type: input
|
|
||||||
id: doc-location
|
|
||||||
attributes:
|
|
||||||
label: Page / Location
|
|
||||||
description: Link to the docs page or the file/path (e.g. https://docs.seerr.dev/... or README.md)
|
|
||||||
placeholder: "https://docs.seerr.dev/..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: doc-area
|
|
||||||
attributes:
|
|
||||||
label: Docs Area
|
|
||||||
options:
|
|
||||||
- docs site
|
|
||||||
- migration guide
|
|
||||||
- README / repo docs
|
|
||||||
- API / integrations
|
|
||||||
- other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: problem
|
|
||||||
attributes:
|
|
||||||
label: What’s wrong / missing?
|
|
||||||
description: Describe the issue in the docs.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: suggested-fix
|
|
||||||
attributes:
|
|
||||||
label: Suggested change
|
|
||||||
description: If you know what should be changed, describe it (or paste proposed wording).
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
|
||||||
id: search-existing
|
|
||||||
attributes:
|
|
||||||
label: Search Existing Issues
|
|
||||||
description: Have you searched existing issues to see if this has already been reported?
|
|
||||||
options:
|
|
||||||
- label: Yes, I have searched existing issues.
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
id: terms
|
|
||||||
attributes:
|
|
||||||
label: Code of Conduct
|
|
||||||
description: By submitting this issue, you agree to follow our Code of Conduct.
|
|
||||||
options:
|
|
||||||
- label: I agree to follow Seerr's [Code of Conduct](https://github.com/seerr-team/seerr/blob/develop/CODE_OF_CONDUCT.md).
|
|
||||||
required: true
|
|
||||||
3
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
3
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
name: ✨ Feature Request
|
name: ✨ Feature Request
|
||||||
description: Suggest an idea
|
description: Suggest an idea
|
||||||
labels: ['awaiting triage']
|
labels: ['enhancement', 'awaiting triage']
|
||||||
type: feature
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
65
.github/ISSUE_TEMPLATE/maintenance.yml
vendored
65
.github/ISSUE_TEMPLATE/maintenance.yml
vendored
@@ -1,65 +0,0 @@
|
|||||||
name: 🧰 Maintenance / Chore
|
|
||||||
description: CI, GitHub Actions, build, dependencies, refactors (non-feature work)
|
|
||||||
title: "[Chore]: "
|
|
||||||
labels: ["maintenance", "awaiting triage"]
|
|
||||||
type: task
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Maintainers / contributors: use this for internal tasks (CI, workflows, tooling, refactors).
|
|
||||||
If you're reporting a user-facing bug or requesting a feature, use the other templates.
|
|
||||||
- type: dropdown
|
|
||||||
id: area
|
|
||||||
attributes:
|
|
||||||
label: Area
|
|
||||||
options:
|
|
||||||
- CI / GitHub Actions
|
|
||||||
- build / packaging
|
|
||||||
- dependencies
|
|
||||||
- release process
|
|
||||||
- refactor / tech debt
|
|
||||||
- tooling / scripts
|
|
||||||
- other
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: summary
|
|
||||||
attributes:
|
|
||||||
label: Summary
|
|
||||||
description: What needs doing and why?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: acceptance
|
|
||||||
attributes:
|
|
||||||
label: Acceptance criteria
|
|
||||||
description: What does "done" look like?
|
|
||||||
placeholder: |
|
|
||||||
- [ ] ...
|
|
||||||
- [ ] ...
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: related
|
|
||||||
attributes:
|
|
||||||
label: Related links
|
|
||||||
description: PRs, failing workflow runs, logs, or relevant issues.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: checkboxes
|
|
||||||
id: search-existing
|
|
||||||
attributes:
|
|
||||||
label: Search Existing Issues
|
|
||||||
description: Have you searched existing issues to see if this has already been reported?
|
|
||||||
options:
|
|
||||||
- label: Yes, I have searched existing issues.
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
id: terms
|
|
||||||
attributes:
|
|
||||||
label: Code of Conduct
|
|
||||||
description: By submitting this issue, you agree to follow our Code of Conduct.
|
|
||||||
options:
|
|
||||||
- label: I agree to follow Seerr's [Code of Conduct](https://github.com/seerr-team/seerr/blob/develop/CODE_OF_CONDUCT.md).
|
|
||||||
required: true
|
|
||||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -18,7 +18,7 @@ env:
|
|||||||
DOCKER_HUB: seerr/seerr
|
DOCKER_HUB: seerr/seerr
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ci-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build (per-arch, native runners)
|
name: Build (per-arch, native runners)
|
||||||
if: github.ref == 'refs/heads/develop'
|
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
@@ -237,7 +237,7 @@ jobs:
|
|||||||
discord:
|
discord:
|
||||||
name: Send Discord Notification
|
name: Send Discord Notification
|
||||||
needs: publish
|
needs: publish
|
||||||
if: always() && github.event_name != 'pull_request'
|
if: always() && github.event_name != 'pull_request' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Determine Workflow Status
|
- name: Determine Workflow Status
|
||||||
|
|||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -20,7 +20,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: codeql-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
2
.github/workflows/conflict_labeler.yml
vendored
2
.github/workflows/conflict_labeler.yml
vendored
@@ -14,7 +14,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: merge-conflict-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
87
.github/workflows/create-tag.yml
vendored
87
.github/workflows/create-tag.yml
vendored
@@ -1,87 +0,0 @@
|
|||||||
---
|
|
||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
|
||||||
name: Create tag
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
determine-tag-version:
|
|
||||||
name: Determine tag version
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
outputs:
|
|
||||||
tag_version: ${{ steps.git-cliff.outputs.tag_version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install git-cliff
|
|
||||||
uses: taiki-e/install-action@cede0bb282aae847dfa8aacca3a41c86d973d4d7 # v2.68.1
|
|
||||||
with:
|
|
||||||
tool: git-cliff
|
|
||||||
|
|
||||||
- name: Get tag version
|
|
||||||
id: git-cliff
|
|
||||||
run: |
|
|
||||||
tag_version=$(git-cliff -c .github/cliff.toml --bumped-version --unreleased)
|
|
||||||
echo "Next tag version is ${tag_version}"
|
|
||||||
echo "tag_version=${tag_version}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
create-tag:
|
|
||||||
name: Create tag
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
needs: determine-tag-version
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAG_VERSION: ${{ needs.determine-tag-version.outputs.tag_version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
|
||||||
with:
|
|
||||||
ssh-key: '${{ secrets.COMMIT_KEY }}'
|
|
||||||
|
|
||||||
- name: Pnpm Setup
|
|
||||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
|
||||||
with:
|
|
||||||
node-version-file: 'package.json'
|
|
||||||
# For workflows with elevated privileges we recommend disabling automatic caching.
|
|
||||||
# https://github.com/actions/setup-node
|
|
||||||
package-manager-cache: false
|
|
||||||
|
|
||||||
- name: Configure git
|
|
||||||
run: |
|
|
||||||
git config --global user.name "${{ github.actor }}"
|
|
||||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Bump package.json
|
|
||||||
run: npm version ${TAG_VERSION} --no-commit-hooks --no-git-tag-version
|
|
||||||
|
|
||||||
- name: Commit updated files
|
|
||||||
run: |
|
|
||||||
git add package.json
|
|
||||||
git commit -m 'chore(release): prepare ${TAG_VERSION}'
|
|
||||||
git push
|
|
||||||
|
|
||||||
- name: Create git tag
|
|
||||||
run: |
|
|
||||||
git tag ${TAG_VERSION}
|
|
||||||
git push origin ${TAG_VERSION}
|
|
||||||
2
.github/workflows/cypress.yml
vendored
2
.github/workflows/cypress.yml
vendored
@@ -28,7 +28,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: cypress-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
2
.github/workflows/docs-deploy.yml
vendored
2
.github/workflows/docs-deploy.yml
vendored
@@ -15,7 +15,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: pages
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
2
.github/workflows/docs-link-check.yml
vendored
2
.github/workflows/docs-link-check.yml
vendored
@@ -25,7 +25,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: docs-link-check-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
2
.github/workflows/helm.yml
vendored
2
.github/workflows/helm.yml
vendored
@@ -14,7 +14,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: helm-charts
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
2
.github/workflows/lint-helm-charts.yml
vendored
2
.github/workflows/lint-helm-charts.yml
vendored
@@ -18,7 +18,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: charts-lint-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
2
.github/workflows/preview.yml
vendored
2
.github/workflows/preview.yml
vendored
@@ -15,7 +15,7 @@ env:
|
|||||||
DOCKER_HUB: seerr/seerr
|
DOCKER_HUB: seerr/seerr
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: preview-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
@@ -11,7 +11,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -304,3 +304,42 @@ jobs:
|
|||||||
run: gh release edit "${{ env.VERSION }}" --draft=false --repo "${{ github.repository }}"
|
run: gh release edit "${{ env.VERSION }}" --draft=false --repo "${{ github.repository }}"
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
discord:
|
||||||
|
name: Send Discord Notification
|
||||||
|
needs: publish-release
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Determine status
|
||||||
|
id: status
|
||||||
|
run: |
|
||||||
|
case "${{ needs.publish-release.result }}" in
|
||||||
|
success) echo "status=Success" >> $GITHUB_OUTPUT; echo "colour=3066993" >> $GITHUB_OUTPUT ;;
|
||||||
|
failure) echo "status=Failure" >> $GITHUB_OUTPUT; echo "colour=15158332" >> $GITHUB_OUTPUT ;;
|
||||||
|
cancelled) echo "status=Cancelled" >> $GITHUB_OUTPUT; echo "colour=10181046" >> $GITHUB_OUTPUT ;;
|
||||||
|
*) echo "status=Skipped" >> $GITHUB_OUTPUT; echo "colour=9807270" >> $GITHUB_OUTPUT ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Send notification
|
||||||
|
run: |
|
||||||
|
WEBHOOK="${{ secrets.DISCORD_WEBHOOK }}"
|
||||||
|
|
||||||
|
PAYLOAD=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"embeds": [{
|
||||||
|
"title": "${{ steps.status.outputs.status }}: ${{ github.workflow }}",
|
||||||
|
"color": ${{ steps.status.outputs.colour }},
|
||||||
|
"fields": [
|
||||||
|
{ "name": "Repository", "value": "[${{ github.repository }}](${{ github.server_url }}/${{ github.repository }})", "inline": true },
|
||||||
|
{ "name": "Ref", "value": "${{ github.ref }}", "inline": true },
|
||||||
|
{ "name": "Event", "value": "${{ github.event_name }}", "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 }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
curl -sS -H "Content-Type: application/json" -X POST -d "$PAYLOAD" "$WEBHOOK" || true
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ on:
|
|||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: renovate-helm-hooks-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
28
.github/workflows/semantic-pr.yml
vendored
28
.github/workflows/semantic-pr.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: "Semantic PR"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- reopened
|
|
||||||
- edited
|
|
||||||
- synchronize
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
main:
|
|
||||||
name: Validate PR Title
|
|
||||||
runs-on: ubuntu-slim
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
checks: write
|
|
||||||
steps:
|
|
||||||
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: close-stale-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
2
.github/workflows/test-docs-deploy.yml
vendored
2
.github/workflows/test-docs-deploy.yml
vendored
@@ -14,7 +14,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: docs-pr-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
2
.github/workflows/trivy-scan.yml
vendored
2
.github/workflows/trivy-scan.yml
vendored
@@ -16,7 +16,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: trivy-scan-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
# Channels DVR Integration for Seerr
|
|
||||||
|
|
||||||
**Status:** Phase 1 Complete (Core Integration)
|
|
||||||
**Date:** 2026-02-20
|
|
||||||
**Implemented by:** Synapse (Opus → Sonnet)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Added Channels DVR as a 4th media server backend to Seerr (alongside Jellyfin, Plex, Emby).
|
|
||||||
|
|
||||||
## What Was Implemented
|
|
||||||
|
|
||||||
### 1. Media Server Type Enum (`server/constants/server.ts`)
|
|
||||||
- Added `CHANNELS_DVR = 4` to `MediaServerType` enum
|
|
||||||
|
|
||||||
### 2. API Client (`server/api/channelsdvr.ts`)
|
|
||||||
- Full REST API client for Channels DVR
|
|
||||||
- Methods:
|
|
||||||
- `getShows()` - List all TV shows
|
|
||||||
- `getShow(id)` - Get specific show
|
|
||||||
- `getShowEpisodes(id)` - Get episodes for a show
|
|
||||||
- `getMovies()` - List all movies
|
|
||||||
- `getMovie(id)` - Get specific movie
|
|
||||||
- `testConnection()` - Connectivity test
|
|
||||||
- TypeScript interfaces for all API responses
|
|
||||||
|
|
||||||
### 3. Library Scanner (`server/lib/scanners/channelsdvr/index.ts`)
|
|
||||||
- Scans Channels DVR library and maps to Seerr
|
|
||||||
- **Key feature:** TMDb ID lookup by title/year search
|
|
||||||
- Processes movies and TV shows
|
|
||||||
- Handles episode/season grouping
|
|
||||||
- Tracks processing status
|
|
||||||
|
|
||||||
### 4. Settings Integration (`server/lib/settings/index.ts`)
|
|
||||||
- Added `ChannelsDVRSettings` interface
|
|
||||||
- Added to `AllSettings` with default initialization
|
|
||||||
- Configuration fields:
|
|
||||||
- `name`: Display name
|
|
||||||
- `url`: Channels DVR server URL (e.g., http://192.168.0.15:8089)
|
|
||||||
- `libraries`: Library configuration array
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. **User configures Channels DVR URL** in Seerr settings
|
|
||||||
2. **Scanner connects** via REST API (no auth needed!)
|
|
||||||
3. **Fetches all content** (movies + TV shows)
|
|
||||||
4. **Maps to TMDb** by searching title + year
|
|
||||||
5. **Processes into Seerr database** for request management
|
|
||||||
|
|
||||||
## Key Design Decisions
|
|
||||||
|
|
||||||
### Why TMDb Search Instead of Direct IDs?
|
|
||||||
- Channels DVR doesn't provide TMDb/IMDb IDs in API
|
|
||||||
- Uses program_id (Gracenote/TMS identifiers)
|
|
||||||
- Solution: Search TMDb by title + release year
|
|
||||||
- First result is used (good enough for most cases)
|
|
||||||
|
|
||||||
### Why No Authentication?
|
|
||||||
- Channels DVR API has no auth (local network only)
|
|
||||||
- Simplifies implementation
|
|
||||||
- Security via network isolation
|
|
||||||
|
|
||||||
### Why Simplified Scanner?
|
|
||||||
- Channels DVR doesn't expose resolution info via API
|
|
||||||
- Defaults all content to non-4K
|
|
||||||
- Future enhancement: parse video files for resolution
|
|
||||||
|
|
||||||
## What's NOT Done (Phase 2 & 3)
|
|
||||||
|
|
||||||
### Phase 2: UI Integration (TODO)
|
|
||||||
- [ ] Settings page for Channels DVR URL configuration
|
|
||||||
- [ ] Server connection test button
|
|
||||||
- [ ] Library selection UI
|
|
||||||
- [ ] Server type selector (Jellyfin/Plex/Emby/Channels DVR)
|
|
||||||
|
|
||||||
### Phase 3: Testing & Polish (TODO)
|
|
||||||
- [ ] Test with real Channels DVR instance (http://192.168.0.15:8089)
|
|
||||||
- [ ] Handle edge cases:
|
|
||||||
- Shows/movies not found on TMDb
|
|
||||||
- Network errors
|
|
||||||
- Invalid URLs
|
|
||||||
- [ ] Add proper error messages
|
|
||||||
- [ ] Document configuration for users
|
|
||||||
- [ ] Consider PR to upstream Seerr project
|
|
||||||
|
|
||||||
## Testing Instructions
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
1. Channels DVR server running (http://192.168.0.15:8089)
|
|
||||||
2. Seerr development environment set up
|
|
||||||
3. Node.js + pnpm installed
|
|
||||||
|
|
||||||
### Manual Testing Steps
|
|
||||||
```bash
|
|
||||||
# 1. Install dependencies
|
|
||||||
cd /home/node/.openclaw/workspace/seerr-explore
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# 2. Build the project
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# 3. Start Seerr
|
|
||||||
pnpm start
|
|
||||||
|
|
||||||
# 4. Configure via UI:
|
|
||||||
# - Go to Settings → Channels DVR
|
|
||||||
# - Enter URL: http://192.168.0.15:8089
|
|
||||||
# - Save
|
|
||||||
|
|
||||||
# 5. Trigger scan:
|
|
||||||
# - Settings → Library Sync → Scan Channels DVR
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Testing (Without Full Seerr)
|
|
||||||
```bash
|
|
||||||
# Test Channels DVR API directly
|
|
||||||
curl http://192.168.0.15:8089/api/v1/shows | jq '.[0]'
|
|
||||||
curl http://192.168.0.15:8089/api/v1/movies | jq '.[0]'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files Changed
|
|
||||||
|
|
||||||
- `server/constants/server.ts` - Added enum value
|
|
||||||
- `server/api/channelsdvr.ts` - New API client
|
|
||||||
- `server/lib/scanners/channelsdvr/index.ts` - New scanner
|
|
||||||
- `server/lib/settings/index.ts` - Added settings interface
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. **Commit changes** to git
|
|
||||||
2. **Test with real Channels DVR** instance
|
|
||||||
3. **Build UI** for configuration (Phase 2)
|
|
||||||
4. **Polish & document** (Phase 3)
|
|
||||||
5. **Consider upstream PR** to Seerr project
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Used Opus for architecture/planning phase
|
|
||||||
- Downgraded to Sonnet for implementation details
|
|
||||||
- Code follows existing Seerr patterns (Jellyfin scanner as reference)
|
|
||||||
- TypeScript types are complete and match Channels DVR API
|
|
||||||
- Ready for testing with real instance
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- Channels DVR API Docs: https://getchannels.com/docs/server-api/introduction/
|
|
||||||
- Channels DVR Instance: http://192.168.0.15:8089
|
|
||||||
- Seerr GitHub: https://github.com/seerr-team/seerr
|
|
||||||
- Our Fork: https://git.bytesnap.io/ByteSnap/channels-seerr
|
|
||||||
@@ -6,12 +6,6 @@ All help is welcome and greatly appreciated! If you would like to contribute to
|
|||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
> Automated AI-generated contributions without human review are not allowed and will be rejected.
|
|
||||||
> This is an open-source project maintained by volunteers.
|
|
||||||
> We do not have the resources to review pull requests that could have been avoided with proper human oversight.
|
|
||||||
> While we have no issue with contributors using AI tools as an aid, it is your responsibility as a contributor to ensure that all submissions are carefully reviewed and meet our quality standards.
|
|
||||||
> Submissions that appear to be unreviewed AI output will be considered low-effort and may result in a ban.
|
|
||||||
>
|
|
||||||
> If you are using **any kind of AI assistance** to contribute to Seerr,
|
> If you are using **any kind of AI assistance** to contribute to Seerr,
|
||||||
> it must be disclosed in the pull request.
|
> it must be disclosed in the pull request.
|
||||||
|
|
||||||
@@ -128,7 +122,7 @@ Steps:
|
|||||||
|
|
||||||
- If you are taking on an existing bug or feature ticket, please comment on the [issue](/../../issues) to avoid multiple people working on the same thing.
|
- If you are taking on an existing bug or feature ticket, please comment on the [issue](/../../issues) to avoid multiple people working on the same thing.
|
||||||
- All commits **must** follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
- All commits **must** follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||||
- Pull requests with titles or commits not following this standard will **not** be merged. PR titles are automatically checked for compliance.
|
- Pull requests with commits not following this standard will **not** be merged.
|
||||||
- Please make meaningful commits, or squash them prior to opening a pull request.
|
- Please make meaningful commits, or squash them prior to opening a pull request.
|
||||||
- Do not squash commits once people have begun reviewing your changes.
|
- Do not squash commits once people have begun reviewing your changes.
|
||||||
- Always rebase your commit to the latest `develop` branch. Do **not** merge `develop` into your branch.
|
- Always rebase your commit to the latest `develop` branch. Do **not** merge `develop` into your branch.
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
# Testing Channels-Seerr with Docker
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
1. **Build and run:**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.test.yml up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Access the web UI:**
|
|
||||||
- Open browser: http://localhost:5055
|
|
||||||
- Complete the setup wizard
|
|
||||||
- Add your Channels DVR server in Settings
|
|
||||||
|
|
||||||
3. **Stop:**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.test.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
- **Config directory:** `./config` (created automatically, persists settings)
|
|
||||||
- **Logs:** `docker-compose logs -f seerr`
|
|
||||||
- **Port:** Default 5055 (change in docker-compose.test.yml if needed)
|
|
||||||
|
|
||||||
## Testing Channels DVR Integration
|
|
||||||
|
|
||||||
1. Start Seerr container
|
|
||||||
2. Navigate to Settings → Channels DVR
|
|
||||||
3. Add your Channels DVR server:
|
|
||||||
- **Server URL:** http://your-channels-server:8089
|
|
||||||
- **Test connection** to verify
|
|
||||||
4. Enable sync jobs (manual or scheduled)
|
|
||||||
5. Check logs for sync activity:
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.test.yml logs -f seerr | grep -i channels
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Testing
|
|
||||||
|
|
||||||
For faster iteration without full rebuilds:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use Dockerfile.local for development
|
|
||||||
docker build -f Dockerfile.local -t channels-seerr:dev .
|
|
||||||
docker run -p 5055:5055 -v ./config:/app/config channels-seerr:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**Build fails:**
|
|
||||||
- Check Node.js version (requires 22.x)
|
|
||||||
- Try: `docker-compose -f docker-compose.test.yml build --no-cache`
|
|
||||||
|
|
||||||
**Can't connect to Channels DVR:**
|
|
||||||
- If Channels is on host machine: Use `http://host.docker.internal:8089`
|
|
||||||
- If on tailnet: Use the Tailscale IP
|
|
||||||
- Check firewall allows connections from Docker network
|
|
||||||
|
|
||||||
**Database issues:**
|
|
||||||
- SQLite (default): Stored in `./config/db/db.sqlite3`
|
|
||||||
- To use Postgres: Uncomment postgres service in docker-compose.test.yml
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
seerr:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
COMMIT_TAG: channels-dvr-test
|
|
||||||
container_name: channels-seerr-test
|
|
||||||
hostname: seerr
|
|
||||||
ports:
|
|
||||||
- "5055:5055"
|
|
||||||
environment:
|
|
||||||
- LOG_LEVEL=debug
|
|
||||||
- TZ=America/Chicago
|
|
||||||
volumes:
|
|
||||||
- ./config:/app/config
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
# Optional: PostgreSQL for production-like testing
|
|
||||||
# Uncomment if you want to test with Postgres instead of SQLite
|
|
||||||
# postgres:
|
|
||||||
# image: postgres:15-alpine
|
|
||||||
# container_name: seerr-postgres
|
|
||||||
# environment:
|
|
||||||
# - POSTGRES_PASSWORD=seerr
|
|
||||||
# - POSTGRES_USER=seerr
|
|
||||||
# - POSTGRES_DB=seerr
|
|
||||||
# volumes:
|
|
||||||
# - postgres-data:/var/lib/postgresql/data
|
|
||||||
# restart: unless-stopped
|
|
||||||
|
|
||||||
# volumes:
|
|
||||||
# postgres-data:
|
|
||||||
@@ -30,7 +30,7 @@ If your PostgreSQL server is configured to accept TCP connections, you can speci
|
|||||||
|
|
||||||
```dotenv
|
```dotenv
|
||||||
DB_TYPE=postgres # Which DB engine to use, either sqlite or postgres. The default is sqlite.
|
DB_TYPE=postgres # Which DB engine to use, either sqlite or postgres. The default is sqlite.
|
||||||
DB_HOST=localhost # (optional) The host (URL) of the database. The default is "localhost".
|
DB_HOST="localhost" # (optional) The host (URL) of the database. The default is "localhost".
|
||||||
DB_PORT="5432" # (optional) The port to connect to. The default is "5432".
|
DB_PORT="5432" # (optional) The port to connect to. The default is "5432".
|
||||||
DB_USER= # (required) Username used to connect to the database.
|
DB_USER= # (required) Username used to connect to the database.
|
||||||
DB_PASS= # (required) Password of the user used to connect to the database.
|
DB_PASS= # (required) Password of the user used to connect to the database.
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
---
|
|
||||||
title: Synology (Advanced)
|
|
||||||
description: Install Seerr on Synology NAS using SynoCommunity
|
|
||||||
sidebar_position: 5
|
|
||||||
---
|
|
||||||
|
|
||||||
# Synology
|
|
||||||
|
|
||||||
:::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 Synology NAS.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Synology NAS running **DSM 7.2** or later
|
|
||||||
- 64-bit architecture (x86_64 or ARMv8)
|
|
||||||
- [SynoCommunity package source](https://synocommunity.com/) added to Package Center
|
|
||||||
|
|
||||||
## Adding the SynoCommunity Package Source
|
|
||||||
|
|
||||||
If you haven't already added SynoCommunity to your Package Center:
|
|
||||||
|
|
||||||
1. Open **Package Center** in DSM
|
|
||||||
2. Click **Settings** in the top-right corner
|
|
||||||
3. Go to the **Package Sources** tab
|
|
||||||
4. Click **Add**
|
|
||||||
5. Enter the following:
|
|
||||||
- **Name**: `SynoCommunity`
|
|
||||||
- **Location**: `https://packages.synocommunity.com`
|
|
||||||
6. Click **OK**
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. In **Package Center**, search for **Seerr**
|
|
||||||
2. Click **Install**
|
|
||||||
3. Follow the installation wizard prompts
|
|
||||||
4. Package Center will automatically install any required dependencies (Node.js v22)
|
|
||||||
|
|
||||||
### Access Seerr
|
|
||||||
|
|
||||||
Once installed, access Seerr at:
|
|
||||||
|
|
||||||
```
|
|
||||||
http://<your-synology-ip>:5055
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also click the **Open** button in Package Center or find Seerr in the DSM main menu.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Seerr's configuration files are stored at:
|
|
||||||
|
|
||||||
```
|
|
||||||
/var/packages/seerr/var/config
|
|
||||||
```
|
|
||||||
|
|
||||||
:::info
|
|
||||||
The Seerr package runs as a dedicated service user managed by DSM. No manual permission configuration is required.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Managing the Service
|
|
||||||
|
|
||||||
You can start, stop, and restart Seerr from **Package Center** → Find Seerr → Use the action buttons.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
When a new version is available:
|
|
||||||
|
|
||||||
1. Open **Package Center**
|
|
||||||
2. Go to **Installed** packages
|
|
||||||
3. Find **Seerr** and click **Update** if available
|
|
||||||
|
|
||||||
:::tip
|
|
||||||
Enable automatic updates in Package Center settings to keep Seerr up to date.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Viewing Logs
|
|
||||||
|
|
||||||
Seerr logs are located at `/var/packages/seerr/var/config/logs` and can be accessed using:
|
|
||||||
|
|
||||||
- **File Browser** package (recommended for most users)
|
|
||||||
- SSH (advanced users)
|
|
||||||
|
|
||||||
### Port Conflicts
|
|
||||||
|
|
||||||
Seerr uses port 5055. If this port is already in use:
|
|
||||||
|
|
||||||
- **Docker containers**: Remap the conflicting container to a different port
|
|
||||||
- **Other packages**: The conflicting package will need to be uninstalled as Seerr's port cannot be changed
|
|
||||||
|
|
||||||
SynoCommunity ensures there are no port conflicts with other SynoCommunity packages or official Synology packages.
|
|
||||||
|
|
||||||
### Package Won't Start
|
|
||||||
|
|
||||||
Ensure Node.js v22 is installed and running by checking its status in **Package Center**.
|
|
||||||
|
|
||||||
## Uninstallation
|
|
||||||
|
|
||||||
1. Open **Package Center**
|
|
||||||
2. Find **Seerr** in your installed packages
|
|
||||||
3. Click **Uninstall**
|
|
||||||
|
|
||||||
:::caution
|
|
||||||
Uninstalling will remove the application but preserve your configuration data by default. Select "Remove data" during uninstallation if you want a complete removal.
|
|
||||||
:::
|
|
||||||
@@ -4,6 +4,12 @@ description: Install Seerr using TrueNAS
|
|||||||
sidebar_position: 4
|
sidebar_position: 4
|
||||||
---
|
---
|
||||||
# TrueNAS
|
# TrueNAS
|
||||||
|
:::danger
|
||||||
|
This method has not yet been updated for Seerr and is currently a work in progress.
|
||||||
|
You can follow the ongoing work on this issue https://github.com/truenas/apps/issues/3374.
|
||||||
|
:::
|
||||||
|
|
||||||
|
<!--
|
||||||
:::warning
|
:::warning
|
||||||
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
Third-party installation methods are maintained by the community. The Seerr team is not responsible for these packages.
|
||||||
:::
|
:::
|
||||||
@@ -11,7 +17,4 @@ Third-party installation methods are maintained by the community. The Seerr team
|
|||||||
:::warning
|
:::warning
|
||||||
This method is not recommended for most users. It is intended for advanced users who are using TrueNAS distribution.
|
This method is not recommended for most users. It is intended for advanced users who are using TrueNAS distribution.
|
||||||
:::
|
:::
|
||||||
|
-->
|
||||||
## Installation
|
|
||||||
|
|
||||||
Go to the 'Apps' menu, click the 'Discover Apps' button in the top right, search for 'Seerr' in the search bar, and install the app.
|
|
||||||
|
|||||||
@@ -21,14 +21,6 @@ If an official Unraid Community Applications template for Seerr isn't available
|
|||||||
|
|
||||||
### 1. Create the config directory
|
### 1. Create the config directory
|
||||||
|
|
||||||
:::note
|
|
||||||
Seerr is now rootless. Unraid typically runs Docker containers as `nobody:users` (UID 99, GID 100), but Seerr now runs internally as UID 1000, GID 1000. This creates a permission mismatch.
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::info
|
|
||||||
**If migrating**: Copy your existing Jellyseerr/Overseerr config files (e.g., from `/mnt/user/appdata/overseerr/` or `/mnt/user/appdata/jellyseerr`) to `/mnt/user/appdata/seerr`, then apply the permissions below
|
|
||||||
:::
|
|
||||||
|
|
||||||
Open the Unraid terminal and run:
|
Open the Unraid terminal and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -210,42 +210,7 @@ See https://aur.archlinux.org/packages/seerr
|
|||||||
|
|
||||||
### TrueNAS
|
### TrueNAS
|
||||||
|
|
||||||
Refer to [Seerr TrueNAS Documentation](/getting-started/third-parties/truenas), all of our examples have been updated to reflect the below change.
|
Waiting for https://github.com/truenas/apps/issues/3374
|
||||||
|
|
||||||
<Tabs groupId="truenas-migration" queryString>
|
|
||||||
<TabItem value="hostpath" label="Host Path">
|
|
||||||
**This guide describes how to migrate from Host Path storage (not ixVolume).**
|
|
||||||
1. Stop Jellyseerr/Overseerr
|
|
||||||
2. Install Seerr and use the same Host Path storage that was used by Jellyseerr/Overseerr
|
|
||||||
3. Start Seerr app
|
|
||||||
4. Delete Jellyseerr/Overseerr app
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="ixvolume" label="ixVolume">
|
|
||||||
**This guide describes how to migrate from ixVolume storage (not Host Path).**
|
|
||||||
1. Stop Jellyseerr/Overseerr
|
|
||||||
2. Create a dataset for Seerr
|
|
||||||
If your apps normally store data under something like:
|
|
||||||
```
|
|
||||||
/mnt/storage/<app-name>
|
|
||||||
```
|
|
||||||
then create a dataset named:
|
|
||||||
```
|
|
||||||
storage/seerr
|
|
||||||
```
|
|
||||||
resulting in:
|
|
||||||
```
|
|
||||||
/mnt/storage/seerr
|
|
||||||
```
|
|
||||||
3. Copy ixVolume Data
|
|
||||||
Open System Settings → Shell, or SSH into your TrueNAS server as root and run :
|
|
||||||
```bash
|
|
||||||
rsync -av /mnt/.ix-apps/app_mounts/jellyseerr/ /mnt/storage/seerr/
|
|
||||||
```
|
|
||||||
4. Install Seerr and use the same Host Path storage that was created before (`/mnt/storage/seerr/config` in our example)
|
|
||||||
5. Start Seerr app
|
|
||||||
6. Delete Jellyseerr/Overseerr app
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
### Unraid
|
### Unraid
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Please check how to migrate to Seerr in our [migration guide](https://docs.seerr
|
|||||||
|
|
||||||
Seerr brings several features that were previously available in Jellyseerr but missing from Overseerr. These additions improve flexibility, performance, and overall control for admins and power users:
|
Seerr brings several features that were previously available in Jellyseerr but missing from Overseerr. These additions improve flexibility, performance, and overall control for admins and power users:
|
||||||
|
|
||||||
* **Alternative media solution:** Added support for Jellyfin and Emby as alternatives to Plex. Only one integration can be used at a time.
|
* **Alternative media solution:** Added support for Jellyfin and Emby in addition to the existing Plex integration.
|
||||||
* **PostgreSQL support**: In addition to SQLite, you can now opt in to using a PostgreSQL database.
|
* **PostgreSQL support**: In addition to SQLite, you can now opt in to using a PostgreSQL database.
|
||||||
* **Blocklist for movies, series, and tags**: Allows permitted users to hide movies, series, or tags from regular users.
|
* **Blocklist for movies, series, and tags**: Allows permitted users to hide movies, series, or tags from regular users.
|
||||||
* **Override rules**: Adjust default request settings based on conditions such as user, tag, or other criteria.
|
* **Override rules**: Adjust default request settings based on conditions such as user, tag, or other criteria.
|
||||||
|
|||||||
@@ -16,12 +16,7 @@ const config: Config = {
|
|||||||
deploymentBranch: 'gh-pages',
|
deploymentBranch: 'gh-pages',
|
||||||
|
|
||||||
onBrokenLinks: 'throw',
|
onBrokenLinks: 'throw',
|
||||||
|
onBrokenMarkdownLinks: 'warn',
|
||||||
markdown: {
|
|
||||||
hooks: {
|
|
||||||
onBrokenMarkdownLinks: 'warn',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'en',
|
||||||
|
|||||||
@@ -1,220 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import ExternalAPI from '@server/api/externalapi';
|
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
|
||||||
import logger from '@server/logger';
|
|
||||||
|
|
||||||
export interface ChannelsDVRShow {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
summary: string;
|
|
||||||
image_url: string;
|
|
||||||
release_year: number;
|
|
||||||
release_date: string;
|
|
||||||
genres: string[];
|
|
||||||
categories: string[];
|
|
||||||
labels: string[];
|
|
||||||
cast: string[];
|
|
||||||
episode_count: number;
|
|
||||||
number_unwatched: number;
|
|
||||||
favorited: boolean;
|
|
||||||
last_watched_at?: number;
|
|
||||||
last_recorded_at?: number;
|
|
||||||
created_at: number;
|
|
||||||
updated_at: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChannelsDVRMovie {
|
|
||||||
id: string;
|
|
||||||
program_id: string;
|
|
||||||
path: string;
|
|
||||||
channel: string;
|
|
||||||
title: string;
|
|
||||||
summary: string;
|
|
||||||
full_summary: string;
|
|
||||||
content_rating: string;
|
|
||||||
image_url: string;
|
|
||||||
thumbnail_url: string;
|
|
||||||
duration: number;
|
|
||||||
playback_time: number;
|
|
||||||
release_year: number;
|
|
||||||
release_date: string;
|
|
||||||
genres: string[];
|
|
||||||
tags: string[];
|
|
||||||
labels: string[];
|
|
||||||
categories: string[];
|
|
||||||
cast: string[];
|
|
||||||
directors: string[];
|
|
||||||
watched: boolean;
|
|
||||||
favorited: boolean;
|
|
||||||
delayed: boolean;
|
|
||||||
cancelled: boolean;
|
|
||||||
corrupted: boolean;
|
|
||||||
completed: boolean;
|
|
||||||
processed: boolean;
|
|
||||||
verified: boolean;
|
|
||||||
last_watched_at?: number;
|
|
||||||
created_at: number;
|
|
||||||
updated_at: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChannelsDVREpisode {
|
|
||||||
id: string;
|
|
||||||
show_id: string;
|
|
||||||
program_id: string;
|
|
||||||
path: string;
|
|
||||||
channel: string;
|
|
||||||
season_number: number;
|
|
||||||
episode_number: number;
|
|
||||||
title: string;
|
|
||||||
episode_title: string;
|
|
||||||
summary: string;
|
|
||||||
full_summary: string;
|
|
||||||
content_rating: string;
|
|
||||||
image_url: string;
|
|
||||||
thumbnail_url: string;
|
|
||||||
duration: number;
|
|
||||||
playback_time: number;
|
|
||||||
original_air_date: string;
|
|
||||||
genres: string[];
|
|
||||||
tags: string[];
|
|
||||||
categories: string[];
|
|
||||||
cast: string[];
|
|
||||||
commercials: number[];
|
|
||||||
watched: boolean;
|
|
||||||
favorited: boolean;
|
|
||||||
delayed: boolean;
|
|
||||||
cancelled: boolean;
|
|
||||||
corrupted: boolean;
|
|
||||||
completed: boolean;
|
|
||||||
processed: boolean;
|
|
||||||
locked: boolean;
|
|
||||||
verified: boolean;
|
|
||||||
created_at: number;
|
|
||||||
updated_at: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChannelsDVRAPI extends ExternalAPI {
|
|
||||||
constructor(baseUrl: string) {
|
|
||||||
super(
|
|
||||||
baseUrl,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'User-Agent': `Seerr/${getAppVersion()}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all TV shows from Channels DVR library
|
|
||||||
*/
|
|
||||||
public async getShows(): Promise<ChannelsDVRShow[]> {
|
|
||||||
try {
|
|
||||||
const data = await this.get<ChannelsDVRShow[]>('/api/v1/shows');
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Failed to fetch shows from Channels DVR', {
|
|
||||||
label: 'Channels DVR API',
|
|
||||||
errorMessage: e.message,
|
|
||||||
});
|
|
||||||
throw new Error('Failed to fetch shows from Channels DVR');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a specific show by ID
|
|
||||||
*/
|
|
||||||
public async getShow(showId: string): Promise<ChannelsDVRShow> {
|
|
||||||
try {
|
|
||||||
const data = await this.get<ChannelsDVRShow>(`/api/v1/shows/${showId}`);
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to fetch show ${showId} from Channels DVR`,
|
|
||||||
{
|
|
||||||
label: 'Channels DVR API',
|
|
||||||
errorMessage: e.message,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
throw new Error(`Failed to fetch show ${showId} from Channels DVR`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all episodes for a specific show
|
|
||||||
*/
|
|
||||||
public async getShowEpisodes(showId: string): Promise<ChannelsDVREpisode[]> {
|
|
||||||
try {
|
|
||||||
const data = await this.get<ChannelsDVREpisode[]>(
|
|
||||||
`/api/v1/shows/${showId}/episodes`
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to fetch episodes for show ${showId} from Channels DVR`,
|
|
||||||
{
|
|
||||||
label: 'Channels DVR API',
|
|
||||||
errorMessage: e.message,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
throw new Error(
|
|
||||||
`Failed to fetch episodes for show ${showId} from Channels DVR`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all movies from Channels DVR library
|
|
||||||
*/
|
|
||||||
public async getMovies(): Promise<ChannelsDVRMovie[]> {
|
|
||||||
try {
|
|
||||||
const data = await this.get<ChannelsDVRMovie[]>('/api/v1/movies');
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Failed to fetch movies from Channels DVR', {
|
|
||||||
label: 'Channels DVR API',
|
|
||||||
errorMessage: e.message,
|
|
||||||
});
|
|
||||||
throw new Error('Failed to fetch movies from Channels DVR');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a specific movie by ID
|
|
||||||
*/
|
|
||||||
public async getMovie(movieId: string): Promise<ChannelsDVRMovie> {
|
|
||||||
try {
|
|
||||||
const data = await this.get<ChannelsDVRMovie>(`/api/v1/movies/${movieId}`);
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to fetch movie ${movieId} from Channels DVR`,
|
|
||||||
{
|
|
||||||
label: 'Channels DVR API',
|
|
||||||
errorMessage: e.message,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
throw new Error(`Failed to fetch movie ${movieId} from Channels DVR`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test connectivity to Channels DVR server
|
|
||||||
*/
|
|
||||||
public async testConnection(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
// Try to fetch shows list as a connectivity test
|
|
||||||
await this.getShows();
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Channels DVR connection test failed', {
|
|
||||||
label: 'Channels DVR API',
|
|
||||||
errorMessage: e.message,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChannelsDVRAPI;
|
|
||||||
@@ -2,7 +2,6 @@ export enum MediaServerType {
|
|||||||
PLEX = 1,
|
PLEX = 1,
|
||||||
JELLYFIN,
|
JELLYFIN,
|
||||||
EMBY,
|
EMBY,
|
||||||
CHANNELS_DVR,
|
|
||||||
NOT_CONFIGURED,
|
NOT_CONFIGURED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,19 +206,6 @@ class Media {
|
|||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetServiceData(): void {
|
|
||||||
this.serviceId = null;
|
|
||||||
this.serviceId4k = null;
|
|
||||||
this.externalServiceId = null;
|
|
||||||
this.externalServiceId4k = null;
|
|
||||||
this.externalServiceSlug = null;
|
|
||||||
this.externalServiceSlug4k = null;
|
|
||||||
this.ratingKey = null;
|
|
||||||
this.ratingKey4k = null;
|
|
||||||
this.jellyfinMediaId = null;
|
|
||||||
this.jellyfinMediaId4k = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterLoad()
|
@AfterLoad()
|
||||||
public setPlexUrls(): void {
|
public setPlexUrls(): void {
|
||||||
const { machineId, webAppUrl } = getSettings().plex;
|
const { machineId, webAppUrl } = getSettings().plex;
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
import ChannelsDVRAPI, {
|
|
||||||
type ChannelsDVRMovie,
|
|
||||||
type ChannelsDVRShow,
|
|
||||||
} from '@server/api/channelsdvr';
|
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
|
||||||
import { MediaServerType } from '@server/constants/server';
|
|
||||||
import BaseScanner from '@server/lib/scanners/baseScanner';
|
|
||||||
import type {
|
|
||||||
ProcessableSeason,
|
|
||||||
RunnableScanner,
|
|
||||||
StatusBase,
|
|
||||||
} from '@server/lib/scanners/baseScanner';
|
|
||||||
import type { Library } from '@server/lib/settings';
|
|
||||||
import { getSettings } from '@server/lib/settings';
|
|
||||||
import logger from '@server/logger';
|
|
||||||
|
|
||||||
interface ChannelsDVRSyncStatus extends StatusBase {
|
|
||||||
currentLibrary?: Library;
|
|
||||||
libraries: Library[];
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChannelsDVRScanner
|
|
||||||
extends BaseScanner<ChannelsDVRMovie | ChannelsDVRShow>
|
|
||||||
implements RunnableScanner<ChannelsDVRSyncStatus>
|
|
||||||
{
|
|
||||||
private channelsClient: ChannelsDVRAPI;
|
|
||||||
private libraries: Library[];
|
|
||||||
private currentLibrary?: Library;
|
|
||||||
private isRecentOnly = false;
|
|
||||||
|
|
||||||
constructor({ isRecentOnly }: { isRecentOnly?: boolean } = {}) {
|
|
||||||
super('Channels DVR Sync');
|
|
||||||
this.isRecentOnly = isRecentOnly ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find TMDb ID for a movie by searching title and year
|
|
||||||
*/
|
|
||||||
private async findMovieTmdbId(
|
|
||||||
title: string,
|
|
||||||
releaseYear: number
|
|
||||||
): Promise<number | null> {
|
|
||||||
try {
|
|
||||||
// Clean up title (remove year suffix if present)
|
|
||||||
const cleanTitle = title.replace(/\s*\(\d{4}\)\s*$/, '').trim();
|
|
||||||
|
|
||||||
this.log(
|
|
||||||
`Searching TMDb for movie: "${cleanTitle}" (${releaseYear})`,
|
|
||||||
'debug'
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchResults = await this.tmdb.searchMovies({
|
|
||||||
query: cleanTitle,
|
|
||||||
page: 1,
|
|
||||||
year: releaseYear,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (searchResults.results.length === 0) {
|
|
||||||
this.log(
|
|
||||||
`No TMDb results found for movie: "${cleanTitle}" (${releaseYear})`,
|
|
||||||
'warn'
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the first result
|
|
||||||
const tmdbId = searchResults.results[0].id;
|
|
||||||
this.log(
|
|
||||||
`Found TMDb ID ${tmdbId} for movie: "${cleanTitle}" (${releaseYear})`,
|
|
||||||
'debug'
|
|
||||||
);
|
|
||||||
return tmdbId;
|
|
||||||
} catch (e) {
|
|
||||||
this.log(
|
|
||||||
`Error searching TMDb for movie: "${title}" (${releaseYear})`,
|
|
||||||
'error',
|
|
||||||
{ errorMessage: e.message }
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find TMDb ID for a TV show by searching name and year
|
|
||||||
*/
|
|
||||||
private async findShowTmdbId(
|
|
||||||
name: string,
|
|
||||||
releaseYear: number
|
|
||||||
): Promise<number | null> {
|
|
||||||
try {
|
|
||||||
this.log(`Searching TMDb for show: "${name}" (${releaseYear})`, 'debug');
|
|
||||||
|
|
||||||
const searchResults = await this.tmdb.searchTvShows({
|
|
||||||
query: name,
|
|
||||||
page: 1,
|
|
||||||
firstAirDateYear: releaseYear,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (searchResults.results.length === 0) {
|
|
||||||
this.log(
|
|
||||||
`No TMDb results found for show: "${name}" (${releaseYear})`,
|
|
||||||
'warn'
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the first result
|
|
||||||
const tmdbId = searchResults.results[0].id;
|
|
||||||
this.log(
|
|
||||||
`Found TMDb ID ${tmdbId} for show: "${name}" (${releaseYear})`,
|
|
||||||
'debug'
|
|
||||||
);
|
|
||||||
return tmdbId;
|
|
||||||
} catch (e) {
|
|
||||||
this.log(
|
|
||||||
`Error searching TMDb for show: "${name}" (${releaseYear})`,
|
|
||||||
'error',
|
|
||||||
{ errorMessage: e.message }
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a Channels DVR movie
|
|
||||||
*/
|
|
||||||
private async processChannelsDVRMovie(movie: ChannelsDVRMovie) {
|
|
||||||
try {
|
|
||||||
// Find TMDb ID by searching title and year
|
|
||||||
const tmdbId = await this.findMovieTmdbId(
|
|
||||||
movie.title,
|
|
||||||
movie.release_year
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tmdbId) {
|
|
||||||
this.log(
|
|
||||||
`Skipping movie "${movie.title}" - could not find TMDb ID`,
|
|
||||||
'warn'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channels DVR doesn't provide resolution info in the API
|
|
||||||
// We'll default to non-4K for now
|
|
||||||
const mediaAddedAt = new Date(movie.created_at);
|
|
||||||
|
|
||||||
await this.processMovie(tmdbId, {
|
|
||||||
is4k: false,
|
|
||||||
mediaAddedAt,
|
|
||||||
ratingKey: movie.id,
|
|
||||||
title: movie.title,
|
|
||||||
serviceId: this.channelsClient.baseUrl,
|
|
||||||
externalServiceId: this.channelsClient.baseUrl,
|
|
||||||
externalServiceSlug: 'channelsdvr',
|
|
||||||
tmdbId: tmdbId,
|
|
||||||
processing: !movie.completed,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.log(`Processed movie: ${movie.title} (TMDb ID: ${tmdbId})`, 'info');
|
|
||||||
} catch (e) {
|
|
||||||
this.log(
|
|
||||||
`Error processing Channels DVR movie: ${movie.title}`,
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
errorMessage: e.message,
|
|
||||||
movieId: movie.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a Channels DVR TV show
|
|
||||||
*/
|
|
||||||
private async processChannelsDVRShow(show: ChannelsDVRShow) {
|
|
||||||
try {
|
|
||||||
// Find TMDb ID by searching name and year
|
|
||||||
const tmdbId = await this.findShowTmdbId(show.name, show.release_year);
|
|
||||||
|
|
||||||
if (!tmdbId) {
|
|
||||||
this.log(
|
|
||||||
`Skipping show "${show.name}" - could not find TMDb ID`,
|
|
||||||
'warn'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mediaAddedAt = new Date(show.created_at);
|
|
||||||
|
|
||||||
// Fetch all episodes for the show from Channels DVR
|
|
||||||
const episodes = await this.channelsClient.getShowEpisodes(show.id);
|
|
||||||
|
|
||||||
// Group episodes by season
|
|
||||||
const seasonMap = new Map<number, ProcessableSeason>();
|
|
||||||
|
|
||||||
for (const episode of episodes) {
|
|
||||||
const seasonNumber = episode.season_number;
|
|
||||||
const episodeNumber = episode.episode_number;
|
|
||||||
|
|
||||||
if (!seasonMap.has(seasonNumber)) {
|
|
||||||
seasonMap.set(seasonNumber, {
|
|
||||||
seasonNumber,
|
|
||||||
episodes: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const season = seasonMap.get(seasonNumber)!;
|
|
||||||
season.episodes.push({
|
|
||||||
episodeNumber,
|
|
||||||
ratingKey: episode.id,
|
|
||||||
mediaAddedAt: new Date(episode.created_at),
|
|
||||||
processing: !episode.completed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const seasons = Array.from(seasonMap.values());
|
|
||||||
|
|
||||||
await this.processTvShow(tmdbId, {
|
|
||||||
seasons,
|
|
||||||
ratingKey: show.id,
|
|
||||||
title: show.name,
|
|
||||||
serviceId: this.channelsClient.baseUrl,
|
|
||||||
externalServiceId: this.channelsClient.baseUrl,
|
|
||||||
externalServiceSlug: 'channelsdvr',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.log(
|
|
||||||
`Processed show: ${show.name} (TMDb ID: ${tmdbId}, ${episodes.length} episodes)`,
|
|
||||||
'info'
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.log(
|
|
||||||
`Error processing Channels DVR show: ${show.name}`,
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
errorMessage: e.message,
|
|
||||||
showId: show.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async run(): Promise<void> {
|
|
||||||
const settings = getSettings();
|
|
||||||
const sessionManager = settings.main.sessionManager;
|
|
||||||
|
|
||||||
if (!settings.channelsdvr.url) {
|
|
||||||
this.log('Channels DVR URL not configured, skipping scan', 'warn');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.channelsClient = new ChannelsDVRAPI(settings.channelsdvr.url);
|
|
||||||
|
|
||||||
// Test connection
|
|
||||||
const connected = await this.channelsClient.testConnection();
|
|
||||||
if (!connected) {
|
|
||||||
throw new Error('Failed to connect to Channels DVR server');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log('Successfully connected to Channels DVR', 'info');
|
|
||||||
|
|
||||||
// Fetch and process all movies
|
|
||||||
this.log('Fetching movies from Channels DVR...', 'info');
|
|
||||||
const movies = await this.channelsClient.getMovies();
|
|
||||||
this.log(`Found ${movies.length} movies`, 'info');
|
|
||||||
|
|
||||||
for (const movie of movies) {
|
|
||||||
await this.processChannelsDVRMovie(movie);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch and process all TV shows
|
|
||||||
this.log('Fetching TV shows from Channels DVR...', 'info');
|
|
||||||
const shows = await this.channelsClient.getShows();
|
|
||||||
this.log(`Found ${shows.length} TV shows`, 'info');
|
|
||||||
|
|
||||||
for (const show of shows) {
|
|
||||||
await this.processChannelsDVRShow(show);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log('Channels DVR sync completed', 'info');
|
|
||||||
} catch (e) {
|
|
||||||
this.log('Channels DVR sync failed', 'error', {
|
|
||||||
errorMessage: e.message,
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async cancel(): Promise<void> {
|
|
||||||
this.cancelled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public status(): ChannelsDVRSyncStatus {
|
|
||||||
return {
|
|
||||||
running: this.running,
|
|
||||||
progress: 0,
|
|
||||||
total: 0,
|
|
||||||
currentLibrary: this.currentLibrary,
|
|
||||||
libraries: this.libraries ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChannelsDVRScanner;
|
|
||||||
@@ -49,13 +49,6 @@ export interface JellyfinSettings {
|
|||||||
serverId: string;
|
serverId: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChannelsDVRSettings {
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
libraries: Library[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TautulliSettings {
|
export interface TautulliSettings {
|
||||||
hostname?: string;
|
hostname?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
@@ -362,7 +355,6 @@ export interface AllSettings {
|
|||||||
main: MainSettings;
|
main: MainSettings;
|
||||||
plex: PlexSettings;
|
plex: PlexSettings;
|
||||||
jellyfin: JellyfinSettings;
|
jellyfin: JellyfinSettings;
|
||||||
channelsdvr: ChannelsDVRSettings;
|
|
||||||
tautulli: TautulliSettings;
|
tautulli: TautulliSettings;
|
||||||
radarr: RadarrSettings[];
|
radarr: RadarrSettings[];
|
||||||
sonarr: SonarrSettings[];
|
sonarr: SonarrSettings[];
|
||||||
@@ -431,11 +423,6 @@ class Settings {
|
|||||||
serverId: '',
|
serverId: '',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
},
|
},
|
||||||
channelsdvr: {
|
|
||||||
name: 'Channels DVR',
|
|
||||||
url: '',
|
|
||||||
libraries: [],
|
|
||||||
},
|
|
||||||
tautulli: {},
|
tautulli: {},
|
||||||
metadataSettings: {
|
metadataSettings: {
|
||||||
tv: MetadataProviderType.TMDB,
|
tv: MetadataProviderType.TMDB,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class WatchlistSync {
|
|||||||
[
|
[
|
||||||
Permission.AUTO_REQUEST,
|
Permission.AUTO_REQUEST,
|
||||||
Permission.AUTO_REQUEST_MOVIE,
|
Permission.AUTO_REQUEST_MOVIE,
|
||||||
Permission.AUTO_REQUEST_TV,
|
Permission.AUTO_APPROVE_TV,
|
||||||
],
|
],
|
||||||
{ type: 'or' }
|
{ type: 'or' }
|
||||||
)
|
)
|
||||||
@@ -70,33 +70,13 @@ class WatchlistSync {
|
|||||||
response.items.map((i) => i.tmdbId)
|
response.items.map((i) => i.tmdbId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const watchlistTmdbIds = response.items.map((i) => i.tmdbId);
|
|
||||||
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
const existingAutoRequests = await requestRepository
|
|
||||||
.createQueryBuilder('request')
|
|
||||||
.leftJoinAndSelect('request.media', 'media')
|
|
||||||
.where('request.requestedBy = :userId', { userId: user.id })
|
|
||||||
.andWhere('request.isAutoRequest = true')
|
|
||||||
.andWhere('media.tmdbId IN (:...tmdbIds)', { tmdbIds: watchlistTmdbIds })
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
const autoRequestedTmdbIds = new Set(
|
|
||||||
existingAutoRequests
|
|
||||||
.filter((r) => r.media != null)
|
|
||||||
.map((r) => `${r.media.mediaType}:${r.media.tmdbId}`)
|
|
||||||
);
|
|
||||||
|
|
||||||
const unavailableItems = response.items.filter(
|
const unavailableItems = response.items.filter(
|
||||||
|
// If we can find watchlist items in our database that are also available, we should exclude them
|
||||||
(i) =>
|
(i) =>
|
||||||
!autoRequestedTmdbIds.has(
|
|
||||||
`${i.type === 'show' ? MediaType.TV : MediaType.MOVIE}:${i.tmdbId}`
|
|
||||||
) &&
|
|
||||||
!mediaItems.find(
|
!mediaItems.find(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.tmdbId === i.tmdbId &&
|
m.tmdbId === i.tmdbId &&
|
||||||
(m.status === MediaStatus.BLOCKLISTED ||
|
((m.status !== MediaStatus.UNKNOWN && m.mediaType === 'movie') ||
|
||||||
(m.status !== MediaStatus.UNKNOWN && m.mediaType === 'movie') ||
|
|
||||||
(m.mediaType === 'tv' && m.status === MediaStatus.AVAILABLE))
|
(m.mediaType === 'tv' && m.status === MediaStatus.AVAILABLE))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -174,12 +174,7 @@ mediaRoutes.delete(
|
|||||||
where: { id: Number(req.params.id) },
|
where: { id: Number(req.params.id) },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (media.status === MediaStatus.BLOCKLISTED) {
|
await mediaRepository.remove(media);
|
||||||
media.resetServiceData();
|
|
||||||
await mediaRepository.save(media);
|
|
||||||
} else {
|
|
||||||
await mediaRepository.remove(media);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(204).send();
|
return res.status(204).send();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -947,6 +947,15 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
|
|||||||
try {
|
try {
|
||||||
await this.sendToRadarr(event.entity as MediaRequest);
|
await this.sendToRadarr(event.entity as MediaRequest);
|
||||||
await this.sendToSonarr(event.entity as MediaRequest);
|
await this.sendToSonarr(event.entity as MediaRequest);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error while sending to *arr in afterUpdate subscriber', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: (event.entity as MediaRequest).id,
|
||||||
|
errorMessage: e instanceof Error ? e.message : String(e),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await this.updateParentStatus(event.entity as MediaRequest);
|
await this.updateParentStatus(event.entity as MediaRequest);
|
||||||
|
|
||||||
if (event.entity.status === MediaRequestStatus.COMPLETED) {
|
if (event.entity.status === MediaRequestStatus.COMPLETED) {
|
||||||
@@ -958,11 +967,14 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error in afterUpdate subscriber', {
|
logger.error(
|
||||||
label: 'Media Request',
|
'Error while updating parent status in afterUpdate subscriber',
|
||||||
requestId: (event.entity as MediaRequest).id,
|
{
|
||||||
errorMessage: e instanceof Error ? e.message : String(e),
|
label: 'Media Request',
|
||||||
});
|
requestId: (event.entity as MediaRequest).id,
|
||||||
|
errorMessage: e instanceof Error ? e.message : String(e),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,14 +986,26 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
|
|||||||
try {
|
try {
|
||||||
await this.sendToRadarr(event.entity as MediaRequest);
|
await this.sendToRadarr(event.entity as MediaRequest);
|
||||||
await this.sendToSonarr(event.entity as MediaRequest);
|
await this.sendToSonarr(event.entity as MediaRequest);
|
||||||
await this.updateParentStatus(event.entity as MediaRequest);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error in afterInsert subscriber', {
|
logger.error('Error while sending to *arr in afterInsert subscriber', {
|
||||||
label: 'Media Request',
|
label: 'Media Request',
|
||||||
requestId: (event.entity as MediaRequest).id,
|
requestId: (event.entity as MediaRequest).id,
|
||||||
errorMessage: e instanceof Error ? e.message : String(e),
|
errorMessage: e instanceof Error ? e.message : String(e),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.updateParentStatus(event.entity as MediaRequest);
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
'Error while updating parent status in afterInsert subscriber',
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: (event.entity as MediaRequest).id,
|
||||||
|
errorMessage: e instanceof Error ? e.message : String(e),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async afterRemove(event: RemoveEvent<MediaRequest>): Promise<void> {
|
public async afterRemove(event: RemoveEvent<MediaRequest>): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user