0.13.1 DVD Series rc1
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1159,6 +1159,29 @@ function normalizePositiveIntegerOrNull(value) {
|
|||||||
return Math.trunc(parsed);
|
return Math.trunc(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSeriesBatchEpisodeSubJobPlan(encodePlan = null) {
|
||||||
|
const plan = encodePlan && typeof encodePlan === 'object' ? encodePlan : {};
|
||||||
|
if (Boolean(plan?.seriesBatchChild) || Boolean(plan?.seriesBatchVirtualEpisode)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const parentJobId = normalizePositiveIntegerOrNull(plan?.seriesBatchParentJobId);
|
||||||
|
const titleId = normalizePositiveIntegerOrNull(plan?.seriesBatchTitleId);
|
||||||
|
const childIndex = normalizePositiveIntegerOrNull(plan?.seriesBatchChildIndex);
|
||||||
|
const childCount = normalizePositiveIntegerOrNull(plan?.seriesBatchChildCount);
|
||||||
|
if (parentJobId && (titleId || childIndex || childCount)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSeriesBatchEpisodeSubJobRow(row = null) {
|
||||||
|
const source = row && typeof row === 'object' ? row : {};
|
||||||
|
const plan = source?.encodePlan && typeof source.encodePlan === 'object'
|
||||||
|
? source.encodePlan
|
||||||
|
: parseJsonSafe(source?.encode_plan_json, null);
|
||||||
|
return isSeriesBatchEpisodeSubJobPlan(plan);
|
||||||
|
}
|
||||||
|
|
||||||
function extractSeasonNumberFromText(value) {
|
function extractSeasonNumberFromText(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -3675,8 +3698,24 @@ class HistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allContainerChildRows = [...childRows, ...nestedChildRows];
|
const allContainerChildRows = [...childRows, ...nestedChildRows];
|
||||||
|
const directDiskRowsByContainerId = new Map();
|
||||||
|
const directEpisodeRowsByContainerId = new Map();
|
||||||
const directChildRowById = new Map();
|
const directChildRowById = new Map();
|
||||||
for (const row of childRows) {
|
for (const row of childRows) {
|
||||||
|
const containerId = normalizeJobIdValue(row?.parent_job_id);
|
||||||
|
const isEpisodeSubJob = isSeriesBatchEpisodeSubJobRow(row);
|
||||||
|
if (containerId) {
|
||||||
|
const map = isEpisodeSubJob
|
||||||
|
? directEpisodeRowsByContainerId
|
||||||
|
: directDiskRowsByContainerId;
|
||||||
|
if (!map.has(containerId)) {
|
||||||
|
map.set(containerId, []);
|
||||||
|
}
|
||||||
|
map.get(containerId).push(row);
|
||||||
|
}
|
||||||
|
if (isEpisodeSubJob) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const childId = normalizeJobIdValue(row?.id);
|
const childId = normalizeJobIdValue(row?.id);
|
||||||
if (childId) {
|
if (childId) {
|
||||||
directChildRowById.set(childId, row);
|
directChildRowById.set(childId, row);
|
||||||
@@ -3737,8 +3776,9 @@ class HistoryService {
|
|||||||
const episodeCount = Number(selectedMetadata?.episodeCount || 0);
|
const episodeCount = Number(selectedMetadata?.episodeCount || 0);
|
||||||
const episodesLength = Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes.length : 0;
|
const episodesLength = Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes.length : 0;
|
||||||
const expectedTotal = episodeCount > 0 ? episodeCount : (episodesLength > 0 ? episodesLength : 0);
|
const expectedTotal = episodeCount > 0 ? episodeCount : (episodesLength > 0 ? episodesLength : 0);
|
||||||
const childIds = childRows
|
const containerChildRows = directDiskRowsByContainerId.get(containerId) || [];
|
||||||
.filter((row) => normalizeJobIdValue(row?.parent_job_id) === containerId)
|
const containerEpisodeRows = directEpisodeRowsByContainerId.get(containerId) || [];
|
||||||
|
const childIds = containerChildRows
|
||||||
.map((row) => normalizeJobIdValue(row?.id))
|
.map((row) => normalizeJobIdValue(row?.id))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
let rawExistsAny = false;
|
let rawExistsAny = false;
|
||||||
@@ -3851,6 +3891,22 @@ class HistoryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const episodeRow of containerEpisodeRows) {
|
||||||
|
const episodeRowId = normalizeJobIdValue(episodeRow?.id);
|
||||||
|
if (!episodeRowId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const outputs = Array.from(childOutputMap.get(episodeRowId) || []);
|
||||||
|
for (const outputPath of outputs) {
|
||||||
|
if (!outputPath || seenOutputs.has(outputPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenOutputs.add(outputPath);
|
||||||
|
if (!includeFsChecks || fs.existsSync(outputPath)) {
|
||||||
|
existingCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (existingCountFromSeriesBatch > existingCount) {
|
if (existingCountFromSeriesBatch > existingCount) {
|
||||||
existingCount = existingCountFromSeriesBatch;
|
existingCount = existingCountFromSeriesBatch;
|
||||||
}
|
}
|
||||||
@@ -4319,6 +4375,10 @@ class HistoryService {
|
|||||||
const childJobs = await Promise.all(
|
const childJobs = await Promise.all(
|
||||||
childRows.map((row) => this.repairImportedOrphanJobClassification(row, settings))
|
childRows.map((row) => this.repairImportedOrphanJobClassification(row, settings))
|
||||||
);
|
);
|
||||||
|
const isSeriesContainer = String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
||||||
|
const detailChildJobs = isSeriesContainer
|
||||||
|
? childJobs.filter((child) => !isSeriesBatchEpisodeSubJobRow(child))
|
||||||
|
: childJobs;
|
||||||
const parsedTail = Number(options.logTailLines);
|
const parsedTail = Number(options.logTailLines);
|
||||||
const logTailLines = Number.isFinite(parsedTail) && parsedTail > 0
|
const logTailLines = Number.isFinite(parsedTail) && parsedTail > 0
|
||||||
? Math.trunc(parsedTail)
|
? Math.trunc(parsedTail)
|
||||||
@@ -4330,7 +4390,7 @@ class HistoryService {
|
|||||||
const hasProcessLog = (!shouldLoadLogs && includeFsChecks) ? hasProcessLogFile(jobId) : false;
|
const hasProcessLog = (!shouldLoadLogs && includeFsChecks) ? hasProcessLogFile(jobId) : false;
|
||||||
const baseLogCount = hasProcessLog ? 1 : 0;
|
const baseLogCount = hasProcessLog ? 1 : 0;
|
||||||
const enrichedChildren = await Promise.all(
|
const enrichedChildren = await Promise.all(
|
||||||
childJobs.map(async (child) => {
|
detailChildJobs.map(async (child) => {
|
||||||
const base = {
|
const base = {
|
||||||
...enrichJobRow(child, settings, { includeFsChecks }),
|
...enrichJobRow(child, settings, { includeFsChecks }),
|
||||||
log_count: includeFsChecks ? (hasProcessLogFile(child.id) ? 1 : 0) : 0
|
log_count: includeFsChecks ? (hasProcessLogFile(child.id) ? 1 : 0) : 0
|
||||||
@@ -4365,7 +4425,6 @@ class HistoryService {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSeriesContainer = String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
|
||||||
let seriesChildSummary = null;
|
let seriesChildSummary = null;
|
||||||
if (isSeriesContainer && enrichedChildren.length > 0) {
|
if (isSeriesContainer && enrichedChildren.length > 0) {
|
||||||
const total = enrichedChildren.length;
|
const total = enrichedChildren.length;
|
||||||
|
|||||||
@@ -13172,6 +13172,19 @@ class PipelineService extends EventEmitter {
|
|||||||
let parentContainerJobId = normalizePositiveInteger(job.parent_job_id || null);
|
let parentContainerJobId = normalizePositiveInteger(job.parent_job_id || null);
|
||||||
const isDvdSeriesSelection = isDvdTmdbMetadataSelection;
|
const isDvdSeriesSelection = isDvdTmdbMetadataSelection;
|
||||||
if (isDvdSeriesSelection) {
|
if (isDvdSeriesSelection) {
|
||||||
|
const resolveDiscNumberFromJobRow = (row) => {
|
||||||
|
const rowMkInfo = this.safeParseJson(row?.makemkv_info_json) || {};
|
||||||
|
const rowAnalyzeContext = rowMkInfo?.analyzeContext && typeof rowMkInfo.analyzeContext === 'object'
|
||||||
|
? rowMkInfo.analyzeContext
|
||||||
|
: {};
|
||||||
|
const rowSelectedMetadata = rowAnalyzeContext?.selectedMetadata && typeof rowAnalyzeContext.selectedMetadata === 'object'
|
||||||
|
? rowAnalyzeContext.selectedMetadata
|
||||||
|
: (rowMkInfo?.selectedMetadata && typeof rowMkInfo.selectedMetadata === 'object'
|
||||||
|
? rowMkInfo.selectedMetadata
|
||||||
|
: {});
|
||||||
|
return normalizePositiveInteger(rowSelectedMetadata?.discNumber);
|
||||||
|
};
|
||||||
|
|
||||||
if (!parentContainerJobId) {
|
if (!parentContainerJobId) {
|
||||||
const existingContainer = await historyService.findSeriesContainerJob(effectiveTmdbId, effectiveSeasonNumber);
|
const existingContainer = await historyService.findSeriesContainerJob(effectiveTmdbId, effectiveSeasonNumber);
|
||||||
if (existingContainer) {
|
if (existingContainer) {
|
||||||
@@ -13212,6 +13225,38 @@ class PipelineService extends EventEmitter {
|
|||||||
parentContainerJobId = Number(containerJob?.id || 0) || null;
|
parentContainerJobId = Number(containerJob?.id || 0) || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parentContainerJobId && effectiveDiscNumber) {
|
||||||
|
const containerChildren = await historyService.listChildJobs(parentContainerJobId);
|
||||||
|
const conflictingDiscChild = (Array.isArray(containerChildren) ? containerChildren : []).find((childRow) => {
|
||||||
|
const childId = Number(childRow?.id || 0);
|
||||||
|
if (!childId || childId === Number(jobId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const childDiscNumber = resolveDiscNumberFromJobRow(childRow);
|
||||||
|
return childDiscNumber === effectiveDiscNumber;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conflictingDiscChild) {
|
||||||
|
const existingChildJobId = Number(conflictingDiscChild?.id || 0) || null;
|
||||||
|
const error = new Error(
|
||||||
|
`Disk ${effectiveDiscNumber} ist fuer diese Serie/Staffel bereits vorhanden (Job #${existingChildJobId || '?'}). Bitte andere Disk-Nummer waehlen.`
|
||||||
|
);
|
||||||
|
error.statusCode = 409;
|
||||||
|
error.details = [
|
||||||
|
{
|
||||||
|
code: 'SERIES_DISC_ALREADY_EXISTS',
|
||||||
|
containerJobId: Number(parentContainerJobId),
|
||||||
|
existingJobId: existingChildJobId,
|
||||||
|
discNumber: Number(effectiveDiscNumber),
|
||||||
|
tmdbId: Number(effectiveTmdbId || 0) || null,
|
||||||
|
seasonNumber: Number(effectiveSeasonNumber || 0) || null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parentContainerJobId) {
|
if (parentContainerJobId) {
|
||||||
const containerMkInfo = this.withAnalyzeContextMediaProfile({
|
const containerMkInfo = this.withAnalyzeContextMediaProfile({
|
||||||
analyzeContext: {
|
analyzeContext: {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.2",
|
"primereact": "^10.9.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -739,6 +739,47 @@ function resolveSeriesDiscNumber(job) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSeriesBatchEpisodeChildJob(job) {
|
||||||
|
const plan = job?.encodePlan && typeof job.encodePlan === 'object'
|
||||||
|
? job.encodePlan
|
||||||
|
: null;
|
||||||
|
if (!plan) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Boolean(plan?.seriesBatchChild) || Boolean(plan?.seriesBatchVirtualEpisode)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const hasParent = normalizePositiveInteger(plan?.seriesBatchParentJobId) !== null;
|
||||||
|
const hasEpisodeMarker = normalizePositiveInteger(plan?.seriesBatchTitleId) !== null
|
||||||
|
|| normalizePositiveInteger(plan?.seriesBatchChildIndex) !== null
|
||||||
|
|| normalizePositiveInteger(plan?.seriesBatchChildCount) !== null;
|
||||||
|
return hasParent && hasEpisodeMarker;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSeriesContainerDiskChildren(children = []) {
|
||||||
|
const rows = Array.isArray(children) ? children : [];
|
||||||
|
const diskCandidates = rows.filter((child) => !isSeriesBatchEpisodeChildJob(child));
|
||||||
|
const byDisc = new Map();
|
||||||
|
const withoutDisc = [];
|
||||||
|
|
||||||
|
for (const child of [...diskCandidates].sort((a, b) => Number(b?.id || 0) - Number(a?.id || 0))) {
|
||||||
|
const discNumber = resolveSeriesDiscNumber(child);
|
||||||
|
if (discNumber !== null) {
|
||||||
|
if (!byDisc.has(discNumber)) {
|
||||||
|
byDisc.set(discNumber, child);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
withoutDisc.push(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
const withDisc = Array.from(byDisc.entries())
|
||||||
|
.sort((left, right) => left[0] - right[0])
|
||||||
|
.map(([, child]) => child);
|
||||||
|
const withoutDiscSorted = withoutDisc.sort(compareSeriesChildJobsByDisc);
|
||||||
|
return [...withDisc, ...withoutDiscSorted];
|
||||||
|
}
|
||||||
|
|
||||||
function compareSeriesChildJobsByDisc(leftJob, rightJob) {
|
function compareSeriesChildJobsByDisc(leftJob, rightJob) {
|
||||||
const leftDisc = resolveSeriesDiscNumber(leftJob);
|
const leftDisc = resolveSeriesDiscNumber(leftJob);
|
||||||
const rightDisc = resolveSeriesDiscNumber(rightJob);
|
const rightDisc = resolveSeriesDiscNumber(rightJob);
|
||||||
@@ -1128,10 +1169,14 @@ export default function JobDetailDialog({
|
|||||||
const metadataDetails = resolveMetadataDetailsForDisplay(job, omdbInfo);
|
const metadataDetails = resolveMetadataDetailsForDisplay(job, omdbInfo);
|
||||||
const seriesDiscNumber = isDvdSeries ? resolveSeriesDiscNumber(job) : null;
|
const seriesDiscNumber = isDvdSeries ? resolveSeriesDiscNumber(job) : null;
|
||||||
const seriesDiscLabel = seriesDiscNumber ? `Disk ${seriesDiscNumber}` : 'Disk unbekannt';
|
const seriesDiscLabel = seriesDiscNumber ? `Disk ${seriesDiscNumber}` : 'Disk unbekannt';
|
||||||
const childJobs = Array.isArray(job?.children)
|
|
||||||
? [...job.children].sort(compareSeriesChildJobsByDisc)
|
|
||||||
: [];
|
|
||||||
const isSeriesContainer = isDvdSeries && String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
const isSeriesContainer = isDvdSeries && String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
||||||
|
const childJobs = Array.isArray(job?.children)
|
||||||
|
? (
|
||||||
|
isSeriesContainer
|
||||||
|
? buildSeriesContainerDiskChildren(job.children)
|
||||||
|
: [...job.children].sort(compareSeriesChildJobsByDisc)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
const seriesChildSummary = job?.seriesChildSummary || null;
|
const seriesChildSummary = job?.seriesChildSummary || null;
|
||||||
const seriesBackupSummary = isSeriesContainer ? seriesChildSummary?.backup : null;
|
const seriesBackupSummary = isSeriesContainer ? seriesChildSummary?.backup : null;
|
||||||
const seriesEncodeSummary = isSeriesContainer ? seriesChildSummary?.encode : null;
|
const seriesEncodeSummary = isSeriesContainer ? seriesChildSummary?.encode : null;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export default function MetadataSelectionDialog({
|
|||||||
const [extraResults, setExtraResults] = useState([]);
|
const [extraResults, setExtraResults] = useState([]);
|
||||||
const [searchBusy, setSearchBusy] = useState(false);
|
const [searchBusy, setSearchBusy] = useState(false);
|
||||||
const [searchError, setSearchError] = useState('');
|
const [searchError, setSearchError] = useState('');
|
||||||
|
const [submitError, setSubmitError] = useState('');
|
||||||
const [seasonFilter, setSeasonFilter] = useState('');
|
const [seasonFilter, setSeasonFilter] = useState('');
|
||||||
const [discValidationError, setDiscValidationError] = useState('');
|
const [discValidationError, setDiscValidationError] = useState('');
|
||||||
const metadataProvider = String(context?.metadataProvider || 'omdb').trim().toLowerCase() || 'omdb';
|
const metadataProvider = String(context?.metadataProvider || 'omdb').trim().toLowerCase() || 'omdb';
|
||||||
@@ -56,20 +57,17 @@ export default function MetadataSelectionDialog({
|
|||||||
const defaultTitle = selectedMetadata.title || context?.detectedTitle || '';
|
const defaultTitle = selectedMetadata.title || context?.detectedTitle || '';
|
||||||
const defaultYear = selectedMetadata.year ? String(selectedMetadata.year) : '';
|
const defaultYear = selectedMetadata.year ? String(selectedMetadata.year) : '';
|
||||||
const defaultImdb = selectedMetadata.imdbId || '';
|
const defaultImdb = selectedMetadata.imdbId || '';
|
||||||
const hintedDiscNumber = selectedMetadata.discNumber || context?.seriesLookupHint?.discNumber || null;
|
|
||||||
const defaultDiscNumber = Number(hintedDiscNumber || 0) > 0
|
|
||||||
? String(Math.trunc(Number(hintedDiscNumber || 0)))
|
|
||||||
: '';
|
|
||||||
|
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
setQuery(defaultTitle);
|
setQuery(defaultTitle);
|
||||||
setManualTitle(defaultTitle);
|
setManualTitle(defaultTitle);
|
||||||
setManualYear(defaultYear);
|
setManualYear(defaultYear);
|
||||||
setManualImdb(defaultImdb);
|
setManualImdb(defaultImdb);
|
||||||
setManualDiscNumber(defaultDiscNumber);
|
setManualDiscNumber('');
|
||||||
setExtraResults([]);
|
setExtraResults([]);
|
||||||
setSearchBusy(false);
|
setSearchBusy(false);
|
||||||
setSearchError('');
|
setSearchError('');
|
||||||
|
setSubmitError('');
|
||||||
setSeasonFilter('');
|
setSeasonFilter('');
|
||||||
setDiscValidationError('');
|
setDiscValidationError('');
|
||||||
}, [visible, context]);
|
}, [visible, context]);
|
||||||
@@ -83,6 +81,12 @@ export default function MetadataSelectionDialog({
|
|||||||
}
|
}
|
||||||
}, [discValidationError, requiresDiscNumber, hasValidDiscNumber]);
|
}, [discValidationError, requiresDiscNumber, hasValidDiscNumber]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submitError) {
|
||||||
|
setSubmitError('');
|
||||||
|
}
|
||||||
|
}, [manualDiscNumber, seasonFilter, selected]);
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
const base = Array.isArray(context?.metadataCandidates)
|
const base = Array.isArray(context?.metadataCandidates)
|
||||||
? context.metadataCandidates
|
? context.metadataCandidates
|
||||||
@@ -166,6 +170,8 @@ export default function MetadataSelectionDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
setDiscValidationError('');
|
||||||
|
setSubmitError('');
|
||||||
if (requiresDiscNumber && !hasValidDiscNumber) {
|
if (requiresDiscNumber && !hasValidDiscNumber) {
|
||||||
setDiscValidationError('Disk-Nummer ist Pflicht und muss eine ganze Zahl >= 1 sein.');
|
setDiscValidationError('Disk-Nummer ist Pflicht und muss eine ganze Zahl >= 1 sein.');
|
||||||
return;
|
return;
|
||||||
@@ -201,7 +207,18 @@ export default function MetadataSelectionDialog({
|
|||||||
...(effectiveDiscNumber ? { discNumber: effectiveDiscNumber } : {})
|
...(effectiveDiscNumber ? { discNumber: effectiveDiscNumber } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
await onSubmit(payload);
|
await onSubmit(payload);
|
||||||
|
} catch (error) {
|
||||||
|
const details = Array.isArray(error?.details) ? error.details : [];
|
||||||
|
const hasDuplicateDiscError = details.some((detail) => String(detail?.code || '').trim().toUpperCase() === 'SERIES_DISC_ALREADY_EXISTS');
|
||||||
|
const message = String(error?.message || 'Metadaten konnten nicht uebernommen werden.').trim() || 'Metadaten konnten nicht uebernommen werden.';
|
||||||
|
if (hasDuplicateDiscError) {
|
||||||
|
setSubmitError(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitError(message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHideRequest = () => {
|
const handleHideRequest = () => {
|
||||||
@@ -262,6 +279,7 @@ export default function MetadataSelectionDialog({
|
|||||||
</div>
|
</div>
|
||||||
{searchError ? <small className="error-text">{searchError}</small> : null}
|
{searchError ? <small className="error-text">{searchError}</small> : null}
|
||||||
{discValidationError ? <small className="error-text">{discValidationError}</small> : null}
|
{discValidationError ? <small className="error-text">{discValidationError}</small> : null}
|
||||||
|
{submitError ? <small className="error-text">{submitError}</small> : null}
|
||||||
|
|
||||||
<div className="table-scroll-wrap table-scroll-medium">
|
<div className="table-scroll-wrap table-scroll-medium">
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|||||||
@@ -485,6 +485,77 @@ function normalizePositiveInt(value) {
|
|||||||
return Math.trunc(parsed);
|
return Math.trunc(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeEpisodeReference(entry = {}) {
|
||||||
|
const source = entry && typeof entry === 'object' ? entry : {};
|
||||||
|
const episodeId = normalizePositiveInt(source?.episodeId ?? source?.id ?? null);
|
||||||
|
const episodeNumber = normalizePositiveInt(source?.episodeNumber ?? source?.number ?? null);
|
||||||
|
const seasonNumber = normalizePositiveInt(source?.seasonNumber ?? source?.season ?? null);
|
||||||
|
if (!episodeId && !episodeNumber) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
episodeId,
|
||||||
|
episodeNumber,
|
||||||
|
seasonNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEpisodeReferenceKey(entry = {}) {
|
||||||
|
const normalized = normalizeEpisodeReference(entry);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (normalized.episodeId) {
|
||||||
|
return `id:${normalized.episodeId}`;
|
||||||
|
}
|
||||||
|
if (normalized.seasonNumber && normalized.episodeNumber) {
|
||||||
|
return `se:${normalized.seasonNumber}:${normalized.episodeNumber}`;
|
||||||
|
}
|
||||||
|
return normalized.episodeNumber ? `ep:${normalized.episodeNumber}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSeriesBatchEpisodePlan(plan = null) {
|
||||||
|
const source = plan && typeof plan === 'object' ? plan : {};
|
||||||
|
if (Boolean(source?.seriesBatchChild) || Boolean(source?.seriesBatchVirtualEpisode)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const parentJobId = normalizePositiveInt(source?.seriesBatchParentJobId);
|
||||||
|
const childIndex = normalizePositiveInt(source?.seriesBatchChildIndex);
|
||||||
|
const childCount = normalizePositiveInt(source?.seriesBatchChildCount);
|
||||||
|
const titleId = normalizeTitleId(source?.seriesBatchTitleId);
|
||||||
|
return Boolean(parentJobId && (childIndex || childCount || titleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectEpisodeReferencesFromPlan(plan = null) {
|
||||||
|
const source = plan && typeof plan === 'object' ? plan : {};
|
||||||
|
const refs = [];
|
||||||
|
const assignments = source?.episodeAssignments && typeof source.episodeAssignments === 'object'
|
||||||
|
? Object.values(source.episodeAssignments)
|
||||||
|
: [];
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
const normalized = normalizeEpisodeReference(assignment);
|
||||||
|
if (normalized) {
|
||||||
|
refs.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const titles = Array.isArray(source?.titles) ? source.titles : [];
|
||||||
|
for (const title of titles) {
|
||||||
|
if (!Boolean(title?.selectedForEncode) && !Boolean(title?.encodeInput)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalized = normalizeEpisodeReference({
|
||||||
|
episodeId: title?.episodeId,
|
||||||
|
episodeNumber: title?.episodeNumber,
|
||||||
|
seasonNumber: title?.seasonNumber
|
||||||
|
});
|
||||||
|
if (normalized) {
|
||||||
|
refs.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSeriesLanguage(value) {
|
function normalizeSeriesLanguage(value) {
|
||||||
const raw = String(value || '').trim();
|
const raw = String(value || '').trim();
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
@@ -730,6 +801,7 @@ export default function PipelineStatusCard({
|
|||||||
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
|
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
|
||||||
const [episodeAssignmentsByTitle, setEpisodeAssignmentsByTitle] = useState({});
|
const [episodeAssignmentsByTitle, setEpisodeAssignmentsByTitle] = useState({});
|
||||||
const [selectedEpisodeFillStart, setSelectedEpisodeFillStart] = useState(null);
|
const [selectedEpisodeFillStart, setSelectedEpisodeFillStart] = useState(null);
|
||||||
|
const [containerUsedEpisodeKeys, setContainerUsedEpisodeKeys] = useState([]);
|
||||||
const [settingsMap, setSettingsMap] = useState({});
|
const [settingsMap, setSettingsMap] = useState({});
|
||||||
const [presetDisplayMap, setPresetDisplayMap] = useState({});
|
const [presetDisplayMap, setPresetDisplayMap] = useState({});
|
||||||
const [scriptCatalog, setScriptCatalog] = useState([]);
|
const [scriptCatalog, setScriptCatalog] = useState([]);
|
||||||
@@ -748,7 +820,7 @@ export default function PipelineStatusCard({
|
|||||||
const converterConfigSaveTimeoutRef = useRef(null);
|
const converterConfigSaveTimeoutRef = useRef(null);
|
||||||
const selectedMetadataEpisodes = useMemo(() => {
|
const selectedMetadataEpisodes = useMemo(() => {
|
||||||
const rows = Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes : [];
|
const rows = Array.isArray(selectedMetadata?.episodes) ? selectedMetadata.episodes : [];
|
||||||
return rows
|
const normalizedRows = rows
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const episodeId = Number(item?.id || 0);
|
const episodeId = Number(item?.id || 0);
|
||||||
const episodeNumber = Number(item?.number ?? item?.episodeNumber ?? 0);
|
const episodeNumber = Number(item?.number ?? item?.episodeNumber ?? 0);
|
||||||
@@ -777,7 +849,15 @@ export default function PipelineStatusCard({
|
|||||||
const bId = Number.isFinite(b.episodeId) ? b.episodeId : Number.MAX_SAFE_INTEGER;
|
const bId = Number.isFinite(b.episodeId) ? b.episodeId : Number.MAX_SAFE_INTEGER;
|
||||||
return aId - bId;
|
return aId - bId;
|
||||||
});
|
});
|
||||||
}, [selectedMetadata?.episodes]);
|
if (!Array.isArray(containerUsedEpisodeKeys) || containerUsedEpisodeKeys.length === 0) {
|
||||||
|
return normalizedRows;
|
||||||
|
}
|
||||||
|
const usedKeySet = new Set(containerUsedEpisodeKeys);
|
||||||
|
return normalizedRows.filter((episode) => {
|
||||||
|
const key = buildEpisodeReferenceKey(episode);
|
||||||
|
return !key || !usedKeySet.has(key);
|
||||||
|
});
|
||||||
|
}, [selectedMetadata?.episodes, containerUsedEpisodeKeys]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -1333,6 +1413,102 @@ export default function PipelineStatusCard({
|
|||||||
const metadataProvider = String(selectedMetadata?.metadataProvider || '').trim().toLowerCase();
|
const metadataProvider = String(selectedMetadata?.metadataProvider || '').trim().toLowerCase();
|
||||||
return hasSeasonNumber || hasEpisodeList || metadataProvider === 'tmdb' || metadataProvider === 'themoviedb';
|
return hasSeasonNumber || hasEpisodeList || metadataProvider === 'tmdb' || metadataProvider === 'themoviedb';
|
||||||
}, [jobMediaProfile, selectedMetadata?.seasonNumber, selectedMetadata?.episodes, selectedMetadata?.metadataProvider]);
|
}, [jobMediaProfile, selectedMetadata?.seasonNumber, selectedMetadata?.episodes, selectedMetadata?.metadataProvider]);
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const loadContainerUsedEpisodes = async () => {
|
||||||
|
if (!isSeriesDvdReview || !retryJobId) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setContainerUsedEpisodeKeys([]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentResponse = await api.getJob(retryJobId, { lite: true, forceRefresh: true });
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentJob = currentResponse?.job && typeof currentResponse.job === 'object'
|
||||||
|
? currentResponse.job
|
||||||
|
: null;
|
||||||
|
const currentJobId = normalizeJobId(currentJob?.id || retryJobId);
|
||||||
|
const normalizedCurrentKind = String(currentJob?.job_kind || '').trim().toLowerCase();
|
||||||
|
const containerJobId = normalizedCurrentKind === 'dvd_series_container'
|
||||||
|
? normalizeJobId(currentJob?.id)
|
||||||
|
: normalizeJobId(currentJob?.parent_job_id);
|
||||||
|
|
||||||
|
if (!containerJobId) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setContainerUsedEpisodeKeys([]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerResponse = await api.getJob(containerJobId, { lite: true, forceRefresh: true });
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const containerJob = containerResponse?.job && typeof containerResponse.job === 'object'
|
||||||
|
? containerResponse.job
|
||||||
|
: null;
|
||||||
|
const children = Array.isArray(containerJob?.children) ? containerJob.children : [];
|
||||||
|
const usedKeys = new Set();
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
const childId = normalizeJobId(child?.id);
|
||||||
|
if (currentJobId && childId && currentJobId === childId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isSeriesBatchEpisodePlan(child?.encodePlan)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refs = collectEpisodeReferencesFromPlan(child?.encodePlan);
|
||||||
|
for (const ref of refs) {
|
||||||
|
const key = buildEpisodeReferenceKey(ref);
|
||||||
|
if (key) {
|
||||||
|
usedKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchEpisodes = Array.isArray(child?.handbrakeInfo?.seriesBatch?.episodes)
|
||||||
|
? child.handbrakeInfo.seriesBatch.episodes
|
||||||
|
: [];
|
||||||
|
for (const episode of batchEpisodes) {
|
||||||
|
const key = buildEpisodeReferenceKey({
|
||||||
|
episodeId: episode?.episodeId,
|
||||||
|
episodeNumber: episode?.episodeNumber,
|
||||||
|
seasonNumber: episode?.seasonNumber
|
||||||
|
});
|
||||||
|
if (key) {
|
||||||
|
usedKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
setContainerUsedEpisodeKeys(Array.from(usedKeys));
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setContainerUsedEpisodeKeys([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadContainerUsedEpisodes();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
isSeriesDvdReview,
|
||||||
|
retryJobId,
|
||||||
|
mediaInfoReview?.generatedAt,
|
||||||
|
mediaInfoReview?.reviewConfirmedAt,
|
||||||
|
selectedMetadata?.tmdbId,
|
||||||
|
selectedMetadata?.seasonNumber
|
||||||
|
]);
|
||||||
const seriesBatchContext = useMemo(() => {
|
const seriesBatchContext = useMemo(() => {
|
||||||
const raw = pipeline?.context?.seriesBatch;
|
const raw = pipeline?.context?.seriesBatch;
|
||||||
if (!raw || typeof raw !== 'object') {
|
if (!raw || typeof raw !== 'object') {
|
||||||
|
|||||||
@@ -1344,8 +1344,8 @@ export default function RipperPage({
|
|||||||
imdbId: job?.imdb_id || null,
|
imdbId: job?.imdb_id || null,
|
||||||
poster: job?.poster_url || null,
|
poster: job?.poster_url || null,
|
||||||
metadataProvider: context?.metadataProvider || 'omdb',
|
metadataProvider: context?.metadataProvider || 'omdb',
|
||||||
seasonNumber: context?.seriesLookupHint?.seasonNumber || null,
|
seasonNumber: null,
|
||||||
discNumber: context?.seriesLookupHint?.discNumber || null
|
discNumber: null
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...context,
|
...context,
|
||||||
@@ -1379,8 +1379,8 @@ export default function RipperPage({
|
|||||||
imdbId: null,
|
imdbId: null,
|
||||||
poster: null,
|
poster: null,
|
||||||
metadataProvider: currentContext?.metadataProvider || 'omdb',
|
metadataProvider: currentContext?.metadataProvider || 'omdb',
|
||||||
seasonNumber: currentContext?.seriesLookupHint?.seasonNumber || null,
|
seasonNumber: null,
|
||||||
discNumber: currentContext?.seriesLookupHint?.discNumber || null
|
discNumber: null
|
||||||
},
|
},
|
||||||
metadataProvider: currentContext?.metadataProvider || currentContext?.selectedMetadata?.metadataProvider || 'omdb',
|
metadataProvider: currentContext?.metadataProvider || currentContext?.selectedMetadata?.metadataProvider || 'omdb',
|
||||||
metadataCandidates: Array.isArray(currentContext?.metadataCandidates) ? currentContext.metadataCandidates : [],
|
metadataCandidates: Array.isArray(currentContext?.metadataCandidates) ? currentContext.metadataCandidates : [],
|
||||||
@@ -2202,7 +2202,8 @@ export default function RipperPage({
|
|||||||
return response.results || [];
|
return response.results || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const doSelectMetadata = async (payload) => {
|
const doSelectMetadata = async (payload, options = {}) => {
|
||||||
|
const suppressErrorToast = Boolean(options?.suppressErrorToast);
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
if (metadataDialogReassignMode) {
|
if (metadataDialogReassignMode) {
|
||||||
@@ -2216,6 +2217,9 @@ export default function RipperPage({
|
|||||||
setMetadataDialogContext(null);
|
setMetadataDialogContext(null);
|
||||||
setMetadataDialogReassignMode(false);
|
setMetadataDialogReassignMode(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (suppressErrorToast) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
showError(error);
|
showError(error);
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
@@ -2223,17 +2227,27 @@ export default function RipperPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMetadataSubmit = async (payload) => {
|
const handleMetadataSubmit = async (payload) => {
|
||||||
|
const payloadProvider = String(payload?.metadataProvider || '').trim().toLowerCase();
|
||||||
|
const currentJobMediaProfile = String(effectiveMetadataDialogContext?.mediaProfile || '').trim().toLowerCase();
|
||||||
|
const isSeriesDvdPayload = payloadProvider === 'tmdb' && currentJobMediaProfile === 'dvd';
|
||||||
|
|
||||||
if (metadataDialogReassignMode) {
|
if (metadataDialogReassignMode) {
|
||||||
await doSelectMetadata(payload);
|
await doSelectMetadata(payload);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serien-DVDs laufen ohne Film-Duplikatdialog weiter; die Disk-Pruefung
|
||||||
|
// erfolgt serverseitig gegen den vorhandenen Staffel-Container.
|
||||||
|
if (isSeriesDvdPayload) {
|
||||||
|
await doSelectMetadata(payload, { suppressErrorToast: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Duplikatprüfung: nur bei OMDB-Auswahl mit imdbId sinnvoll
|
// Duplikatprüfung: nur bei OMDB-Auswahl mit imdbId sinnvoll
|
||||||
const searchTitle = payload.title || '';
|
const searchTitle = payload.title || '';
|
||||||
const searchImdbId = payload.imdbId || null;
|
const searchImdbId = payload.imdbId || null;
|
||||||
if (searchTitle) {
|
if (searchTitle) {
|
||||||
try {
|
try {
|
||||||
const currentJobMediaProfile = String(effectiveMetadataDialogContext?.mediaProfile || '').trim().toLowerCase();
|
|
||||||
const historyResponse = await api.getJobs({ search: searchTitle, limit: 50, lite: true });
|
const historyResponse = await api.getJobs({ search: searchTitle, limit: 50, lite: true });
|
||||||
const historyJobs = Array.isArray(historyResponse?.jobs) ? historyResponse.jobs : [];
|
const historyJobs = Array.isArray(historyResponse?.jobs) ? historyResponse.jobs : [];
|
||||||
const duplicate = historyJobs.find((job) => {
|
const duplicate = historyJobs.find((job) => {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.13.0-1",
|
"version": "0.13.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||||
"dev:backend": "npm run dev --prefix backend",
|
"dev:backend": "npm run dev --prefix backend",
|
||||||
|
|||||||
Reference in New Issue
Block a user