Compare commits
18 Commits
preview-ty
...
preview-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b30794dd20 | ||
|
|
1ed86c14c0 | ||
|
|
91261f6a61 | ||
|
|
3dea58eead | ||
|
|
3eea8ee98e | ||
|
|
7cd3521cfd | ||
|
|
e53c2a34dc | ||
|
|
095784bf62 | ||
|
|
4f7819f028 | ||
|
|
028c7c2434 | ||
|
|
e3dc1c302d | ||
|
|
a44a3b1e14 | ||
|
|
186998b888 | ||
|
|
df54fb9451 | ||
|
|
673f3f2939 | ||
|
|
3cd66589ca | ||
|
|
dbee2fdf9f | ||
|
|
0ffe3e8067 |
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -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
83
.github/workflows/detect-duplicate.yml
vendored
Normal 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
|
||||
65
.github/workflows/rebuild-issue-index.yml
vendored
Normal file
65
.github/workflows/rebuild-issue-index.yml
vendored
Normal 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
1
bin/duplicate-detector/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
120
bin/duplicate-detector/build-index.mjs
Normal file
120
bin/duplicate-detector/build-index.mjs
Normal 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);
|
||||
});
|
||||
274
bin/duplicate-detector/detect.mjs
Normal file
274
bin/duplicate-detector/detect.mjs
Normal 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);
|
||||
});
|
||||
17
bin/duplicate-detector/package.json
Normal file
17
bin/duplicate-detector/package.json
Normal 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
655
bin/duplicate-detector/pnpm-lock.yaml
generated
Normal 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: {}
|
||||
116
bin/duplicate-detector/utils.mjs
Normal file
116
bin/duplicate-detector/utils.mjs
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
BIN
gen-docs/blog/2026-02-10/dns-cache.png
Normal file
BIN
gen-docs/blog/2026-02-10/dns-cache.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
gen-docs/blog/2026-02-10/metadata-providers.png
Normal file
BIN
gen-docs/blog/2026-02-10/metadata-providers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
127
gen-docs/blog/2026-02-10/seerr-release.md
Normal file
127
gen-docs/blog/2026-02-10/seerr-release.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
### 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!
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
118
gen-docs/static/img/os_icon.svg
Normal file
118
gen-docs/static/img/os_icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
@@ -24,6 +24,6 @@ module.exports = {
|
||||
},
|
||||
experimental: {
|
||||
scrollRestoration: true,
|
||||
largePageDataBytes: 256000,
|
||||
largePageDataBytes: 512 * 1000,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
3
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"`
|
||||
);
|
||||
}
|
||||
}
|
||||
369
server/migration/sqlite/1770627968781-AddPerformanceIndexes.ts
Normal file
369
server/migration/sqlite/1770627968781-AddPerformanceIndexes.ts
Normal 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") `
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -134,7 +134,6 @@ const OverrideRuleTiles = ({
|
||||
const users: User[] = response.data.results;
|
||||
setUsers(users);
|
||||
}
|
||||
setUsers(users);
|
||||
})();
|
||||
}, [rules, users]);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -202,7 +202,7 @@ const UserWebPushSettings = () => {
|
||||
if (user?.id) {
|
||||
verifyWebPush();
|
||||
}
|
||||
}, [user?.id, currentSettings]);
|
||||
}, [user?.id, currentSettings, dataDevices]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSubscriptionEndpoint = async () => {
|
||||
|
||||
@@ -83,6 +83,8 @@ const useDiscover = <
|
||||
{
|
||||
initialSize: 3,
|
||||
revalidateFirstPage: false,
|
||||
dedupingInterval: 30000,
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user