Compare commits

...

18 Commits

Author SHA1 Message Date
fallenbagel
b30794dd20 fix(base-scanner): derive media availability from actual season state instead of scanner input
The media-level status was incorrectly set to AVAILABLE when only some seasons were available. The
issue was isAllStandardSeasons/isAll4kSeasons checked the scanner's input array which only contains
seasons found by current scanner (e.g. 1 seaason in jellyfin). If that single season was fully
available, the check passes and the media gets marked AVAILABLE regardless of how many total seasons
existed. And the shouldStayAvailable (and its 4k check) locked the AVAILABLE status once set, as
long as no new seasons were added with a status which prevented the incorrect status from correcting
itself.
2026-02-14 00:46:25 +08:00
fallenbagel
1ed86c14c0 fix(media-request-subscriber): prevent mediald nullification from cascade saves (#2356) 2026-02-13 15:02:22 +05:00
fallenbagel
91261f6a61 fix(settings): DNS cache UI consistency, validation, and conditional rendering (#2382) 2026-02-13 00:16:10 +01:00
Gauthier
3dea58eead fix(overriderules): display the users of an override rule (#2410) 2026-02-12 23:20:16 +01:00
Gauthier
3eea8ee98e fix(watchlist): remove error log when a media from the watchlist is blacklisted (#2407) 2026-02-12 13:16:56 +05:00
Ludovic Ortega
7cd3521cfd docs(docker): document available image tags and their usage (#2402)
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2026-02-11 19:17:03 +05:00
Gauthier
e53c2a34dc docs(blog): update authors description and add Discord link (#2405)
Co-authored-by: fallenbagel <98979876+fallenbagel@users.noreply.github.com>
2026-02-11 18:42:52 +05:00
Gauthier
095784bf62 docs(blog): add Seerr release blog post (#2401)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 13:50:28 +01:00
fallenbagel
4f7819f028 fix: add IF EXISTS to SQLite migration DROP INDEX statements (#2398) 2026-02-10 14:25:36 +01:00
Gauthier
028c7c2434 fix(overriderules): test service using the right endpoint (#2399) 2026-02-10 13:18:56 +01:00
0xsysr3ll
e3dc1c302d fix(webpush): avoid querying push subs with empty user list (#2380) 2026-02-09 15:34:13 +01:00
fallenbagel
a44a3b1e14 perf: add database indexes & minor frontend/backend improvements (#2396) 2026-02-09 17:06:53 +05:00
fallenbagel
186998b888 chore(workflows): remove redundant Hugging Face model cache (#2397) 2026-02-09 12:01:55 +01:00
0xsysr3ll
df54fb9451 ci(workflow): add i18n label check to avoid duplicate comments (#2390) 2026-02-09 10:30:25 +01:00
seerr-weblate-bot
673f3f2939 chore(i18n): update translations from Weblate (#2395)
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Ulrik J <ulrik.johansen@me.com>
Co-authored-by: fallenbagel <98979876+fallenbagel@users.noreply.github.com>
2026-02-09 07:17:17 +05:00
fallenbagel
3cd66589ca chore(i18n): rescue translations from #2384 (#2394)
Co-authored-by: Ulrik J <ulrik.johansen@me.com>
2026-02-09 07:07:11 +05:00
fallenbagel
dbee2fdf9f ci(duplicate-issues): migrate to pnpm from npm (#2388) 2026-02-08 12:28:02 +00:00
fallenbagel
0ffe3e8067 ci(issues): add LLM-driven duplicate issue detection (#2381) 2026-02-08 15:45:49 +05:00
75 changed files with 2737 additions and 345 deletions

View File

@@ -67,6 +67,7 @@ jobs:
- name: i18n Check
shell: bash
env:
I18N_LABEL: i18n-out-of-sync
BODY: |
The i18n check failed because translation messages are out of sync.
@@ -75,16 +76,16 @@ jobs:
Please run `pnpm i18n:extract` and commit the changes.
run: |
retry() { n=0; until "$@"; do n=$((n+1)); [ $n -ge 3 ] && break; echo "retry $n: $*" >&2; sleep 2; done; }
node bin/check-i18n.js
check_failed=$?
if [ $check_failed -eq 1 ]; then
retry gh pr edit "$NUMBER" -R "$GH_REPO" --add-label "i18n-out-of-sync" || true
retry gh pr comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true
check_failed=0; node bin/check-i18n.js || check_failed=$?
pr_labels=$(gh pr view "$NUMBER" -R "$GH_REPO" --json labels -q '.labels[].name' 2>/dev/null) || true
has_label=0
while IFS= read -r name; do [ -n "$name" ] && [ "$name" = "$I18N_LABEL" ] && has_label=1 && break; done <<< "$pr_labels"
if [ "$check_failed" -eq 1 ]; then
[ "$has_label" -eq 0 ] && { retry gh pr edit "$NUMBER" -R "$GH_REPO" --add-label "$I18N_LABEL" || true; retry gh pr comment "$NUMBER" -R "$GH_REPO" -b "$BODY" || true; }
else
retry gh pr edit "$NUMBER" -R "$GH_REPO" --remove-label "i18n-out-of-sync" || true
[ "$has_label" -eq 1 ] && retry gh pr edit "$NUMBER" -R "$GH_REPO" --remove-label "$I18N_LABEL" || true
fi
exit $check_failed
test:
name: Lint & Test Build
if: github.event_name == 'pull_request'

83
.github/workflows/detect-duplicate.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Duplicate Issue Detector
on:
issues:
types: [opened]
permissions: {}
env:
EMBEDDING_MODEL: ${{ vars.EMBEDDING_MODEL }}
GROQ_MODEL: ${{ vars.GROQ_MODEL }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
jobs:
detect-duplicate:
runs-on: ubuntu-24.04
if: ${{ !github.event.issue.pull_request }}
permissions:
issues: write
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- 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'
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
working-directory: bin/duplicate-detector
env:
CI: true
run: pnpm install --frozen-lockfile
- name: Download issue index
uses: dawidd6/action-download-artifact@5c98f0b039f36ef966fdb7dfa9779262785ecb05 # v14
with:
name: issue-index
workflow: rebuild-issue-index.yml
path: bin/duplicate-detector
search_artifacts: true
if_no_artifact_found: warn
- name: Build index if missing
working-directory: bin/duplicate-detector
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
INDEX_PATH: issue_index.json
run: |
if [ ! -f issue_index.json ]; then
echo "No index found — building from scratch..."
node build-index.mjs
fi
- name: Detect duplicates
working-directory: bin/duplicate-detector
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
INDEX_PATH: issue_index.json
run: node detect.mjs

View File

@@ -0,0 +1,65 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Rebuild Issue Index
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
permissions: {}
env:
EMBEDDING_MODEL: ${{ vars.EMBEDDING_MODEL }}
jobs:
build-index:
runs-on: ubuntu-24.04
permissions:
issues: read
actions: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- 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'
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
working-directory: bin/duplicate-detector
env:
CI: true
run: pnpm install --frozen-lockfile
- name: Build issue index
working-directory: bin/duplicate-detector
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
INDEX_PATH: issue_index.json
run: node build-index.mjs
- name: Upload index artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: issue-index
path: bin/duplicate-detector/issue_index.json
retention-days: 7

1
bin/duplicate-detector/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env node
/**
* Build Issue Embedding Index
*
* Fetches all open issues and recently closed ones,
* generates embeddings using a local ONNX transformer model,
* and saves them as a JSON artifact for the duplicate detector.
*/
import { pipeline } from '@huggingface/transformers';
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { fetchIssues, issueText } from './utils.mjs';
const MODEL_NAME = process.env.EMBEDDING_MODEL || 'Xenova/all-MiniLM-L6-v2';
const OUTPUT_PATH = 'issue_index.json';
const INCLUDE_CLOSED_DAYS = 90;
const MAX_ISSUES = 5000;
const BATCH_SIZE = 64;
async function main() {
console.log('Fetching open issues...');
const openIssues = await fetchIssues({
state: 'open',
maxIssues: MAX_ISSUES,
});
console.log(`Fetched ${openIssues.length} open issues`);
const since = new Date(
Date.now() - INCLUDE_CLOSED_DAYS * 24 * 60 * 60 * 1000
).toISOString();
console.log(
`Fetching closed issues from last ${INCLUDE_CLOSED_DAYS} days...`
);
const closedIssues = await fetchIssues({
state: 'closed',
since,
maxIssues: MAX_ISSUES,
});
console.log(`Fetched ${closedIssues.length} closed issues`);
let allIssues = [...openIssues, ...closedIssues];
const seen = new Set();
allIssues = allIssues.filter((issue) => {
if (seen.has(issue.number)) return false;
seen.add(issue.number);
return true;
});
console.log(`Total unique issues to index: ${allIssues.length}`);
if (allIssues.length === 0) {
console.warn('No issues found - writing empty index');
writeFileSync(OUTPUT_PATH, JSON.stringify({ issues: [], embeddings: [] }));
return;
}
console.log(`Loading model: ${MODEL_NAME}`);
const extractor = await pipeline('feature-extraction', MODEL_NAME, {
dtype: 'fp32',
});
const texts = allIssues.map((issue) => issueText(issue.title, issue.body));
const allEmbeddings = [];
console.log(`Generating embeddings for ${texts.length} issues...`);
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
const batch = texts.slice(i, i + BATCH_SIZE);
const output = await extractor(batch, {
pooling: 'mean',
normalize: true,
});
const vectors = output.tolist();
allEmbeddings.push(...vectors);
const progress = Math.min(i + BATCH_SIZE, texts.length);
console.log(` ${progress}/${texts.length}`);
}
const issueMetadata = allIssues.map((issue) => {
const body = (issue.body || '').trim();
return {
number: issue.number,
title: issue.title,
state: issue.state,
url: issue.html_url,
body_preview: body.slice(0, 500) || '',
labels: (issue.labels || []).map((l) => l.name),
created_at: issue.created_at,
updated_at: issue.updated_at,
};
});
const indexData = {
issues: issueMetadata,
embeddings: allEmbeddings,
model: MODEL_NAME,
issue_count: issueMetadata.length,
built_at: new Date().toISOString(),
};
const dir = dirname(OUTPUT_PATH);
if (dir && dir !== '.') mkdirSync(dir, { recursive: true });
writeFileSync(OUTPUT_PATH, JSON.stringify(indexData));
const sizeMb = (
Buffer.byteLength(JSON.stringify(indexData)) /
(1024 * 1024)
).toFixed(1);
console.log(
`Index saved to ${OUTPUT_PATH} (${sizeMb} MB, ${issueMetadata.length} issues)`
);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,274 @@
#!/usr/bin/env node
/**
* Duplicate Issue Detector
*
* Triggered on new issue creation. Compares the new issue against an
* existing embedding index, then uses an LLM to
* confirm duplicates before posting a comment for maintainer review.
*/
import { pipeline } from '@huggingface/transformers';
import { existsSync, readFileSync } from 'node:fs';
import {
addLabel,
dotProduct,
fetchIssues,
getIssue,
issueText,
postComment,
} from './utils.mjs';
const SIMILARITY_THRESHOLD = 0.55;
const TOP_K = 5;
const MAX_COMMENT_CANDIDATES = 3;
const MODEL_NAME = process.env.EMBEDDING_MODEL || 'Xenova/all-MiniLM-L6-v2';
const GROQ_MODEL = process.env.GROQ_MODEL || 'llama-3.3-70b-versatile';
const INDEX_PATH = 'issue_index.json';
const LABEL_NAME = 'possible-duplicate';
const GROQ_API_KEY = process.env.GROQ_API_KEY || '';
const ISSUE_NUMBER = parseInt(process.env.ISSUE_NUMBER, 10);
function loadIndex(path) {
if (!existsSync(path)) {
console.error(
`Index file not found at ${path}. Run build-index.mjs first.`
);
process.exit(1);
}
const data = JSON.parse(readFileSync(path, 'utf-8'));
console.log(`Loaded index with ${data.issues.length} issues`);
return data;
}
function findSimilar(
queryEmbedding,
index,
{ topK = TOP_K, threshold = SIMILARITY_THRESHOLD, excludeNumber } = {}
) {
const { issues, embeddings } = index;
if (!issues.length) return [];
const scored = issues.map((issue, i) => ({
...issue,
score: dotProduct(queryEmbedding, embeddings[i]),
}));
return scored
.sort((a, b) => b.score - a.score)
.filter(
(c) =>
c.score >= threshold && (!excludeNumber || c.number !== excludeNumber)
)
.slice(0, topK);
}
const CONFIRM_SYSTEM_PROMPT = `You are a GitHub issue triage assistant. You will be given a NEW issue and one \
or more CANDIDATE issues that may be duplicates.
For each candidate, determine if the new issue is truly a duplicate (same root \
problem/request) or merely related (similar area but different issue).
Respond ONLY with a JSON array of objects, each with:
- "number": the candidate issue number
- "duplicate": true or false
- "reason": one-sentence explanation
Example:
[{"number": 123, "duplicate": true, "reason": "Both report the same crash when ..."}]`;
async function confirmWithLlm(newIssue, candidates) {
if (!GROQ_API_KEY) {
console.warn('GROQ_API_KEY not set — skipping LLM confirmation');
return candidates;
}
const candidateText = candidates
.map(
(c) =>
`### Candidate #${c.number} (similarity: ${c.score.toFixed(2)})\n` +
`**Title:** ${c.title}\n` +
`**State:** ${c.state}\n` +
`**Body preview:** ${(c.body_preview || 'N/A').slice(0, 500)}`
)
.join('\n\n');
const userPrompt =
`## NEW ISSUE #${newIssue.number}\n` +
`**Title:** ${newIssue.title}\n` +
`**Body:**\n${(newIssue.body || 'No body').slice(0, 1500)}\n\n` +
`---\n\n` +
`## CANDIDATES\n${candidateText}`;
try {
const resp = await fetch(
'https://api.groq.com/openai/v1/chat/completions',
{
method: 'POST',
headers: {
Authorization: `Bearer ${GROQ_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: GROQ_MODEL,
messages: [
{ role: 'system', content: CONFIRM_SYSTEM_PROMPT },
{ role: 'user', content: userPrompt },
],
temperature: 0.1,
max_tokens: 1024,
}),
signal: AbortSignal.timeout(30_000),
}
);
if (!resp.ok) {
const text = await resp.text();
throw new Error(`Groq API error ${resp.status}: ${text}`);
}
let content = (await resp.json()).choices[0].message.content.trim();
if (content.startsWith('```')) {
content = content
.split('\n')
.slice(1)
.join('\n')
.replace(/```\s*$/, '')
.trim();
}
const verdicts = JSON.parse(content);
if (!Array.isArray(verdicts)) {
throw new Error('Invalid LLM response format - expected array');
}
const verdictMap = new Map(verdicts.map((v) => [v.number, v]));
const confirmed = [];
for (const c of candidates) {
const verdict = verdictMap.get(c.number);
if (verdict?.duplicate) {
c.llm_reason = verdict.reason || '';
confirmed.push(c);
} else {
const reason = verdict?.reason || 'not evaluated';
console.log(` #${c.number} ruled out by LLM: ${reason}`);
}
}
return confirmed;
} catch (err) {
console.warn(
`LLM confirmation failed: ${err.message} - falling back to all candidates`
);
return candidates;
}
}
function formatComment(candidates) {
const lines = [
'**Possible duplicate detected**',
'',
'This issue may be a duplicate of the following (detected via semantic similarity + LLM review):',
'',
];
for (const c of candidates.slice(0, MAX_COMMENT_CANDIDATES)) {
const confidence = `${(c.score * 100).toFixed(0)}%`;
let line = `- #${c.number} (${confidence} match) — ${c.title}`;
if (c.llm_reason) {
line += `\n > *${c.llm_reason}*`;
}
lines.push(line);
}
lines.push(
'',
'A maintainer will review this. If this is **not** a duplicate, no action is needed.',
'',
`<!-- duplicate-bot: candidates=${candidates.map((c) => c.number).join(',')} -->`
);
return lines.join('\n');
}
async function main() {
if (!ISSUE_NUMBER) {
console.error('ISSUE_NUMBER not set');
process.exit(1);
}
console.log(`Processing issue #${ISSUE_NUMBER}`);
const issue = await getIssue(ISSUE_NUMBER);
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const recentIssues = await fetchIssues({
creator: issue.user.login,
since: oneHourAgo,
state: 'all',
});
if (recentIssues.length > 10) {
console.log(
`User ${issue.user.login} created ${recentIssues.length} issues in the last hour - skipping to prevent spam`
);
return;
}
if (issue.pull_request) {
console.log('Skipping - this is a pull request');
return;
}
if (issue.user.type === 'Bot') {
console.log('Skipping - issue created by bot');
return;
}
console.log(`Loading model: ${MODEL_NAME}`);
const extractor = await pipeline('feature-extraction', MODEL_NAME, {
dtype: 'fp32',
});
const index = loadIndex(INDEX_PATH);
const text = issueText(issue.title, issue.body);
const output = await extractor(text, { pooling: 'mean', normalize: true });
const queryEmbedding = output.tolist()[0];
let candidates = findSimilar(queryEmbedding, index, {
topK: TOP_K,
threshold: SIMILARITY_THRESHOLD,
excludeNumber: issue.number,
});
if (!candidates.length) {
console.log('No similar issues found above threshold - done');
return;
}
console.log(`Found ${candidates.length} candidates above threshold:`);
for (const c of candidates) {
console.log(` #${c.number} (${c.score.toFixed(3)}) - ${c.title}`);
}
console.log('Running LLM confirmation via Groq...');
candidates = await confirmWithLlm(issue, candidates);
if (!candidates.length) {
console.log('LLM ruled out all candidates - done');
return;
}
const comment = formatComment(candidates);
await postComment(ISSUE_NUMBER, comment);
await addLabel(ISSUE_NUMBER, LABEL_NAME);
console.log('Done!');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,17 @@
{
"name": "duplicate-detector",
"version": "1.0.0",
"private": true,
"type": "module",
"packageManager": "pnpm@10.17.1",
"scripts": {
"build-index": "node build-index.mjs",
"detect": "node detect.mjs"
},
"dependencies": {
"@huggingface/transformers": "^3.8.1"
},
"engines": {
"node": ">=22.0"
}
}

655
bin/duplicate-detector/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,655 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@huggingface/transformers':
specifier: ^3.8.1
version: 3.8.1
packages:
'@emnapi/runtime@1.8.1':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@huggingface/jinja@0.5.5':
resolution: {integrity: sha512-xRlzazC+QZwr6z4ixEqYHo9fgwhTZ3xNSdljlKfUFGZSdlvt166DljRELFUfFytlYOYvo3vTisA/AFOuOAzFQQ==}
engines: {node: '>=18'}
'@huggingface/transformers@3.8.1':
resolution: {integrity: sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==}
'@img/colour@1.0.0':
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
'@protobufjs/base64@1.1.2':
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
'@protobufjs/codegen@2.0.4':
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
'@protobufjs/eventemitter@1.1.0':
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
'@protobufjs/fetch@1.1.0':
resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
'@protobufjs/float@1.0.2':
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
'@protobufjs/inquire@1.1.0':
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
'@protobufjs/path@1.1.2':
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
'@protobufjs/pool@1.1.0':
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
'@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@types/node@25.2.2':
resolution: {integrity: sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==}
boolean@3.2.0:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
define-properties@1.2.1:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es6-error@4.1.1:
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
flatbuffers@25.9.23:
resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==}
global-agent@3.0.0:
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
engines: {node: '>=10.0'}
globalthis@1.0.4:
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
engines: {node: '>= 0.4'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
guid-typescript@1.0.9:
resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==}
has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
matcher@3.0.0:
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
engines: {node: '>=10'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
minizlib@3.1.0:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
onnxruntime-common@1.21.0:
resolution: {integrity: sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==}
onnxruntime-common@1.22.0-dev.20250409-89f8206ba4:
resolution: {integrity: sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==}
onnxruntime-node@1.21.0:
resolution: {integrity: sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==}
os: [win32, darwin, linux]
onnxruntime-web@1.22.0-dev.20250409-89f8206ba4:
resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==}
platform@1.3.6:
resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
protobufjs@7.5.4:
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
engines: {node: '>=12.0.0'}
roarr@2.15.4:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'}
semver-compare@1.0.0:
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
serialize-error@7.0.1:
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
engines: {node: '>=10'}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
tar@7.5.7:
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
engines: {node: '>=18'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
type-fest@0.13.1:
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
engines: {node: '>=10'}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
snapshots:
'@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
optional: true
'@huggingface/jinja@0.5.5': {}
'@huggingface/transformers@3.8.1':
dependencies:
'@huggingface/jinja': 0.5.5
onnxruntime-node: 1.21.0
onnxruntime-web: 1.22.0-dev.20250409-89f8206ba4
sharp: 0.34.5
'@img/colour@1.0.0': {}
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.8.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
'@protobufjs/aspromise@1.1.2': {}
'@protobufjs/base64@1.1.2': {}
'@protobufjs/codegen@2.0.4': {}
'@protobufjs/eventemitter@1.1.0': {}
'@protobufjs/fetch@1.1.0':
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/inquire': 1.1.0
'@protobufjs/float@1.0.2': {}
'@protobufjs/inquire@1.1.0': {}
'@protobufjs/path@1.1.2': {}
'@protobufjs/pool@1.1.0': {}
'@protobufjs/utf8@1.1.0': {}
'@types/node@25.2.2':
dependencies:
undici-types: 7.16.0
boolean@3.2.0: {}
chownr@3.0.0: {}
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
es-errors: 1.3.0
gopd: 1.2.0
define-properties@1.2.1:
dependencies:
define-data-property: 1.1.4
has-property-descriptors: 1.0.2
object-keys: 1.1.1
detect-libc@2.1.2: {}
detect-node@2.1.0: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es6-error@4.1.1: {}
escape-string-regexp@4.0.0: {}
flatbuffers@25.9.23: {}
global-agent@3.0.0:
dependencies:
boolean: 3.2.0
es6-error: 4.1.1
matcher: 3.0.0
roarr: 2.15.4
semver: 7.7.4
serialize-error: 7.0.1
globalthis@1.0.4:
dependencies:
define-properties: 1.2.1
gopd: 1.2.0
gopd@1.2.0: {}
guid-typescript@1.0.9: {}
has-property-descriptors@1.0.2:
dependencies:
es-define-property: 1.0.1
json-stringify-safe@5.0.1: {}
long@5.3.2: {}
matcher@3.0.0:
dependencies:
escape-string-regexp: 4.0.0
minipass@7.1.2: {}
minizlib@3.1.0:
dependencies:
minipass: 7.1.2
object-keys@1.1.1: {}
onnxruntime-common@1.21.0: {}
onnxruntime-common@1.22.0-dev.20250409-89f8206ba4: {}
onnxruntime-node@1.21.0:
dependencies:
global-agent: 3.0.0
onnxruntime-common: 1.21.0
tar: 7.5.7
onnxruntime-web@1.22.0-dev.20250409-89f8206ba4:
dependencies:
flatbuffers: 25.9.23
guid-typescript: 1.0.9
long: 5.3.2
onnxruntime-common: 1.22.0-dev.20250409-89f8206ba4
platform: 1.3.6
protobufjs: 7.5.4
platform@1.3.6: {}
protobufjs@7.5.4:
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2
'@protobufjs/codegen': 2.0.4
'@protobufjs/eventemitter': 1.1.0
'@protobufjs/fetch': 1.1.0
'@protobufjs/float': 1.0.2
'@protobufjs/inquire': 1.1.0
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 25.2.2
long: 5.3.2
roarr@2.15.4:
dependencies:
boolean: 3.2.0
detect-node: 2.1.0
globalthis: 1.0.4
json-stringify-safe: 5.0.1
semver-compare: 1.0.0
sprintf-js: 1.1.3
semver-compare@1.0.0: {}
semver@7.7.4: {}
serialize-error@7.0.1:
dependencies:
type-fest: 0.13.1
sharp@0.34.5:
dependencies:
'@img/colour': 1.0.0
detect-libc: 2.1.2
semver: 7.7.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
sprintf-js@1.1.3: {}
tar@7.5.7:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
minipass: 7.1.2
minizlib: 3.1.0
yallist: 5.0.0
tslib@2.8.1:
optional: true
type-fest@0.13.1: {}
undici-types@7.16.0: {}
yallist@5.0.0: {}

View File

@@ -0,0 +1,116 @@
const GITHUB_API = 'https://api.github.com';
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY;
function ghHeaders() {
return {
Authorization: `token ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github+json',
};
}
export async function fetchIssues({
state = 'open',
since,
maxIssues = 5000,
} = {}) {
const issues = [];
let page = 1;
const perPage = 100;
while (issues.length < maxIssues) {
const params = new URLSearchParams({
state,
per_page: String(perPage),
page: String(page),
sort: 'updated',
direction: 'desc',
});
if (since) params.set('since', since);
const url = `${GITHUB_API}/repos/${GITHUB_REPOSITORY}/issues?${params}`;
const resp = await fetch(url, { headers: ghHeaders() });
if (!resp.ok) {
throw new Error(`GitHub API error: ${resp.status} ${resp.statusText}`);
}
const batch = await resp.json();
if (!batch.length) break;
for (const item of batch) {
if (!item.pull_request) {
issues.push(item);
}
}
page++;
if (batch.length < perPage) break;
}
return issues.slice(0, maxIssues);
}
export async function getIssue(issueNumber) {
const url = `${GITHUB_API}/repos/${GITHUB_REPOSITORY}/issues/${issueNumber}`;
const resp = await fetch(url, { headers: ghHeaders() });
if (!resp.ok) {
throw new Error(`GitHub API error: ${resp.status} ${resp.statusText}`);
}
return resp.json();
}
export async function postComment(issueNumber, body) {
const url = `${GITHUB_API}/repos/${GITHUB_REPOSITORY}/issues/${issueNumber}/comments`;
const resp = await fetch(url, {
method: 'POST',
headers: { ...ghHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ body }),
});
if (!resp.ok) {
throw new Error(
`Failed to post comment: ${resp.status} ${resp.statusText}`
);
}
console.log(`Posted comment on #${issueNumber}`);
}
export async function addLabel(issueNumber, label) {
const url = `${GITHUB_API}/repos/${GITHUB_REPOSITORY}/issues/${issueNumber}/labels`;
const resp = await fetch(url, {
method: 'POST',
headers: { ...ghHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ labels: [label] }),
});
if (resp.status === 404) {
console.warn(
`Label '${label}' does not exist - skipping. Create it manually.`
);
return;
}
if (!resp.ok) {
throw new Error(`Failed to add label: ${resp.status} ${resp.statusText}`);
}
console.log(`Added label '${label}' to #${issueNumber}`);
}
export function issueText(title, body) {
body = (body || '').trim();
if (body.length > 2000) body = body.slice(0, 2000) + '...';
return body ? `${title}\n\n${body}` : title;
}
export function dotProduct(a, b) {
let sum = 0;
for (let i = 0; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum;
}

View File

@@ -13,6 +13,12 @@ Refer to [Configuring Databases](/extending-seerr/database-config#postgresql-opt
:::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)
Our Docker images are available with the following tags:
- `latest`: Always points to the most recent stable release.
- Version tags (e.g., `v3.0.0`): For specific stable versions.
- `develop`: Rolling release/nightly builds for using the latest changes (use with caution).
:::
:::info
@@ -139,9 +145,6 @@ Then, restart all services defined in the Compose file:
```bash
docker compose up -d
```
:::tip
You may alternatively use a third-party mechanism like [dockge](https://github.com/louislam/dockge) to manage your docker compose files.
:::
</TabItem>
</Tabs>

View File

@@ -5,6 +5,10 @@ title: Migration guide
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
:::important
Read our [release announcement](/blog/seerr-release) to learn what Seerr means for Jellyseerr and Overseerr users.
:::
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.
@@ -26,6 +30,19 @@ Some methods are currently not maintained, but this does not mean they are perma
## Docker
Refer to [Seerr Docker Documentation](/getting-started/docker), all of our examples have been updated to reflect the below change.
:::info
Seerr provides a secure, fully featured image with everything you need included.
We sincerely appreciate the past contributions from third-party maintainers, which helped enhance this image and its capabilities.
To maintain consistency and security, we encourage everyone to use the features available in the official Seerr image.
If you feel something is missing, please submit a feature request—your feedback is always welcome!
Our Docker images are available with the following tags:
- `latest`: Always points to the most recent stable release.
- Version tags (e.g., `v3.0.0`): For specific stable versions.
- `develop`: Rolling release/nightly builds for using the latest changes (use with caution).
:::
Changes :
- Renamed all references from `overseerr` or `jellyseerr` to `seerr`.
- The container image reference has been updated.

View File

@@ -4,7 +4,7 @@ description: The official Seerr blog for release notes, technical updates, and c
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
image: https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/gen-docs/static/img/logo_full.svg
hide_table_of_contents: false
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -0,0 +1,127 @@
---
title: "Seerr Release: Unifying Overseerr and Jellyseerr"
description: "Overseerr and Jellyseerr are merging into a unified project: Seerr"
slug: seerr-release
authors: [seerr-team]
image: https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/gen-docs/static/img/logo_full.svg
hide_table_of_contents: false
---
We're excited to announce a major update: the Jellyseerr and Overseerr teams are officially merging into a single team called **Seerr**. This unification marks an important step forward as we bring our efforts together under one banner.
For users, this means one shared codebase combining all existing Overseerr functionalities with the latest Jellyseerr features, along with Jellyfin and Emby support, allowing us to deliver updates more efficiently and keep the project moving forward.
Please check how to migrate to Seerr in our [migration guide](https://docs.seerr.dev/migration-guide) and stay tuned for more updates on the project!
<!--truncate-->
## What's new in Seerr for Overseerr 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 in addition to the existing Plex integration.
* **PostgreSQL support**: In addition to SQLite, you can now opt in to using a PostgreSQL database.
* **Blacklist 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.
* **TVDB metadata**: Option to use TheTVDB metadata for series (as in Sonarr) instead of TMDB.
* **DNS caching**: Reduces lookup times and external requests, especially useful when using systems like Pi-Hole/Adguard Home.
* **Helm chart included**: Enables easier installation and maintenance in Kubernetes environments.
* **ntfy.sh notifications**: Support for sending notifications via ntfy.sh.
* **Disable special seasons:** Adds a setting to prevent special seasons from being shown or requested.
* **New languages**: Turkish and Basque
## What's new since the previous Jellyseerr release
This release also brings several important improvements and long-requested features, including **TheTVDB metadata support**, **DNS caching**, and **dynamic webhook placeholders**, along with a few quality-of-life improvements for developers and users alike.
### PNPM v10 Upgrade
We're updating Seerr to **PNPM v10** to keep up-to-date development tools. If you are building Seerr from source or if you contribute to Seerr, you'll need to **update your local PNPM installation** before working on the project.
This doesn't concern you if you're using Docker.
To update, run the following command:
`pnpm self-update`
After updating, verify your version with:
`pnpm -v`
You should see version **10.x.x**.
### TVDB Metadata Provider (Experimental)
We're excited to introduce support for **TheTVDB** as a new metadata provider!
Previously, Seerr relied solely on **TMDB** for movie and TV show information, which sometimes led to discrepancies in season and episode numbering when working with **Sonarr**, since Sonarr uses **TheTVDB** as its metadata source.
With this new integration, Seerr can now use **the same data source as Sonarr** for series and anime, ensuring consistent and accurate season and episode information across both platforms.
You can try this new experimental feature in the new “Metadata Providers” tab of the settings page:
![Metadata Providers](./metadata-providers.png)
### DNS Caching (Experimental)
By default, Node.js doesn't cache any DNS requests. Our DNS cache manager addresses the problems caused by extremely high DNS query rates, particularly for large Jellyfin libraries as each HTTP request was also resulting in another DNS request. Therefore, by caching these DNS lookups, **Seerr will now reduce stress on DNS servers** and avoid rate-limiting or blocks encountered with services like **Pi-Hole**/**Adguard Home**.
We will post another blog post soon on all the issues we encountered with DNS caching in Node.js.
You can enable this by checking the “DNS Cache” setting in the network tabs of the Seerr settings:
![DNS Cache](./dns-cache.png)
### AniDB for Jellyfin Libraries
This new version also brings additional metadata to Jellyfin-managed collections. When there's no provider ID from TMDB or TVDB, Seerr will automatically **fall back on AniDB**, expanding coverage for lesser-known or region-specific anime.
### Dynamic Placeholders in Webhook URLs
Webhook notifications are now more powerful and adaptable with **dynamic placeholder support in webhook URLs**. This allows Seerr to automatically replace placeholders in the webhook URL with real values at runtime.
For example, you can include the requester's username directly in your webhook URL to better integrate with third-party services or user-specific endpoints.
This feature can be enabled from the **Notifications** settings page, where available placeholders are listed for reference. It's currently marked as **experimental**, and we welcome community feedback to help refine and expand support for additional placeholders in future releases.
### Optional Images in Notifications
Another small feature: **images in notifications are now optional** (but still enabled by default). Previous versions always included images in notifications, which could lead to broken links or failed requests if images were missing or unavailable.
### Security improvement
Some outdated dependencies have been updated (some work is still in progress). Helm charts and containers are now cryptographically signed and can be verified and enforced client-side. Containers now run as rootless. Workflows have been completely reworked to minimize third-party actions. Permissions have been strengthened, and actions are now pinned to specific hashes for better traceability. The release process has been updated to remove many outdated and plugin dependencies, replacing them with more standard industry solutions.
:::important
## Note for PostgreSQL users (optional)
If you're 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`. This update of the mount point is required to ensure the container functions correctly after the upgrade.
:::
## Conclusion
Seerr is built and maintained by dedicated volunteer contributors, whose skills and commitment make it all possible. Many thanks to everyone who contributed to this version:
* [0xsysr3ll](https://github.com/0xSysR3ll)
* [ale183](https://github.com/ale183)
* [Brandon Cohen](https://github.com/OwsleyJr)
* [Disparate2761](https://github.com/Disparate2761)
* [fallenbagel](https://github.com/fallenbagel)
* [Gauthier](https://github.com/gauthier-th)
* [Gauvain](https://github.com/Gauvino)
* [Georgy](https://github.com/tarasverq)
* [Ishan Jain](https://github.com/ishanjain28)
* [James Kruger](https://github.com/theGunner295)
* [Joe Harrison](https://github.com/sudo-kraken)
* [J. Winters-Brown](https://github.com/ofgrenudo)
* [Ludovic Ortega](https://github.com/M0NsTeRRR)
* [RolliePollie18](https://github.com/RolliePollie18)
* [Ryan Cohen](https://github.com/sct)
* [salty](https://github.com/saltydk)
* [samohtxotom](https://github.com/samohtxotom)
* [Sergii Bogomolov](https://github.com/sbogomolov)
* [Someone](https://github.com/InterN0te)
* [TacoCake](https://github.com/TacoCake)
* [Terry Sposato](https://github.com/tsposato)
* [TheCatLady](https://github.com/TheCatLady)
* [Thibaut Noah](https://github.com/tirrorex)
* [THOMAS B](https://github.com/TOomaAh)
Keep an eye on our blog for in-depth looks at our work and upcoming releases!

View File

@@ -1,8 +1,8 @@
fallenbagel:
name: Fallenbagel
page: true
title: Developer & Maintainer of Jellyseerr
description: Core Maintainer & Developer of Jellyseerr | Full-Stack Software Engineer | MSc Software Engineering Candidate.
title: Developer & Maintainer of Seerr
description: Core Maintainer & Developer of Seerr | Full-Stack Software Engineer | MSc Software Engineering Student.
url: https://github.com/fallenbagel
image_url: https://github.com/fallenbagel.png
email: hello@fallenbagel.com
@@ -12,10 +12,18 @@ fallenbagel:
gauthier-th:
name: Gauthier
page: true
title: Co-Developer & Co-Maintainer of Jellyseerr
description: Co-Maintainer & Developer of Jellyseerr | PhD Student in AI at ICB, Dijon
title: Developer & Maintainer of Seerr
description: Core Maintainer & Developer of Seerr | PhD Student in AI at ICB, Dijon
url: https://gauthierth.fr
image_url: https://github.com/gauthier-th.png
email: mail@gauthierth.fr
socials:
github: gauthier-th
seerr-team:
name: Seerr Team
title: The team behind Seerr, formerly known as the Jellyseerr and Overseerr teams.
url: https://seerr.dev
image_url: https://github.com/seerr-team.png
socials:
github: seerr-team

View File

@@ -64,7 +64,7 @@ const config: Config = {
navbar: {
logo: {
alt: 'Seerr',
src: 'img/logo.svg',
src: 'img/logo_full.svg',
},
items: [
{
@@ -72,6 +72,11 @@ const config: Config = {
label: 'Blog',
position: 'right',
},
{
href: 'https://discord.gg/seerr',
label: 'Discord',
position: 'right',
},
{
href: 'https://github.com/seerr-team/seerr',
label: 'GitHub',

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -24,6 +24,6 @@ module.exports = {
},
experimental: {
scrollRestoration: true,
largePageDataBytes: 256000,
largePageDataBytes: 512 * 1000,
},
};

View File

@@ -56,7 +56,6 @@
"country-flag-icons": "1.6.4",
"cronstrue": "2.23.0",
"date-fns": "2.29.3",
"dayjs": "1.11.19",
"dns-caching": "^0.2.7",
"email-templates": "12.0.3",
"express": "4.21.2",

3
pnpm-lock.yaml generated
View File

@@ -84,9 +84,6 @@ importers:
date-fns:
specifier: 2.29.3
version: 2.29.3
dayjs:
specifier: 1.11.19
version: 1.11.19
dns-caching:
specifier: ^0.2.7
version: 0.2.7

View File

@@ -26,6 +26,7 @@ import { MediaRequest } from './MediaRequest';
import Season from './Season';
@Entity()
@Index(['tmdbId', 'mediaType'])
class Media {
public static async getRelatedMedia(
user: User | undefined,
@@ -101,9 +102,11 @@ class Media {
public imdbId?: string;
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
@Index()
public status: MediaStatus;
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
@Index()
public status4k: MediaStatus;
@OneToMany(() => MediaRequest, (request) => request.media, {

View File

@@ -21,6 +21,7 @@ import {
AfterUpdate,
Column,
Entity,
Index,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
@@ -513,6 +514,7 @@ export class MediaRequest {
public id: number;
@Column({ type: 'integer' })
@Index()
public status: MediaRequestStatus;
@ManyToOne(() => Media, (media) => media.requests, {

View File

@@ -165,12 +165,15 @@ app
try {
const descriptor = Object.getOwnPropertyDescriptor(req, 'ip');
if (descriptor?.writable === true) {
(req as any).ip = getClientIp(req) ?? '';
Object.defineProperty(req, 'ip', {
...descriptor,
value: getClientIp(req) ?? '',
});
}
} catch (e) {
logger.error('Failed to attach the ip to the request', {
label: 'Middleware',
message: e.message,
message: (e as Error).message,
});
} finally {
next();

View File

@@ -260,13 +260,16 @@ class WebPushAgent
shouldSendAdminNotification(type, user, payload)
);
const allSubs = await userPushSubRepository
.createQueryBuilder('pushSub')
.leftJoinAndSelect('pushSub.user', 'user')
.where('pushSub.userId IN (:...users)', {
users: manageUsers.map((user) => user.id),
})
.getMany();
const allSubs =
manageUsers.length > 0
? await userPushSubRepository
.createQueryBuilder('pushSub')
.leftJoinAndSelect('pushSub.user', 'user')
.where('pushSub.userId IN (:...users)', {
users: manageUsers.map((user) => user.id),
})
.getMany()
: [];
// We only want to send the custom notification when type is approved or declined
// Otherwise, default to the normal notification

View File

@@ -385,26 +385,6 @@ class BaseScanner<T> {
}
}
// We want to skip specials when checking if a show is available
const isAllStandardSeasons =
seasons.length &&
seasons
.filter((season) => season.seasonNumber !== 0)
.every(
(season) =>
season.episodes === season.totalEpisodes && season.episodes > 0
);
const isAll4kSeasons =
seasons.length &&
seasons
.filter((season) => season.seasonNumber !== 0)
.every(
(season) =>
season.episodes4k === season.totalEpisodes &&
season.episodes4k > 0
);
if (media) {
media.seasons = [...media.seasons, ...newSeasons];
@@ -464,43 +444,38 @@ class BaseScanner<T> {
externalServiceSlug;
}
// If the show is already available, and there are no new seasons, dont adjust
// the status. Skip specials when performing availability check
const shouldStayAvailable =
media.status === MediaStatus.AVAILABLE &&
newSeasons.filter(
(season) =>
season.status !== MediaStatus.UNKNOWN &&
season.status !== MediaStatus.DELETED &&
season.seasonNumber !== 0
).length === 0;
const shouldStayAvailable4k =
media.status4k === MediaStatus.AVAILABLE &&
newSeasons.filter(
(season) =>
season.status4k !== MediaStatus.UNKNOWN &&
season.status4k !== MediaStatus.DELETED &&
season.seasonNumber !== 0
).length === 0;
media.status =
isAllStandardSeasons || shouldStayAvailable
? MediaStatus.AVAILABLE
: media.seasons.some(
(season) =>
season.status === MediaStatus.PARTIALLY_AVAILABLE ||
season.status === MediaStatus.AVAILABLE
const nonSpecialSeasons = media.seasons.filter(
(s) => s.seasonNumber !== 0
);
// Check the actual season objects instead scanner input
// to determine overall availability status
const isAllStandardSeasonsAvailable =
nonSpecialSeasons.length > 0 &&
nonSpecialSeasons.every((s) => s.status === MediaStatus.AVAILABLE);
const isAll4kSeasonsAvailable =
nonSpecialSeasons.length > 0 &&
nonSpecialSeasons.every((s) => s.status4k === MediaStatus.AVAILABLE);
media.status = isAllStandardSeasonsAvailable
? MediaStatus.AVAILABLE
: media.seasons.some(
(season) =>
season.status === MediaStatus.PARTIALLY_AVAILABLE ||
season.status === MediaStatus.AVAILABLE
)
? MediaStatus.PARTIALLY_AVAILABLE
: (!seasons.length && media.status !== MediaStatus.DELETED) ||
media.seasons.some(
(season) => season.status === MediaStatus.PROCESSING
)
? MediaStatus.PARTIALLY_AVAILABLE
: (!seasons.length && media.status !== MediaStatus.DELETED) ||
media.seasons.some(
(season) => season.status === MediaStatus.PROCESSING
)
? MediaStatus.PROCESSING
: media.status === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.UNKNOWN;
? MediaStatus.PROCESSING
: media.status === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.UNKNOWN;
media.status4k =
(isAll4kSeasons || shouldStayAvailable4k) && this.enable4kShow
isAll4kSeasonsAvailable && this.enable4kShow
? MediaStatus.AVAILABLE
: this.enable4kShow &&
media.seasons.some(
@@ -520,6 +495,22 @@ class BaseScanner<T> {
await mediaRepository.save(media);
this.log(`Updating existing title: ${title}`);
} else {
// For new media, check actual newSeasons objects instead of scanner
// input to determine overall availability status
const nonSpecialNewSeasons = newSeasons.filter(
(s) => s.seasonNumber !== 0
);
const isAllStandardSeasonsAvailable =
nonSpecialNewSeasons.length > 0 &&
nonSpecialNewSeasons.every((s) => s.status === MediaStatus.AVAILABLE);
const isAll4kSeasonsAvailable =
nonSpecialNewSeasons.length > 0 &&
nonSpecialNewSeasons.every(
(s) => s.status4k === MediaStatus.AVAILABLE
);
const newMedia = new Media({
mediaType: MediaType.TV,
seasons: newSeasons,
@@ -564,7 +555,7 @@ class BaseScanner<T> {
)
? jellyfinMediaId
: undefined,
status: isAllStandardSeasons
status: isAllStandardSeasonsAvailable
? MediaStatus.AVAILABLE
: newSeasons.some(
(season) =>
@@ -578,7 +569,7 @@ class BaseScanner<T> {
? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN,
status4k:
isAll4kSeasons && this.enable4kShow
isAll4kSeasonsAvailable && this.enable4kShow
? MediaStatus.AVAILABLE
: this.enable4kShow &&
newSeasons.some(

View File

@@ -3,6 +3,7 @@ import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import {
BlacklistedMediaError,
DuplicateMediaRequestError,
MediaRequest,
NoSeasonsAvailableError,
@@ -144,6 +145,9 @@ class WatchlistSync {
errorMessage: e.message,
});
break;
// Blacklisted media should be silently ignored during watchlist sync to avoid spam
case BlacklistedMediaError:
break;
default:
logger.error('Failed to create media request from watchlist', {
label: 'Watchlist Sync',

View File

@@ -0,0 +1,35 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddPerformanceIndexes1770627987304 implements MigrationInterface {
name = 'AddPerformanceIndexes1770627987304';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE INDEX "IDX_4c696e8ed36ae34fe18abe59d2" ON "media_request" ("status") `
);
await queryRunner.query(
`CREATE INDEX "IDX_c730c2d67f271a372c39a07b7e" ON "media" ("status") `
);
await queryRunner.query(
`CREATE INDEX "IDX_5d6218de4f547909391a5c1347" ON "media" ("status4k") `
);
await queryRunner.query(
`CREATE INDEX "IDX_f8233358694d1677a67899b90a" ON "media" ("tmdbId", "mediaType") `
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_f8233358694d1677a67899b90a"`
);
await queryRunner.query(
`DROP INDEX "public"."IDX_5d6218de4f547909391a5c1347"`
);
await queryRunner.query(
`DROP INDEX "public"."IDX_c730c2d67f271a372c39a07b7e"`
);
await queryRunner.query(
`DROP INDEX "public"."IDX_4c696e8ed36ae34fe18abe59d2"`
);
}
}

View File

@@ -0,0 +1,369 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class AddPerformanceIndexes1770627968781 implements MigrationInterface {
name = 'AddPerformanceIndexes1770627968781';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX IF EXISTS "UQ_6427d07d9a171a3a1ab87480005"`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_939f205946256cc0d2a1ac51a8"`
);
await queryRunner.query(
`CREATE TABLE "temporary_watchlist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ratingKey" varchar NOT NULL, "mediaType" varchar NOT NULL, "title" varchar NOT NULL, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_watchlist"("id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId") SELECT "id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId" FROM "watchlist"`
);
await queryRunner.query(`DROP TABLE "watchlist"`);
await queryRunner.query(
`ALTER TABLE "temporary_watchlist" RENAME TO "watchlist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") `
);
await queryRunner.query(
`CREATE TABLE "temporary_issue_comment" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "message" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "issueId" integer, CONSTRAINT "FK_180710fead1c94ca499c57a7d42" FOREIGN KEY ("issueId") REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_707b033c2d0653f75213614789d" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_issue_comment"("id", "message", "createdAt", "updatedAt", "userId", "issueId") SELECT "id", "message", "createdAt", "updatedAt", "userId", "issueId" FROM "issue_comment"`
);
await queryRunner.query(`DROP TABLE "issue_comment"`);
await queryRunner.query(
`ALTER TABLE "temporary_issue_comment" RENAME TO "issue_comment"`
);
await queryRunner.query(
`CREATE TABLE "temporary_issue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "issueType" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "problemSeason" integer NOT NULL DEFAULT (0), "problemEpisode" integer NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaId" integer, "createdById" integer, "modifiedById" integer, CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_10b17b49d1ee77e7184216001e0" FOREIGN KEY ("createdById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_276e20d053f3cff1645803c95d8" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_issue"("id", "issueType", "status", "problemSeason", "problemEpisode", "createdAt", "updatedAt", "mediaId", "createdById", "modifiedById") SELECT "id", "issueType", "status", "problemSeason", "problemEpisode", "createdAt", "updatedAt", "mediaId", "createdById", "modifiedById" FROM "issue"`
);
await queryRunner.query(`DROP TABLE "issue"`);
await queryRunner.query(`ALTER TABLE "temporary_issue" RENAME TO "issue"`);
await queryRunner.query(
`CREATE TABLE "temporary_override_rule" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" varchar, "genre" varchar, "language" varchar, "keywords" varchar, "profileId" integer, "rootFolder" varchar, "tags" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP))`
);
await queryRunner.query(
`INSERT INTO "temporary_override_rule"("id", "radarrServiceId", "sonarrServiceId", "users", "genre", "language", "keywords", "profileId", "rootFolder", "tags", "createdAt", "updatedAt") SELECT "id", "radarrServiceId", "sonarrServiceId", "users", "genre", "language", "keywords", "profileId", "rootFolder", "tags", "createdAt", "updatedAt" FROM "override_rule"`
);
await queryRunner.query(`DROP TABLE "override_rule"`);
await queryRunner.query(
`ALTER TABLE "temporary_override_rule" RENAME TO "override_rule"`
);
await queryRunner.query(
`CREATE TABLE "temporary_season_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "requestId" integer, CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a" FOREIGN KEY ("requestId") REFERENCES "media_request" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_season_request"("id", "seasonNumber", "status", "createdAt", "updatedAt", "requestId") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "requestId" FROM "season_request"`
);
await queryRunner.query(`DROP TABLE "season_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_season_request" RENAME TO "season_request"`
);
await queryRunner.query(
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "media_request"`
);
await queryRunner.query(`DROP TABLE "media_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion" FROM "user"`
);
await queryRunner.query(`DROP TABLE "user"`);
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_6bbafa28411e6046421991ea21"`
);
await queryRunner.query(
`CREATE TABLE "temporary_blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "userId" integer, "mediaId" integer, "blacklistedTags" varchar, CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"), CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId", "blacklistedTags") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId", "blacklistedTags" FROM "blacklist"`
);
await queryRunner.query(`DROP TABLE "blacklist"`);
await queryRunner.query(
`ALTER TABLE "temporary_blacklist" RENAME TO "blacklist"`
);
await queryRunner.query(
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
);
await queryRunner.query(
`CREATE TABLE "temporary_season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaId" integer, "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_season"("id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId", "status4k") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId", "status4k" FROM "season"`
);
await queryRunner.query(`DROP TABLE "season"`);
await queryRunner.query(
`ALTER TABLE "temporary_season" RENAME TO "season"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_7157aad07c73f6a6ae3bbd5ef5"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_41a289eb1fa489c1bc6f38d9c3"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_7ff2d11f6a83cb52386eaebe74"`
);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "status4k" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaAddedAt" datetime DEFAULT (CURRENT_TIMESTAMP), "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaId" varchar, "jellyfinMediaId4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE TABLE "temporary_discover_slider" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" integer NOT NULL, "order" integer NOT NULL, "isBuiltIn" boolean NOT NULL DEFAULT (0), "enabled" boolean NOT NULL DEFAULT (1), "title" varchar, "data" varchar, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP))`
);
await queryRunner.query(
`INSERT INTO "temporary_discover_slider"("id", "type", "order", "isBuiltIn", "enabled", "title", "data", "createdAt", "updatedAt") SELECT "id", "type", "order", "isBuiltIn", "enabled", "title", "data", "createdAt", "updatedAt" FROM "discover_slider"`
);
await queryRunner.query(`DROP TABLE "discover_slider"`);
await queryRunner.query(
`ALTER TABLE "temporary_discover_slider" RENAME TO "discover_slider"`
);
await queryRunner.query(
`CREATE INDEX "IDX_4c696e8ed36ae34fe18abe59d2" ON "media_request" ("status") `
);
await queryRunner.query(
`CREATE INDEX "IDX_c730c2d67f271a372c39a07b7e" ON "media" ("status") `
);
await queryRunner.query(
`CREATE INDEX "IDX_5d6218de4f547909391a5c1347" ON "media" ("status4k") `
);
await queryRunner.query(
`CREATE INDEX "IDX_f8233358694d1677a67899b90a" ON "media" ("tmdbId", "mediaType") `
);
await queryRunner.query(
`CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_f8233358694d1677a67899b90a"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_5d6218de4f547909391a5c1347"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_c730c2d67f271a372c39a07b7e"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_4c696e8ed36ae34fe18abe59d2"`
);
await queryRunner.query(
`ALTER TABLE "discover_slider" RENAME TO "temporary_discover_slider"`
);
await queryRunner.query(
`CREATE TABLE "discover_slider" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "type" integer NOT NULL, "order" integer NOT NULL, "isBuiltIn" boolean NOT NULL DEFAULT (0), "enabled" boolean NOT NULL DEFAULT (1), "title" varchar, "data" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`
);
await queryRunner.query(
`INSERT INTO "discover_slider"("id", "type", "order", "isBuiltIn", "enabled", "title", "data", "createdAt", "updatedAt") SELECT "id", "type", "order", "isBuiltIn", "enabled", "title", "data", "createdAt", "updatedAt" FROM "temporary_discover_slider"`
);
await queryRunner.query(`DROP TABLE "temporary_discover_slider"`);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_7ff2d11f6a83cb52386eaebe74"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_41a289eb1fa489c1bc6f38d9c3"`
);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_7157aad07c73f6a6ae3bbd5ef5"`
);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "status4k" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "mediaAddedAt" datetime DEFAULT (CURRENT_TIMESTAMP), "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, "jellyfinMediaId" varchar, "jellyfinMediaId4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "status4k", "createdAt", "updatedAt", "lastSeasonChange", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k", "ratingKey", "ratingKey4k", "jellyfinMediaId", "jellyfinMediaId4k" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`ALTER TABLE "season" RENAME TO "temporary_season"`
);
await queryRunner.query(
`CREATE TABLE "season" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer, "status4k" integer NOT NULL DEFAULT (1), CONSTRAINT "FK_087099b39600be695591da9a49c" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "season"("id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId", "status4k") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "mediaId", "status4k" FROM "temporary_season"`
);
await queryRunner.query(`DROP TABLE "temporary_season"`);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_6bbafa28411e6046421991ea21"`
);
await queryRunner.query(
`ALTER TABLE "blacklist" RENAME TO "temporary_blacklist"`
);
await queryRunner.query(
`CREATE TABLE "blacklist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "title" varchar, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "mediaId" integer, "blacklistedTags" varchar, CONSTRAINT "UQ_5f933c8ed6ad2c31739e6b94886" UNIQUE ("tmdbId"), CONSTRAINT "UQ_e49b27917899e01d7aca6b0b15c" UNIQUE ("mediaId"), CONSTRAINT "FK_53c1ab62c3e5875bc3ac474823e" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_62b7ade94540f9f8d8bede54b99" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "blacklist"("id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId", "blacklistedTags") SELECT "id", "mediaType", "title", "tmdbId", "createdAt", "userId", "mediaId", "blacklistedTags" FROM "temporary_blacklist"`
);
await queryRunner.query(`DROP TABLE "temporary_blacklist"`);
await queryRunner.query(
`CREATE INDEX "IDX_6bbafa28411e6046421991ea21" ON "blacklist" ("tmdbId") `
);
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
await queryRunner.query(
`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))`
);
await queryRunner.query(
`INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion" FROM "temporary_user"`
);
await queryRunner.query(`DROP TABLE "temporary_user"`);
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
);
await queryRunner.query(
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
await queryRunner.query(
`ALTER TABLE "season_request" RENAME TO "temporary_season_request"`
);
await queryRunner.query(
`CREATE TABLE "season_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "seasonNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "requestId" integer, CONSTRAINT "FK_6f14737e346d6b27d8e50d2157a" FOREIGN KEY ("requestId") REFERENCES "media_request" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "season_request"("id", "seasonNumber", "status", "createdAt", "updatedAt", "requestId") SELECT "id", "seasonNumber", "status", "createdAt", "updatedAt", "requestId" FROM "temporary_season_request"`
);
await queryRunner.query(`DROP TABLE "temporary_season_request"`);
await queryRunner.query(
`ALTER TABLE "override_rule" RENAME TO "temporary_override_rule"`
);
await queryRunner.query(
`CREATE TABLE "override_rule" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "radarrServiceId" integer, "sonarrServiceId" integer, "users" varchar, "genre" varchar, "language" varchar, "keywords" varchar, "profileId" integer, "rootFolder" varchar, "tags" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`
);
await queryRunner.query(
`INSERT INTO "override_rule"("id", "radarrServiceId", "sonarrServiceId", "users", "genre", "language", "keywords", "profileId", "rootFolder", "tags", "createdAt", "updatedAt") SELECT "id", "radarrServiceId", "sonarrServiceId", "users", "genre", "language", "keywords", "profileId", "rootFolder", "tags", "createdAt", "updatedAt" FROM "temporary_override_rule"`
);
await queryRunner.query(`DROP TABLE "temporary_override_rule"`);
await queryRunner.query(`ALTER TABLE "issue" RENAME TO "temporary_issue"`);
await queryRunner.query(
`CREATE TABLE "issue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "issueType" integer NOT NULL, "status" integer NOT NULL DEFAULT (1), "problemSeason" integer NOT NULL DEFAULT (0), "problemEpisode" integer NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "mediaId" integer, "createdById" integer, "modifiedById" integer, CONSTRAINT "FK_da88a1019c850d1a7b143ca02e5" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_10b17b49d1ee77e7184216001e0" FOREIGN KEY ("createdById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_276e20d053f3cff1645803c95d8" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "issue"("id", "issueType", "status", "problemSeason", "problemEpisode", "createdAt", "updatedAt", "mediaId", "createdById", "modifiedById") SELECT "id", "issueType", "status", "problemSeason", "problemEpisode", "createdAt", "updatedAt", "mediaId", "createdById", "modifiedById" FROM "temporary_issue"`
);
await queryRunner.query(`DROP TABLE "temporary_issue"`);
await queryRunner.query(
`ALTER TABLE "issue_comment" RENAME TO "temporary_issue_comment"`
);
await queryRunner.query(
`CREATE TABLE "issue_comment" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "message" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer, "issueId" integer, CONSTRAINT "FK_180710fead1c94ca499c57a7d42" FOREIGN KEY ("issueId") REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_707b033c2d0653f75213614789d" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "issue_comment"("id", "message", "createdAt", "updatedAt", "userId", "issueId") SELECT "id", "message", "createdAt", "updatedAt", "userId", "issueId" FROM "temporary_issue_comment"`
);
await queryRunner.query(`DROP TABLE "temporary_issue_comment"`);
await queryRunner.query(
`DROP INDEX IF EXISTS "IDX_939f205946256cc0d2a1ac51a8"`
);
await queryRunner.query(
`ALTER TABLE "watchlist" RENAME TO "temporary_watchlist"`
);
await queryRunner.query(
`CREATE TABLE "watchlist" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "ratingKey" varchar NOT NULL, "mediaType" varchar NOT NULL, "title" varchar NOT NULL, "tmdbId" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "requestedById" integer, "mediaId" integer, CONSTRAINT "UNIQUE_USER_DB" UNIQUE ("tmdbId", "requestedById"), CONSTRAINT "FK_6641da8d831b93dfcb429f8b8bc" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_ae34e6b153a90672eb9dc4857d7" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "watchlist"("id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId") SELECT "id", "ratingKey", "mediaType", "title", "tmdbId", "createdAt", "updatedAt", "requestedById", "mediaId" FROM "temporary_watchlist"`
);
await queryRunner.query(`DROP TABLE "temporary_watchlist"`);
await queryRunner.query(
`CREATE INDEX "IDX_939f205946256cc0d2a1ac51a8" ON "watchlist" ("tmdbId") `
);
await queryRunner.query(
`ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"`
);
await queryRunner.query(
`CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (datetime('now')), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"`
);
await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`);
await queryRunner.query(
`CREATE UNIQUE INDEX "UQ_6427d07d9a171a3a1ab87480005" ON "user_push_subscription" ("endpoint", "userId") `
);
}
}

View File

@@ -15,6 +15,7 @@ import {
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest';
import Season from '@server/entity/Season';
import SeasonRequest from '@server/entity/SeasonRequest';
import notificationManager, { Notification } from '@server/lib/notifications';
import { getSettings } from '@server/lib/settings';
@@ -27,7 +28,7 @@ import type {
RemoveEvent,
UpdateEvent,
} from 'typeorm';
import { EventSubscriber } from 'typeorm';
import { EventSubscriber, Not } from 'typeorm';
const sanitizeDisplayName = (displayName: string): string => {
return displayName
@@ -397,10 +398,23 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
await mediaRepository.save(media);
})
.catch(async () => {
const requestRepository = getRepository(MediaRequest);
try {
const requestRepository = getRepository(MediaRequest);
entity.status = MediaRequestStatus.FAILED;
requestRepository.save(entity);
if (entity.status !== MediaRequestStatus.FAILED) {
entity.status = MediaRequestStatus.FAILED;
await requestRepository.save(entity);
}
} catch (saveError) {
logger.error('Failed to mark request as FAILED', {
label: 'Media Request',
requestId: entity.id,
errorMessage:
saveError instanceof Error
? saveError.message
: String(saveError),
});
}
logger.warn(
'Something went wrong sending movie request to Radarr, marking status as FAILED',
@@ -503,7 +517,6 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});
if (!media) {
@@ -690,7 +703,6 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
// We grab media again here to make sure we have the latest version of it
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});
if (!media) {
@@ -707,10 +719,23 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
await mediaRepository.save(media);
})
.catch(async () => {
const requestRepository = getRepository(MediaRequest);
try {
const requestRepository = getRepository(MediaRequest);
entity.status = MediaRequestStatus.FAILED;
requestRepository.save(entity);
if (entity.status !== MediaRequestStatus.FAILED) {
entity.status = MediaRequestStatus.FAILED;
await requestRepository.save(entity);
}
} catch (saveError) {
logger.error('Failed to mark request as FAILED', {
label: 'Media Request',
requestId: entity.id,
errorMessage:
saveError instanceof Error
? saveError.message
: String(saveError),
});
}
logger.warn(
'Something went wrong sending series request to Sonarr, marking status as FAILED',
@@ -758,7 +783,6 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOne({
where: { id: entity.media.id },
relations: { requests: true },
});
if (!media) {
logger.error('Media data not found', {
@@ -768,26 +792,29 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
});
return;
}
const statusKey = entity.is4k ? 'status4k' : 'status';
const seasonRequestRepository = getRepository(SeasonRequest);
const requestRepository = getRepository(MediaRequest);
if (
entity.status === MediaRequestStatus.APPROVED &&
// Do not update the status if the item is already partially available or available
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
media[entity.is4k ? 'status4k' : 'status'] !==
MediaStatus.PARTIALLY_AVAILABLE &&
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
media[statusKey] !== MediaStatus.AVAILABLE &&
media[statusKey] !== MediaStatus.PARTIALLY_AVAILABLE &&
media[statusKey] !== MediaStatus.PROCESSING
) {
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
mediaRepository.save(media);
media[statusKey] = MediaStatus.PROCESSING;
await mediaRepository.save(media);
}
if (
media.mediaType === MediaType.MOVIE &&
entity.status === MediaRequestStatus.DECLINED &&
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
media[statusKey] !== MediaStatus.DELETED
) {
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
media[statusKey] = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
}
/**
@@ -799,14 +826,71 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
if (
media.mediaType === MediaType.TV &&
entity.status === MediaRequestStatus.DECLINED &&
media.requests.filter(
(request) => request.status === MediaRequestStatus.PENDING
).length === 0 &&
media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
media[statusKey] === MediaStatus.PENDING
) {
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
mediaRepository.save(media);
const pendingCount = await requestRepository.count({
where: {
media: { id: media.id },
status: MediaRequestStatus.PENDING,
is4k: entity.is4k,
id: Not(entity.id),
},
});
if (pendingCount === 0) {
// Re-fetch media without requests to avoid cascade issues
const freshMedia = await mediaRepository.findOne({
where: { id: media.id },
});
if (freshMedia) {
freshMedia[statusKey] = MediaStatus.UNKNOWN;
await mediaRepository.save(freshMedia);
}
}
}
// Reset season statuses when a TV request is declined
if (
media.mediaType === MediaType.TV &&
entity.status === MediaRequestStatus.DECLINED
) {
const seasonRepository = getRepository(Season);
const actualSeasons = await seasonRepository.find({
where: { media: { id: media.id } },
});
for (const seasonRequest of entity.seasons) {
seasonRequest.status = MediaRequestStatus.DECLINED;
await seasonRequestRepository.save(seasonRequest);
const season = actualSeasons.find(
(s) => s.seasonNumber === seasonRequest.seasonNumber
);
if (season && season[statusKey] === MediaStatus.PENDING) {
const otherActiveRequests = await requestRepository
.createQueryBuilder('request')
.leftJoinAndSelect('request.seasons', 'season')
.where('request.mediaId = :mediaId', { mediaId: media.id })
.andWhere('request.id != :requestId', { requestId: entity.id })
.andWhere('request.is4k = :is4k', { is4k: entity.is4k })
.andWhere('request.status NOT IN (:...statuses)', {
statuses: [
MediaRequestStatus.DECLINED,
MediaRequestStatus.COMPLETED,
],
})
.andWhere('season.seasonNumber = :seasonNumber', {
seasonNumber: season.seasonNumber,
})
.getCount();
if (otherActiveRequests === 0) {
season[statusKey] = MediaStatus.UNKNOWN;
await seasonRepository.save(season);
}
}
}
}
// Approve child seasons if parent is approved
@@ -830,54 +914,74 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface<MediaRe
relations: { requests: true },
});
if (!fullMedia) return;
if (
const needsStatusUpdate =
!fullMedia.requests.some((request) => !request.is4k) &&
fullMedia.status !== MediaStatus.AVAILABLE
) {
fullMedia.status = MediaStatus.UNKNOWN;
}
fullMedia.status !== MediaStatus.AVAILABLE;
if (
const needs4kStatusUpdate =
!fullMedia.requests.some((request) => request.is4k) &&
fullMedia.status4k !== MediaStatus.AVAILABLE
) {
fullMedia.status4k = MediaStatus.UNKNOWN;
}
fullMedia.status4k !== MediaStatus.AVAILABLE;
await manager.save(fullMedia);
if (needsStatusUpdate || needs4kStatusUpdate) {
// Re-fetch WITHOUT requests to avoid cascade issues on save
const cleanMedia = await manager.findOneOrFail(Media, {
where: { id: entity.media.id },
});
if (needsStatusUpdate) {
cleanMedia.status = MediaStatus.UNKNOWN;
}
if (needs4kStatusUpdate) {
cleanMedia.status4k = MediaStatus.UNKNOWN;
}
await manager.save(cleanMedia);
}
}
public afterUpdate(event: UpdateEvent<MediaRequest>): void {
public async afterUpdate(event: UpdateEvent<MediaRequest>): Promise<void> {
if (!event.entity) {
return;
}
this.sendToRadarr(event.entity as MediaRequest);
this.sendToSonarr(event.entity as MediaRequest);
try {
await this.sendToRadarr(event.entity as MediaRequest);
await this.sendToSonarr(event.entity as MediaRequest);
await this.updateParentStatus(event.entity as MediaRequest);
this.updateParentStatus(event.entity as MediaRequest);
if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
this.notifyAvailableMovie(event.entity as MediaRequest, event);
}
if (event.entity.media.mediaType === MediaType.TV) {
this.notifyAvailableSeries(event.entity as MediaRequest, event);
if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
await this.notifyAvailableMovie(event.entity as MediaRequest, event);
}
if (event.entity.media.mediaType === MediaType.TV) {
await this.notifyAvailableSeries(event.entity as MediaRequest, event);
}
}
} catch (e) {
logger.error('Error in afterUpdate subscriber', {
label: 'Media Request',
requestId: (event.entity as MediaRequest).id,
errorMessage: e instanceof Error ? e.message : String(e),
});
}
}
public afterInsert(event: InsertEvent<MediaRequest>): void {
public async afterInsert(event: InsertEvent<MediaRequest>): Promise<void> {
if (!event.entity) {
return;
}
this.sendToRadarr(event.entity as MediaRequest);
this.sendToSonarr(event.entity as MediaRequest);
this.updateParentStatus(event.entity as MediaRequest);
try {
await this.sendToRadarr(event.entity as MediaRequest);
await this.sendToSonarr(event.entity as MediaRequest);
await this.updateParentStatus(event.entity as MediaRequest);
} catch (e) {
logger.error('Error 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> {

View File

@@ -360,7 +360,12 @@ const TvRequestModal = ({
).length > 0
) {
data.mediaInfo.requests
.filter((request) => request.is4k === is4k)
.filter(
(request) =>
request.is4k === is4k &&
request.status !== MediaRequestStatus.DECLINED &&
request.status !== MediaRequestStatus.COMPLETED
)
.forEach((request) => {
if (!seasonRequest) {
seasonRequest = request.seasons.find(

View File

@@ -19,7 +19,7 @@ import type {
WatchProviderDetails,
} from '@server/models/common';
import axios from 'axios';
import { orderBy } from 'lodash';
import orderBy from 'lodash/orderBy';
import { useEffect, useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
import type { MultiValue, SingleValue } from 'react-select';

View File

@@ -11,11 +11,7 @@ import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import type OverrideRule from '@server/entity/OverrideRule';
import type {
DVRSettings,
RadarrSettings,
SonarrSettings,
} from '@server/lib/settings';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { useCallback, useEffect, useState } from 'react';
@@ -81,23 +77,26 @@ const OverrideRuleModal = ({
});
const getServiceInfos = useCallback(
async ({
hostname,
port,
apiKey,
baseUrl,
useSsl = false,
}: {
hostname: string;
port: number;
apiKey: string;
baseUrl?: string;
useSsl?: boolean;
}) => {
async (
{
hostname,
port,
apiKey,
baseUrl,
useSsl = false,
}: {
hostname: string;
port: number;
apiKey: string;
baseUrl?: string;
useSsl?: boolean;
},
type: 'radarr' | 'sonarr'
) => {
setIsTesting(true);
try {
const response = await axios.post<DVRTestResponse>(
'/api/v1/settings/sonarr/test',
`/api/v1/settings/${type}/test`,
{
hostname,
apiKey,
@@ -119,15 +118,19 @@ const OverrideRuleModal = ({
);
useEffect(() => {
let service: DVRSettings | null = null;
if (rule?.radarrServiceId !== null && rule?.radarrServiceId !== undefined) {
service = radarrServices[rule?.radarrServiceId] || null;
if (
rule?.radarrServiceId !== null &&
rule?.radarrServiceId !== undefined &&
radarrServices[rule?.radarrServiceId]
) {
getServiceInfos(radarrServices[rule?.radarrServiceId], 'radarr');
}
if (rule?.sonarrServiceId !== null && rule?.sonarrServiceId !== undefined) {
service = sonarrServices[rule?.sonarrServiceId] || null;
}
if (service) {
getServiceInfos(service);
if (
rule?.sonarrServiceId !== null &&
rule?.sonarrServiceId !== undefined &&
sonarrServices[rule?.sonarrServiceId]
) {
getServiceInfos(sonarrServices[rule?.sonarrServiceId], 'sonarr');
}
}, [
getServiceInfos,
@@ -256,13 +259,13 @@ const OverrideRuleModal = ({
setFieldValue('radarrServiceId', id);
setFieldValue('sonarrServiceId', null);
if (radarrServices[id]) {
getServiceInfos(radarrServices[id]);
getServiceInfos(radarrServices[id], 'radarr');
}
} else if (e.target.value.startsWith('sonarr-')) {
setFieldValue('radarrServiceId', null);
setFieldValue('sonarrServiceId', id);
if (sonarrServices[id]) {
getServiceInfos(sonarrServices[id]);
getServiceInfos(sonarrServices[id], 'sonarr');
}
} else {
setFieldValue('radarrServiceId', null);

View File

@@ -134,7 +134,6 @@ const OverrideRuleTiles = ({
const users: User[] = response.data.results;
setUsers(users);
}
setUsers(users);
})();
}, [rules, users]);

View File

@@ -69,6 +69,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages(
dnsCacheGlobalStats: 'Global DNS Cache Stats',
dnsCacheGlobalStatsDescription:
'These stats are aggregated across all DNS cache entries.',
dnsNoCacheEntries: 'No DNS lookups have been cached yet.',
size: 'Size',
hits: 'Hits',
misses: 'Misses',
@@ -611,91 +612,133 @@ const SettingsJobs = () => {
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">{intl.formatMessage(messages.dnsCache)}</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
<Table.TH>{intl.formatMessage(messages.dnscachename)}</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscacheactiveaddress)}
</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscachehits)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscachemisses)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.dnscacheage)}</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
<Table.TBody>
{Object.entries(cacheData?.dnsCache.entries || {}).map(
([hostname, data]) => (
<tr key={`cache-list-${hostname}`}>
<Table.TD>{hostname}</Table.TD>
<Table.TD>{data.activeAddress}</Table.TD>
<Table.TD>{intl.formatNumber(data.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(data.misses)}</Table.TD>
<Table.TD>{formatAge(data.age)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushDnsCache(hostname)}
>
<TrashIcon />
<span>{intl.formatMessage(messages.flushdnscache)}</span>
</Button>
</Table.TD>
</tr>
)
)}
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">
{intl.formatMessage(messages.dnsCacheGlobalStats)}
</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheGlobalStatsDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
{Object.entries(cacheData?.dnsCache.stats || {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName]) => (
<Table.TH key={`dns-stat-header-${statName}`}>
{messages[statName]
? intl.formatMessage(messages[statName])
: statName}
{cacheData?.dnsCache != null && (
<>
<div>
<h3 className="heading">{intl.formatMessage(messages.dnsCache)}</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheDescription)}
</p>
</div>
<div className="section">
<Table>
<thead>
<tr>
<Table.TH>
{intl.formatMessage(messages.dnscachename)}
</Table.TH>
))}
</tr>
</thead>
<Table.TBody>
<tr>
{Object.entries(cacheData?.dnsCache.stats || {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName, statValue]) => (
<Table.TD key={`dns-stat-${statName}`}>
{statName === 'hitRate'
? intl.formatNumber(statValue, {
style: 'percent',
maximumFractionDigits: 2,
})
: intl.formatNumber(statValue)}
</Table.TD>
))}
</tr>
</Table.TBody>
</Table>
</div>
<Table.TH>
{intl.formatMessage(messages.dnscacheactiveaddress)}
</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscachehits)}
</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscachemisses)}
</Table.TH>
<Table.TH>
{intl.formatMessage(messages.dnscacheage)}
</Table.TH>
<Table.TH></Table.TH>
</tr>
</thead>
<Table.TBody>
{(() => {
if (!cacheData) {
return (
<tr>
<Table.TD colSpan={6} alignText="center">
<LoadingSpinner />
</Table.TD>
</tr>
);
}
const entries = Object.entries(
cacheData.dnsCache?.entries ?? {}
);
if (entries.length === 0) {
return (
<tr>
<Table.TD colSpan={6} alignText="center">
{intl.formatMessage(messages.dnsNoCacheEntries)}
</Table.TD>
</tr>
);
}
return entries.map(([hostname, data]) => (
<tr key={`cache-list-${hostname}`}>
<Table.TD>{hostname}</Table.TD>
<Table.TD>{data.activeAddress}</Table.TD>
<Table.TD>{intl.formatNumber(data.hits)}</Table.TD>
<Table.TD>{intl.formatNumber(data.misses)}</Table.TD>
<Table.TD>{formatAge(data.age)}</Table.TD>
<Table.TD alignText="right">
<Button
buttonType="danger"
onClick={() => flushDnsCache(hostname)}
>
<TrashIcon />
<span>
{intl.formatMessage(messages.flushdnscache)}
</span>
</Button>
</Table.TD>
</tr>
));
})()}
</Table.TBody>
</Table>
</div>
<div>
<h3 className="heading">
{intl.formatMessage(messages.dnsCacheGlobalStats)}
</h3>
<p className="description">
{intl.formatMessage(messages.dnsCacheGlobalStatsDescription)}
</p>
</div>
<div className="section">
{!cacheData ? (
<LoadingSpinner />
) : (
<Table>
<thead>
<tr>
{Object.entries(cacheData.dnsCache?.stats ?? {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName]) => (
<Table.TH key={`dns-stat-header-${statName}`}>
{messages[statName]
? intl.formatMessage(messages[statName])
: statName}
</Table.TH>
))}
</tr>
</thead>
<Table.TBody>
<tr>
{Object.entries(cacheData.dnsCache?.stats ?? {})
.filter(([statName]) => statName !== 'maxSize')
.map(([statName, statValue]) => (
<Table.TD key={`dns-stat-${statName}`}>
{statName === 'hitRate'
? intl.formatNumber(statValue, {
style: 'percent',
maximumFractionDigits: 2,
})
: intl.formatNumber(statValue)}
</Table.TD>
))}
</tr>
</Table.TBody>
</Table>
)}
</div>
</>
)}
<div className="break-words">
<h3 className="heading">{intl.formatMessage(messages.imagecache)}</h3>
<p className="description">

View File

@@ -29,6 +29,8 @@ const messages = defineMessages('components.Settings.SettingsNetwork', {
trustProxyTip:
'Allow Seerr to correctly register client IP addresses behind a proxy',
proxyEnabled: 'HTTP(S) Proxy',
proxyEnabledTip:
'Send ALL outgoing HTTP/HTTPS requests through a proxy server (host/port). Does NOT enable HTTPS, SSL, or certificate configuration.',
proxyHostname: 'Proxy Hostname',
proxyPort: 'Proxy Port',
proxySsl: 'Use SSL For Proxy',
@@ -78,13 +80,16 @@ const SettingsNetwork = () => {
then: Yup.number()
.typeError(intl.formatMessage(messages.validationDnsCacheMaxTtl))
.required(intl.formatMessage(messages.validationDnsCacheMaxTtl))
.min(0),
.min(-1),
}),
proxyPort: Yup.number().when('proxyEnabled', {
is: (proxyEnabled: boolean) => proxyEnabled,
then: Yup.number().required(
intl.formatMessage(messages.validationProxyPort)
),
then: Yup.number()
.typeError(intl.formatMessage(messages.validationProxyPort))
.integer(intl.formatMessage(messages.validationProxyPort))
.min(1, intl.formatMessage(messages.validationProxyPort))
.max(65535, intl.formatMessage(messages.validationProxyPort))
.required(intl.formatMessage(messages.validationProxyPort)),
}),
});
@@ -288,50 +293,50 @@ const SettingsNetwork = () => {
<div className="form-row">
<label
htmlFor="dnsCacheForceMinTtl"
className="checkbox-label"
className="text-label"
>
{intl.formatMessage(messages.dnsCacheForceMinTtl)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="dnsCacheForceMinTtl"
name="dnsCacheForceMinTtl"
type="number"
/>
</div>
{errors.dnsCacheForceMinTtl &&
touched.dnsCacheForceMinTtl &&
typeof errors.dnsCacheForceMinTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMinTtl}
</div>
)}
<Field
id="dnsCacheForceMinTtl"
name="dnsCacheForceMinTtl"
type="text"
inputMode="numeric"
className="short"
/>
</div>
{errors.dnsCacheForceMinTtl &&
touched.dnsCacheForceMinTtl &&
typeof errors.dnsCacheForceMinTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMinTtl}
</div>
)}
</div>
<div className="form-row">
<label
htmlFor="dnsCacheForceMaxTtl"
className="checkbox-label"
className="text-label"
>
{intl.formatMessage(messages.dnsCacheForceMaxTtl)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="dnsCacheForceMaxTtl"
name="dnsCacheForceMaxTtl"
type="number"
/>
</div>
{errors.dnsCacheForceMaxTtl &&
touched.dnsCacheForceMaxTtl &&
typeof errors.dnsCacheForceMaxTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMaxTtl}
</div>
)}
<Field
id="dnsCacheForceMaxTtl"
name="dnsCacheForceMaxTtl"
type="text"
inputMode="text"
className="short"
/>
</div>
{errors.dnsCacheForceMaxTtl &&
touched.dnsCacheForceMaxTtl &&
typeof errors.dnsCacheForceMaxTtl === 'string' && (
<div className="error">
{errors.dnsCacheForceMaxTtl}
</div>
)}
</div>
</div>
</>
@@ -343,6 +348,9 @@ const SettingsNetwork = () => {
</span>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<span className="label-tip">
{intl.formatMessage(messages.proxyEnabledTip)}
</span>
</label>
<div className="form-input-area">
<Field
@@ -387,13 +395,13 @@ const SettingsNetwork = () => {
{intl.formatMessage(messages.proxyPort)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
id="proxyPort"
name="proxyPort"
type="number"
/>
</div>
<Field
id="proxyPort"
name="proxyPort"
type="text"
inputMode="numeric"
className="short"
/>
{errors.proxyPort &&
touched.proxyPort &&
typeof errors.proxyPort === 'string' && (

View File

@@ -202,7 +202,7 @@ const UserWebPushSettings = () => {
if (user?.id) {
verifyWebPush();
}
}, [user?.id, currentSettings]);
}, [user?.id, currentSettings, dataDevices]);
useEffect(() => {
const getSubscriptionEndpoint = async () => {

View File

@@ -83,6 +83,8 @@ const useDiscover = <
{
initialSize: 3,
revalidateFirstPage: false,
dedupingInterval: 30000,
revalidateOnFocus: false,
}
);

View File

@@ -1599,5 +1599,11 @@
"i18n.removeFromBlacklistSuccess": "",
"i18n.removefromBlacklist": "",
"i18n.specials": "",
"components.Selector.searchUsers": ""
"components.Selector.searchUsers": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.save": "",
"components.UserProfile.UserSettings.LinkJellyfinModal.title": "",
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegion": "",
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": ""
"components.UserProfile.UserSettings.UserGeneralSettings.streamingRegionTip": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Blacklist.blacklistSettingsDescription": "",
"components.Blacklist.blacklistdate": "",
"components.Blacklist.blacklistedby": "",
"components.Blacklist.blacklistsettings": ""
"components.Blacklist.blacklistsettings": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"i18n.deleted": "",
"components.DiscoverTvUpcoming.upcomingtv": "",
"components.Settings.SettingsNetwork.docs": "",
"component.BlacklistModal.blacklisting": ""
"component.BlacklistModal.blacklisting": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -557,7 +557,7 @@
"components.Settings.SettingsJobsCache.jobname": "Jobnavn",
"components.Settings.SettingsJobsCache.jobs": "Jobs",
"components.Settings.SettingsJobsCache.jobsDescription": "Seerr kører visse vedligeholdelsesopgaver som regelmæssigt planlagte jobs, men de kan også udløses manuelt nedenfor. Manuel kørsel af et job vil ikke ændre dets tidsplan.",
"components.Settings.SettingsLogs.logsDescription": "Du kan også se disse logfiler direkte via <code>stdout</code>, eller i <code>{appDataPath}/logs/jellyseerr.log</code>.",
"components.Settings.SettingsLogs.logsDescription": "Du kan også se disse logfiler direkte via <code>stdout</code>, eller i <code>{appDataPath}/logs/seerr.log</code>.",
"components.Settings.SettingsUsers.userSettingsDescription": "Konfigurer globale og standardbrugerindstillinger.",
"components.Settings.SonarrModal.animeTags": "Anime-tags",
"components.Settings.SonarrModal.animelanguageprofile": "Anime-sprogprofil",
@@ -1568,7 +1568,7 @@
"components.Settings.copyBlacklistedTagsTip": "Kopiér konfigurationen af skjulnings-tags",
"components.Settings.importBlacklistedTagsTip": "Importér konfiguration af skjulnings-tags",
"components.Settings.SettingsJobsCache.dnsCacheDescription": "Seerr cacher DNS-opslag for at optimere ydeevnen og undgå unødvendige API-kald.",
"components.Settings.SettingsNetwork.networksettingsDescription": "Konfigurer netværksindstillinger for din Seerr-instans.",
"components.Settings.SettingsNetwork.networksettingsDescription": "Konfigurér netværksindstillinger for din Seerr-instans.",
"components.Settings.SettingsNetwork.trustProxyTip": "Tillad Seerr at registrere klient-IP-adresser korrekt bag en proxy",
"components.Settings.manualscanDescriptionJellyfin": "Normalt vil dette kun blive kørt én gang i døgnet. Seerr vil tjekke din {mediaServerName}-server for senest tilføjede mere aggressivt. Hvis dette er første gang, du konfigurerer Seerr, anbefales en engangs fuld manuel biblioteksscanning!",
"components.Settings.SettingsMain.discoverRegion": "Region til Udforsk-siden",
@@ -1594,10 +1594,16 @@
"components.Blacklist.blacklistedby": "{date} af {user}",
"component.BlacklistBlock.blacklistdate": "Skjult dato",
"component.BlacklistBlock.blacklistedby": "Skjult af",
"component.BlacklistModal.blacklisting": "Skjulning",
"component.BlacklistModal.blacklisting": "Skjul",
"components.Blacklist.blacklistNotFoundError": "<strong>{title}</strong> er ikke skjult.",
"components.Blacklist.blacklistSettingsDescription": "Administrér skjulte medier.",
"components.Blacklist.blacklistsettings": "Skjulnings-indstillinger",
"components.Selector.searchStatus": "Vælg status...",
"i18n.addToBlacklist": "Skjul"
"i18n.addToBlacklist": "Skjul",
"components.Settings.SettingsAbout.aboutseerr": "Om Seer",
"components.Settings.SettingsAbout.contribute": "Giv et bidrag",
"components.Settings.SettingsAbout.supportseerr": "Støt Seerr",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "Du skal angive en gyldig maksimum-TLL",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "Du skal angive en gyldig minimum-TLL",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "Aktivt abonnement"
}

View File

@@ -1599,5 +1599,11 @@
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.engine": "Engine",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "typ",
"i18n.deleted": "Gelöscht",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "Alle {jobScheduleDays, plural, ein {day} andere {{jobScheduleDays} days}}"
"components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "Alle {jobScheduleDays, plural, ein {day} andere {{jobScheduleDays} days}}",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Blacklist.blacklistdate": "",
"components.Blacklist.blacklistedby": "",
"components.Blacklist.blacklistsettings": "",
"components.Settings.SettingsJobsCache.usersavatars": ""
"components.Settings.SettingsJobsCache.usersavatars": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -890,6 +890,7 @@
"components.Settings.SettingsJobsCache.dnsCacheDescription": "Seerr caches DNS lookups to optimize performance and avoid making unnecessary API calls.",
"components.Settings.SettingsJobsCache.dnsCacheGlobalStats": "Global DNS Cache Stats",
"components.Settings.SettingsJobsCache.dnsCacheGlobalStatsDescription": "These stats are aggregated across all DNS cache entries.",
"components.Settings.SettingsJobsCache.dnsNoCacheEntries": "No DNS lookups have been cached yet.",
"components.Settings.SettingsJobsCache.dnscacheactiveaddress": "Active Address",
"components.Settings.SettingsJobsCache.dnscacheage": "Age",
"components.Settings.SettingsJobsCache.dnscacheflushed": "{hostname} dns cache flushed.",
@@ -1015,6 +1016,7 @@
"components.Settings.SettingsNetwork.proxyBypassFilterTip": "Use ',' as a separator, and '*.' as a wildcard for subdomains",
"components.Settings.SettingsNetwork.proxyBypassLocalAddresses": "Bypass Proxy for Local Addresses",
"components.Settings.SettingsNetwork.proxyEnabled": "HTTP(S) Proxy",
"components.Settings.SettingsNetwork.proxyEnabledTip": "Send ALL outgoing HTTP/HTTPS requests through a proxy server (host/port). Does NOT enable HTTPS, SSL, or certificate configuration.",
"components.Settings.SettingsNetwork.proxyHostname": "Proxy Hostname",
"components.Settings.SettingsNetwork.proxyPassword": "Proxy Password",
"components.Settings.SettingsNetwork.proxyPort": "Proxy Port",

View File

@@ -1599,5 +1599,11 @@
"components.Settings.SettingsUsers.loginMethodsTip": "",
"components.Setup.signinWithEmby": "",
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "",
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": ""
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Blacklist.blacklistedby": "",
"components.Blacklist.blacklistsettings": "",
"components.Settings.SettingsMain.streamingRegion": "",
"components.Settings.SettingsMain.streamingRegionTip": ""
"components.Settings.SettingsMain.streamingRegionTip": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.yes": "Bai",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "Ezabatu harpidetza",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.device": "Gailua",
"i18n.completed": "Osatuta"
"i18n.completed": "Osatuta",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.RequestModal.AdvancedRequester.qualityprofile": "Laatuprofiili",
"components.RequestModal.AdvancedRequester.notagoptions": "Ei tägejä",
"components.RequestModal.QuotaDisplay.season": "kausi",
"components.RequestModal.numberofepisodes": "Jaksoja"
"components.RequestModal.numberofepisodes": "Jaksoja",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.noSpecialCharacters": "La configuration doit être une liste d'identifiants de mots-clés TMDB délimités par des virgules, et ne doit ni commencer ni se terminer par une virgule.",
"components.Settings.SettingsNetwork.dnsCacheHoverTip": "N'activez PAS ceci si vous rencontrez des problèmes avec la résolution DNS",
"components.Settings.animeMetadataProvider": "Fournisseur de métadonnées pour les animés",
"components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "Tous les {jobScheduleDays, plural, one {jour} other {{jobScheduleDays} jours}}"
"components.Settings.SettingsJobsCache.editJobScheduleSelectorDays": "Tous les {jobScheduleDays, plural, one {jour} other {{jobScheduleDays} jours}}",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.SettingsAbout.Releases.latestversion": "",
"components.Settings.SettingsJobsCache.canceljob": "",
"components.Settings.SettingsJobsCache.command": "",
"components.Settings.SettingsUsers.defaultPermissions": ""
"components.Settings.SettingsUsers.defaultPermissions": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"i18n.blacklistDuplicateError": "",
"pages.oops": "",
"pages.pagenotfound": "",
"pages.returnHome": ""
"pages.returnHome": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Blacklist.blacklistSettingsDescription": "",
"components.Blacklist.blacklistdate": "",
"components.Blacklist.blacklistedby": "",
"components.Blacklist.blacklistsettings": ""
"components.Blacklist.blacklistsettings": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Login.validationemailformat": "Érvényes e-mail cím szükséges",
"components.Discover.updatefailed": "Hiba történt a felfedezés testreszabási beállításainak frissítésekor.",
"components.Login.emailtooltip": "A címnek nem kell a {mediaServerName} példányhoz tartoznia.",
"components.Login.hostname": "{mediaServerName} URL"
"components.Login.hostname": "{mediaServerName} URL",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "",
"i18n.completed": "",
"i18n.deleted": ""
"i18n.deleted": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -637,13 +637,13 @@
"components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "有効な URL を入力してください",
"components.Blacklist.filterBlacklistedTags": "",
"components.Blacklist.filterManual": "",
"components.Blacklist.mediaName": "",
"components.Blacklist.mediaName": "タイトル",
"components.Blacklist.mediaTmdbId": "",
"components.Blacklist.mediaType": "",
"components.Blacklist.mediaType": "タイプ",
"components.Blacklist.showAllBlacklisted": "",
"components.Discover.CreateSlider.addSlider": "",
"components.Discover.CreateSlider.addcustomslider": "",
"components.Discover.CreateSlider.addfail": "",
"components.Discover.CreateSlider.addSlider": "スライダーを追加",
"components.Discover.CreateSlider.addcustomslider": "カスタムスライダーを作成",
"components.Discover.CreateSlider.addfail": "スライダーの作成に失敗しました。",
"components.Discover.CreateSlider.addsuccess": "",
"components.Discover.CreateSlider.editSlider": "",
"components.Discover.CreateSlider.editfail": "",
@@ -1584,8 +1584,8 @@
"components.Settings.SettingsMain.apikey": "",
"components.Settings.toastPlexConnectingFailure": "",
"components.Settings.toastPlexConnectingSuccess": "",
"components.AirDateBadge.airedrelative": "",
"components.AirDateBadge.airsrelative": "",
"components.AirDateBadge.airedrelative": "{relativeTime}に放送",
"components.AirDateBadge.airsrelative": "{relativeTime}に放送予定",
"components.Login.validationEmailFormat": "",
"components.Settings.Notifications.NotificationsPushbullet.accessToken": "",
"components.Settings.Notifications.NotificationsPushbullet.accessTokenTip": "",
@@ -1599,5 +1599,11 @@
"components.Blacklist.blacklistdate": "",
"components.Blacklist.blacklistedby": "",
"components.Blacklist.blacklistsettings": "",
"components.Settings.apiKey": ""
"components.Settings.apiKey": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Blacklist.blacklistdate": "",
"components.Blacklist.blacklistedby": "",
"components.Blacklist.blacklistsettings": "",
"components.Settings.Notifications.webhookRoleId": ""
"components.Settings.Notifications.webhookRoleId": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Blacklist.blacklistsettings": "",
"components.Login.initialsignin": "",
"components.Settings.SettingsLogs.copiedLogMessage": "",
"components.Settings.SettingsLogs.copyToClipboard": ""
"components.Settings.SettingsLogs.copyToClipboard": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Discover.FilterSlideover.voteCount": "",
"components.Settings.SettingsMain.toastSettingsSuccess": "",
"components.Setup.back": "",
"components.Setup.configemby": ""
"components.Setup.configemby": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.Notifications.NotificationsNtfy.topic": "Onderwerp",
"components.Settings.Notifications.NotificationsNtfy.username": "Gebruikersnaam",
"components.Settings.Notifications.NotificationsNtfy.validationNtfyTopic": "Je moet een onderwerp invullen",
"components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "Je moet een geldige URL invullen"
"components.Settings.Notifications.NotificationsNtfy.validationNtfyUrl": "Je moet een geldige URL invullen",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.SettingsMain.enableSpecialEpisodes": "",
"components.Settings.SettingsMain.streamingRegion": "",
"components.Settings.SettingsMain.streamingRegionTip": "",
"i18n.blacklistError": ""
"i18n.blacklistError": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Blacklist.blacklistSettingsDescription": "",
"components.Blacklist.blacklistdate": "",
"components.Blacklist.blacklistedby": "",
"components.Blacklist.blacklistsettings": ""
"components.Blacklist.blacklistsettings": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"i18n.specials": "",
"components.Login.port": "",
"components.TvDetails.play": "",
"components.Settings.SettingsUsers.atLeastOneAuth": ""
"components.Settings.SettingsUsers.atLeastOneAuth": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.SonarrModal.selectRootFolder": "",
"components.UserList.accounttype": "",
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "",
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": ""
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "",
"i18n.completed": "",
"i18n.deleted": ""
"i18n.deleted": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"i18n.save": "",
"i18n.saving": "",
"i18n.settings": "",
"i18n.specials": ""
"i18n.specials": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Login.enablessl": "",
"components.Setup.configplex": "",
"components.Setup.configuremediaserver": "",
"components.TitleCard.cleardata": ""
"components.TitleCard.cleardata": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.Notifications.NotificationsGotify.validationUrlTrailingSlash": "",
"components.UserProfile.UserSettings.UserNotificationSettings.validationPushoverUserKey": "",
"components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "",
"i18n.specials": ""
"i18n.specials": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.managedevices": "Hantera enheter",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.subscriptiondeleted": "Tog bort prenumerationer.",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.type": "typ",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "Okänd"
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.unknown": "Okänd",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "",
"i18n.completed": "",
"i18n.deleted": ""
"i18n.deleted": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.SettingsNetwork.forceIpv4FirstTip": "Змусити Jellyseerr спершу резолвити адреси IPv4 замість IPv6",
"components.Settings.SettingsNetwork.networkDisclaimer": "Параметри мережі з вашого контейнера/системи слід використовувати замість цих налаштувань. Дивіться {docs} для отримання додаткової інформації.",
"components.Settings.SettingsNetwork.networksettingsDescription": "Налаштуйте параметри мережі для вашого екземпляра Jellyseerr.",
"components.Settings.SettingsNetwork.proxyBypassFilter": "Ігноровані адреси проксі"
"components.Settings.SettingsNetwork.proxyBypassFilter": "Ігноровані адреси проксі",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingsfailed": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.webpushsettingssaved": "",
"i18n.completed": "",
"i18n.deleted": ""
"i18n.deleted": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}

View File

@@ -1599,5 +1599,11 @@
"components.Settings.SettingsUsers.loginMethods": "",
"components.Settings.SettingsUsers.loginMethodsTip": "",
"i18n.removeFromBlacklistSuccess": "",
"i18n.removefromBlacklist": ""
"i18n.removefromBlacklist": "",
"components.Settings.SettingsAbout.aboutseerr": "",
"components.Settings.SettingsAbout.contribute": "",
"components.Settings.SettingsAbout.supportseerr": "",
"components.Settings.SettingsNetwork.validationDnsCacheMaxTtl": "",
"components.Settings.SettingsNetwork.validationDnsCacheMinTtl": "",
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": ""
}