Compare commits
5 Commits
3feb780e16
...
9297a84eea
| Author | SHA1 | Date | |
|---|---|---|---|
| 9297a84eea | |||
| 154cb48b77 | |||
| d462de8263 | |||
| cac6442b5b | |||
| ba3732af6b |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-backend",
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@@ -850,7 +850,7 @@ const SETTINGS_SCHEMA_METADATA_UPDATES = [
|
||||
{
|
||||
key: 'output_template_dvd_series_multi_episode',
|
||||
required: 1,
|
||||
description: 'Template für zusammengefasste DVD-Serienepisoden ohne Dateiendung. Platzhalter: {seriesTitle}, {seasonNr}, {episodeNoStart}, {episodeNoEnd}, {episodeRange}, {discNr}, {year}, {language}. Optional mit führender Null: {0:seasonNr}, {0:episodeNoStart}, {0:episodeNoEnd}. Alias: {seasonNo} entspricht {seasonNr}.',
|
||||
description: 'Template für zusammengefasste DVD-Serienepisoden ohne Dateiendung. Platzhalter: {seriesTitle}, {seasonNr}, {episodeNoStart}, {episodeNoEnd}, {episodeRange}, {episodeTitle}, {parts}, {discNr}, {year}, {language}. Optional mit führender Null: {0:seasonNr}, {0:episodeNoStart}, {0:episodeNoEnd}. Alias: {seasonNo} entspricht {seasonNr}.',
|
||||
validation_json: '{"minLength":1}'
|
||||
}
|
||||
];
|
||||
@@ -925,6 +925,46 @@ async function migrateSettingsSchemaMetadata(db) {
|
||||
});
|
||||
}
|
||||
|
||||
const dvdSeriesMultiEpisodeTemplateDefault = '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})';
|
||||
const legacyDvdSeriesMultiEpisodeTemplateDefaults = [
|
||||
'{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}'
|
||||
];
|
||||
const legacyTemplatePlaceholders = legacyDvdSeriesMultiEpisodeTemplateDefaults.map(() => '?').join(', ');
|
||||
await db.run(
|
||||
`
|
||||
UPDATE settings_schema
|
||||
SET default_value = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE key = 'output_template_dvd_series_multi_episode'
|
||||
AND (
|
||||
default_value IS NULL
|
||||
OR TRIM(default_value) = ''
|
||||
OR default_value IN (${legacyTemplatePlaceholders})
|
||||
)
|
||||
`,
|
||||
[dvdSeriesMultiEpisodeTemplateDefault, ...legacyDvdSeriesMultiEpisodeTemplateDefaults]
|
||||
);
|
||||
await db.run(
|
||||
`
|
||||
INSERT INTO settings_values (key, value, updated_at)
|
||||
VALUES ('output_template_dvd_series_multi_episode', ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(key) DO NOTHING
|
||||
`,
|
||||
[dvdSeriesMultiEpisodeTemplateDefault]
|
||||
);
|
||||
await db.run(
|
||||
`
|
||||
UPDATE settings_values
|
||||
SET value = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE key = 'output_template_dvd_series_multi_episode'
|
||||
AND (
|
||||
value IS NULL
|
||||
OR TRIM(value) = ''
|
||||
OR value IN (${legacyTemplatePlaceholders})
|
||||
)
|
||||
`,
|
||||
[dvdSeriesMultiEpisodeTemplateDefault, ...legacyDvdSeriesMultiEpisodeTemplateDefaults]
|
||||
);
|
||||
|
||||
// Migrate raw_dir_cd_owner label
|
||||
await db.run(
|
||||
`UPDATE settings_schema SET label = 'Eigentümer CD RAW-Ordner', updated_at = CURRENT_TIMESTAMP
|
||||
@@ -1042,9 +1082,9 @@ async function migrateSettingsSchemaMetadata(db) {
|
||||
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||
VALUES ('output_template_dvd_series_multi_episode', 'Pfade', 'Output Template (DVD Serie, Multi-Episode)', 'string', 1, 'Template für zusammengefasste DVD-Serienepisoden ohne Dateiendung. Platzhalter: {seriesTitle}, {seasonNr}, {episodeNoStart}, {episodeNoEnd}, {episodeRange}, {discNr}, {year}, {language}. Optional mit führender Null: {0:seasonNr}, {0:episodeNoStart}, {0:episodeNoEnd}. Alias: {seasonNo} entspricht {seasonNr}.', '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}', '[]', '{"minLength":1}', 537)`
|
||||
VALUES ('output_template_dvd_series_multi_episode', 'Pfade', 'Output Template (DVD Serie, Multi-Episode)', 'string', 1, 'Template für zusammengefasste DVD-Serienepisoden ohne Dateiendung. Platzhalter: {seriesTitle}, {seasonNr}, {episodeNoStart}, {episodeNoEnd}, {episodeRange}, {episodeTitle}, {parts}, {discNr}, {year}, {language}. Optional mit führender Null: {0:seasonNr}, {0:episodeNoStart}, {0:episodeNoEnd}. Alias: {seasonNo} entspricht {seasonNr}.', '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})', '[]', '{"minLength":1}', 537)`
|
||||
);
|
||||
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_template_dvd_series_multi_episode', '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}')`);
|
||||
await db.run(`INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_template_dvd_series_multi_episode', '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})')`);
|
||||
|
||||
await db.run(
|
||||
`INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||
|
||||
@@ -56,9 +56,22 @@ router.post(
|
||||
asyncHandler(async (req, res) => {
|
||||
const rawPath = String(req.body?.rawPath || '').trim();
|
||||
logger.info('post:orphan-raw:import', { reqId: req.reqId, rawPath });
|
||||
const job = await historyService.importOrphanRawFolder(rawPath);
|
||||
const uiReset = await pipelineService.resetFrontendState('history_orphan_import');
|
||||
res.json({ job, uiReset });
|
||||
const importedJob = await historyService.importOrphanRawFolder(rawPath);
|
||||
const importedJobId = Number(importedJob?.id || 0);
|
||||
const activation = (Number.isFinite(importedJobId) && importedJobId > 0)
|
||||
? await pipelineService.analyzeRawImportJob(importedJobId, {
|
||||
rawPath: importedJob?.raw_path || rawPath
|
||||
})
|
||||
: null;
|
||||
|
||||
const refreshedJob = importedJobId > 0
|
||||
? await historyService.getJobById(importedJobId)
|
||||
: null;
|
||||
|
||||
res.json({
|
||||
job: refreshedJob || importedJob,
|
||||
activation
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -366,6 +366,7 @@ router.post(
|
||||
providerId,
|
||||
tmdbId,
|
||||
metadataKind,
|
||||
workflowKind,
|
||||
seasonNumber,
|
||||
seasonName,
|
||||
episodeCount,
|
||||
@@ -393,6 +394,7 @@ router.post(
|
||||
metadataProvider,
|
||||
providerId,
|
||||
tmdbId,
|
||||
workflowKind,
|
||||
seasonNumber,
|
||||
discNumber
|
||||
});
|
||||
@@ -411,6 +413,7 @@ router.post(
|
||||
providerId,
|
||||
tmdbId,
|
||||
metadataKind,
|
||||
workflowKind,
|
||||
seasonNumber,
|
||||
seasonName,
|
||||
episodeCount,
|
||||
|
||||
@@ -1159,6 +1159,20 @@ function normalizePositiveIntegerOrNull(value) {
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
|
||||
function normalizeDvdMetadataWorkflowKind(value) {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
if (['film', 'movie', 'feature'].includes(raw)) {
|
||||
return 'film';
|
||||
}
|
||||
if (['series', 'tv', 'season', 'episode'].includes(raw)) {
|
||||
return 'series';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveSeriesAssignmentEpisodeSpan(assignment = null) {
|
||||
const source = assignment && typeof assignment === 'object' ? assignment : {};
|
||||
const start = normalizePositiveIntegerOrNull(
|
||||
@@ -1313,6 +1327,17 @@ function extractSelectedMetadataFromMakemkvInfo(makemkvInfo = {}) {
|
||||
function hasSeriesMetadataSignals(selectedMetadata = {}, analyzeContext = {}) {
|
||||
const metadata = selectedMetadata && typeof selectedMetadata === 'object' ? selectedMetadata : {};
|
||||
const analyze = analyzeContext && typeof analyzeContext === 'object' ? analyzeContext : {};
|
||||
const workflowKind = normalizeDvdMetadataWorkflowKind(
|
||||
metadata.workflowKind
|
||||
|| analyze.workflowKind
|
||||
|| null
|
||||
);
|
||||
if (workflowKind === 'film') {
|
||||
return false;
|
||||
}
|
||||
if (workflowKind === 'series') {
|
||||
return true;
|
||||
}
|
||||
const metadataKind = String(metadata.metadataKind || analyze.metadataKind || '').trim().toLowerCase();
|
||||
const seasonNumber = normalizePositiveNumberOrNull(
|
||||
metadata.seasonNumber ?? analyze?.seriesLookupHint?.seasonNumber ?? null
|
||||
@@ -4967,114 +4992,6 @@ class HistoryService {
|
||||
const sourceSelectedMetadata = sourceJobContext?.selectedMetadata && typeof sourceJobContext.selectedMetadata === 'object'
|
||||
? sourceJobContext.selectedMetadata
|
||||
: {};
|
||||
const sourceAnalyzeContext = sourceJobContext?.analyzeContext && typeof sourceJobContext.analyzeContext === 'object'
|
||||
? sourceJobContext.analyzeContext
|
||||
: {};
|
||||
const sourceHasSeriesSignals = hasSeriesMetadataSignals(sourceSelectedMetadata, sourceAnalyzeContext);
|
||||
const seriesImportHints = effectiveDetectedMediaType === 'dvd' && (seriesRawPathHint || sourceHasSeriesSignals)
|
||||
? buildSeriesImportHints({
|
||||
folderName: path.basename(finalRawPath),
|
||||
rawPath: finalRawPath,
|
||||
metadata,
|
||||
sourceSelectedMetadata,
|
||||
sourceAnalyzeContext
|
||||
})
|
||||
: null;
|
||||
let effectiveSeriesImportHints = seriesImportHints;
|
||||
let inferredSeriesContainer = null;
|
||||
if (effectiveDetectedMediaType === 'dvd') {
|
||||
const hintedTmdbId = normalizePositiveIntegerOrNull(
|
||||
effectiveSeriesImportHints?.selectedMetadata?.tmdbId
|
||||
?? effectiveSeriesImportHints?.selectedMetadata?.providerId
|
||||
?? null
|
||||
);
|
||||
const hintedSeasonNumber = normalizePositiveIntegerOrNull(
|
||||
effectiveSeriesImportHints?.selectedMetadata?.seasonNumber
|
||||
?? effectiveSeriesImportHints?.analyzeContextPatch?.seriesLookupHint?.seasonNumber
|
||||
?? null
|
||||
);
|
||||
|
||||
if (hintedTmdbId && hintedSeasonNumber) {
|
||||
inferredSeriesContainer = await this.findSeriesContainerJob(hintedTmdbId, hintedSeasonNumber);
|
||||
}
|
||||
|
||||
if (!inferredSeriesContainer && seriesRawPathHint) {
|
||||
inferredSeriesContainer = await this.findLikelySeriesContainerJob({
|
||||
title: sourceSelectedMetadata?.title || sourceSelectedMetadata?.seriesTitle || effectiveTitle || metadata.title || null,
|
||||
year: sourceSelectedMetadata?.year || sourceJob?.year || metadata.year || null
|
||||
});
|
||||
}
|
||||
|
||||
if (inferredSeriesContainer) {
|
||||
const containerInfo = parseJsonSafe(inferredSeriesContainer?.makemkv_info_json, {}) || {};
|
||||
const containerSelectedMetadata = extractSelectedMetadataFromMakemkvInfo(containerInfo);
|
||||
const containerAnalyzeContext = containerInfo?.analyzeContext && typeof containerInfo.analyzeContext === 'object'
|
||||
? containerInfo.analyzeContext
|
||||
: {};
|
||||
const containerTmdbId = normalizePositiveIntegerOrNull(
|
||||
containerSelectedMetadata?.tmdbId
|
||||
|| containerSelectedMetadata?.providerId
|
||||
|| null
|
||||
);
|
||||
const containerSeasonNumber = normalizePositiveIntegerOrNull(containerSelectedMetadata?.seasonNumber || null);
|
||||
const discNumberHint = normalizePositiveIntegerOrNull(
|
||||
effectiveSeriesImportHints?.selectedMetadata?.discNumber
|
||||
?? extractDiscNumberFromText(`${path.basename(finalRawPath)} ${path.basename(absRawPath)}`)
|
||||
?? null
|
||||
);
|
||||
|
||||
if (containerTmdbId && containerSeasonNumber) {
|
||||
const mergedTitle = String(
|
||||
effectiveSeriesImportHints?.selectedMetadata?.title
|
||||
|| containerSelectedMetadata?.title
|
||||
|| effectiveTitle
|
||||
|| inferredSeriesContainer?.title
|
||||
|| ''
|
||||
).trim() || null;
|
||||
effectiveSeriesImportHints = {
|
||||
selectedMetadata: {
|
||||
...containerSelectedMetadata,
|
||||
...(effectiveSeriesImportHints?.selectedMetadata && typeof effectiveSeriesImportHints.selectedMetadata === 'object'
|
||||
? effectiveSeriesImportHints.selectedMetadata
|
||||
: {}),
|
||||
metadataProvider: 'tmdb',
|
||||
metadataKind: String(containerSelectedMetadata?.metadataKind || 'series').trim().toLowerCase() || 'series',
|
||||
tmdbId: containerTmdbId,
|
||||
seasonNumber: containerSeasonNumber,
|
||||
title: mergedTitle,
|
||||
...(discNumberHint ? { discNumber: discNumberHint } : {})
|
||||
},
|
||||
analyzeContextPatch: {
|
||||
...(effectiveSeriesImportHints?.analyzeContextPatch && typeof effectiveSeriesImportHints.analyzeContextPatch === 'object'
|
||||
? effectiveSeriesImportHints.analyzeContextPatch
|
||||
: {}),
|
||||
metadataProvider: 'tmdb',
|
||||
metadataKind: String(containerAnalyzeContext?.metadataKind || containerSelectedMetadata?.metadataKind || 'series').trim().toLowerCase() || 'series',
|
||||
seriesLookupHint: {
|
||||
...(containerAnalyzeContext?.seriesLookupHint && typeof containerAnalyzeContext.seriesLookupHint === 'object'
|
||||
? containerAnalyzeContext.seriesLookupHint
|
||||
: {}),
|
||||
query: mergedTitle,
|
||||
seasonNumber: containerSeasonNumber,
|
||||
...(discNumberHint ? { discNumber: discNumberHint } : {})
|
||||
},
|
||||
seriesAnalysis: {
|
||||
...(containerAnalyzeContext?.seriesAnalysis && typeof containerAnalyzeContext.seriesAnalysis === 'object'
|
||||
? containerAnalyzeContext.seriesAnalysis
|
||||
: {}),
|
||||
summary: {
|
||||
...(containerAnalyzeContext?.seriesAnalysis?.summary && typeof containerAnalyzeContext.seriesAnalysis.summary === 'object'
|
||||
? containerAnalyzeContext.seriesAnalysis.summary
|
||||
: {}),
|
||||
seriesLike: true,
|
||||
confidence: containerAnalyzeContext?.seriesAnalysis?.summary?.confidence || 'high'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const sourcePosterCandidate = this.getPreferredPosterCandidateForImport(sourceJobContext, null);
|
||||
const recoveredCdEncodePlan = cdRecovery ? this.buildRecoveredCdEncodePlan(cdRecovery) : null;
|
||||
const orphanImportInfo = this.buildOrphanRawImportMakemkvInfo({
|
||||
@@ -5083,12 +5000,13 @@ class HistoryService {
|
||||
requestedRawPath: absRawPath,
|
||||
sourceFolderJobId: metadata.folderJobId || null,
|
||||
mediaProfile: effectiveDetectedMediaType,
|
||||
existingInfo: sourceJobContext?.makemkvInfo || null,
|
||||
// RAW-Import soll wie "Disk analysieren" starten:
|
||||
// keine geerbte Serien-/Metadatenzuordnung vor dem eigentlichen Analyse-Scan.
|
||||
existingInfo: cdRecovery ? (sourceJobContext?.makemkvInfo || null) : null,
|
||||
recovery: cdRecovery,
|
||||
selectedMetadata: effectiveSeriesImportHints?.selectedMetadata || null,
|
||||
analyzeContextPatch: effectiveSeriesImportHints?.analyzeContextPatch || null
|
||||
selectedMetadata: null,
|
||||
analyzeContextPatch: null
|
||||
});
|
||||
const inferredSeriesContainerId = normalizeJobIdValue(inferredSeriesContainer?.id);
|
||||
const recoveredPosterUrl = orphanPosterUrl
|
||||
|| (thumbnailService.isLocalUrl(sourcePosterCandidate) ? null : sourcePosterCandidate)
|
||||
|| null;
|
||||
@@ -5108,11 +5026,7 @@ class HistoryService {
|
||||
await this.updateJob(created.id, {
|
||||
...(effectiveDetectedMediaType === 'audiobook' ? { job_kind: 'audiobook', media_type: 'audiobook' } : {}),
|
||||
...(effectiveDetectedMediaType === 'cd' ? { job_kind: 'cd', media_type: 'cd' } : {}),
|
||||
...(effectiveDetectedMediaType === 'dvd'
|
||||
? (inferredSeriesContainerId
|
||||
? { job_kind: 'dvd_series_child', media_type: 'dvd', parent_job_id: inferredSeriesContainerId }
|
||||
: { job_kind: 'dvd', media_type: 'dvd' })
|
||||
: {}),
|
||||
...(effectiveDetectedMediaType === 'dvd' ? { job_kind: 'dvd', media_type: 'dvd', parent_job_id: null } : {}),
|
||||
...(effectiveDetectedMediaType === 'bluray' ? { job_kind: 'bluray', media_type: 'bluray' } : {}),
|
||||
status: 'FINISHED',
|
||||
last_state: 'FINISHED',
|
||||
@@ -5191,13 +5105,6 @@ class HistoryService {
|
||||
`Metadaten aus vorhandenem Job #${sourceJob.id} wiederhergestellt.`
|
||||
);
|
||||
}
|
||||
if (inferredSeriesContainerId) {
|
||||
await this.appendLog(
|
||||
created.id,
|
||||
'SYSTEM',
|
||||
`Automatisch dem Serien-Container #${inferredSeriesContainerId} zugeordnet.`
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('job:import-orphan-raw', {
|
||||
jobId: created.id,
|
||||
@@ -5236,6 +5143,7 @@ class HistoryService {
|
||||
};
|
||||
const requestedProviderRaw = String(payload.metadataProvider || '').trim().toLowerCase();
|
||||
const requestedTmdbId = parseTmdbId(payload?.tmdbId ?? payload?.providerId ?? null);
|
||||
const requestedWorkflowKind = normalizeDvdMetadataWorkflowKind(payload?.workflowKind);
|
||||
const metadataProvider = requestedProviderRaw === 'themoviedb'
|
||||
? 'tmdb'
|
||||
: (requestedProviderRaw || (requestedTmdbId !== null ? 'tmdb' : 'omdb'));
|
||||
@@ -5455,6 +5363,7 @@ class HistoryService {
|
||||
year,
|
||||
imdbId,
|
||||
poster: posterUrl,
|
||||
workflowKind: 'series',
|
||||
metadataProvider: 'tmdb',
|
||||
providerId,
|
||||
tmdbId: effectiveTmdbId,
|
||||
@@ -5474,6 +5383,7 @@ class HistoryService {
|
||||
: {};
|
||||
const nextAnalyzeContext = {
|
||||
...analyzeContext,
|
||||
workflowKind: 'series',
|
||||
metadataProvider: 'tmdb',
|
||||
metadataKind,
|
||||
selectedMetadata: {
|
||||
@@ -5563,24 +5473,68 @@ class HistoryService {
|
||||
const imdbId = omdb?.imdbId || imdbIdInput || job.imdb_id || null;
|
||||
const posterUrl = omdb?.poster || manualPoster || job.poster_url || null;
|
||||
const selectedFromOmdb = omdb ? 1 : Number(payload.fromOmdb ? 1 : 0);
|
||||
const resolvedJobMediaType = inferMediaType(
|
||||
job,
|
||||
makemkvInfo,
|
||||
job?.mediainfo_info_json,
|
||||
job?.encode_plan_json,
|
||||
job?.handbrake_info_json
|
||||
);
|
||||
const shouldUseFilmWorkflow = resolvedJobMediaType === 'dvd' && metadataProvider === 'omdb';
|
||||
const existingWorkflowKind = normalizeDvdMetadataWorkflowKind(
|
||||
existingSelectedMetadata?.workflowKind
|
||||
|| analyzeContext?.workflowKind
|
||||
|| null
|
||||
);
|
||||
const effectiveWorkflowKind = shouldUseFilmWorkflow
|
||||
? 'film'
|
||||
: (requestedWorkflowKind || existingWorkflowKind);
|
||||
const nextSelectedMetadata = {
|
||||
...(existingSelectedMetadata && typeof existingSelectedMetadata === 'object' ? existingSelectedMetadata : {}),
|
||||
title,
|
||||
year,
|
||||
imdbId,
|
||||
poster: posterUrl,
|
||||
metadataProvider: 'omdb'
|
||||
metadataProvider: 'omdb',
|
||||
...(effectiveWorkflowKind ? { workflowKind: effectiveWorkflowKind } : {}),
|
||||
...(shouldUseFilmWorkflow
|
||||
? {
|
||||
providerId: null,
|
||||
tmdbId: null,
|
||||
metadataKind: 'movie',
|
||||
seasonNumber: null,
|
||||
seasonName: null,
|
||||
episodeCount: 0,
|
||||
episodes: [],
|
||||
discNumber: null,
|
||||
tmdbDetails: null
|
||||
}
|
||||
: {})
|
||||
};
|
||||
const existingAnalyzeSelected = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
|
||||
? analyzeContext.selectedMetadata
|
||||
: {};
|
||||
const existingSeriesLookupHint = analyzeContext?.seriesLookupHint && typeof analyzeContext.seriesLookupHint === 'object'
|
||||
? analyzeContext.seriesLookupHint
|
||||
: {};
|
||||
const nextAnalyzeContext = {
|
||||
...analyzeContext,
|
||||
...(effectiveWorkflowKind ? { workflowKind: effectiveWorkflowKind } : {}),
|
||||
metadataProvider: 'omdb',
|
||||
...(shouldUseFilmWorkflow ? { metadataKind: 'movie' } : {}),
|
||||
selectedMetadata: {
|
||||
...existingAnalyzeSelected,
|
||||
...nextSelectedMetadata
|
||||
},
|
||||
...(shouldUseFilmWorkflow
|
||||
? {
|
||||
seriesLookupHint: {
|
||||
...existingSeriesLookupHint,
|
||||
seasonNumber: null,
|
||||
discNumber: null
|
||||
}
|
||||
}
|
||||
: {})
|
||||
};
|
||||
const topLevelSelected = makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
|
||||
? makemkvInfo.selectedMetadata
|
||||
@@ -5601,7 +5555,14 @@ class HistoryService {
|
||||
poster_url: posterUrl,
|
||||
omdb_json: omdb?.raw ? JSON.stringify(omdb.raw) : (job.omdb_json || null),
|
||||
selected_from_omdb: selectedFromOmdb,
|
||||
makemkv_info_json: JSON.stringify(nextMakemkvInfo)
|
||||
makemkv_info_json: JSON.stringify(nextMakemkvInfo),
|
||||
...(shouldUseFilmWorkflow
|
||||
? {
|
||||
parent_job_id: null,
|
||||
job_kind: 'dvd',
|
||||
media_type: 'dvd'
|
||||
}
|
||||
: {})
|
||||
});
|
||||
|
||||
// Bild herunterladen, in persistenten Ordner verschieben und poster_url aktualisieren
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -468,8 +468,8 @@ VALUES ('output_template_dvd_series_episode', 'Pfade', 'Output Template (DVD Ser
|
||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_template_dvd_series_episode', '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}');
|
||||
|
||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||
VALUES ('output_template_dvd_series_multi_episode', 'Pfade', 'Output Template (DVD Serie, Multi-Episode)', 'string', 1, 'Template für zusammengefasste DVD-Serienepisoden ohne Dateiendung. Platzhalter: {seriesTitle}, {seasonNr}, {episodeNoStart}, {episodeNoEnd}, {episodeRange}, {discNr}, {year}, {language}. Optional mit führender Null: {0:seasonNr}, {0:episodeNoStart}, {0:episodeNoEnd}. Alias: {seasonNo} entspricht {seasonNr}.', '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}', '[]', '{"minLength":1}', 537);
|
||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_template_dvd_series_multi_episode', '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}');
|
||||
VALUES ('output_template_dvd_series_multi_episode', 'Pfade', 'Output Template (DVD Serie, Multi-Episode)', 'string', 1, 'Template für zusammengefasste DVD-Serienepisoden ohne Dateiendung. Platzhalter: {seriesTitle}, {seasonNr}, {episodeNoStart}, {episodeNoEnd}, {episodeRange}, {episodeTitle}, {parts}, {discNr}, {year}, {language}. Optional mit führender Null: {0:seasonNr}, {0:episodeNoStart}, {0:episodeNoEnd}. Alias: {seasonNo} entspricht {seasonNr}.', '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})', '[]', '{"minLength":1}', 537);
|
||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('output_template_dvd_series_multi_episode', '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})');
|
||||
|
||||
-- Tools – CD
|
||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"dependencies": {
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -78,7 +78,10 @@ function stripEpisodePartSuffix(value) {
|
||||
if (!source) {
|
||||
return '';
|
||||
}
|
||||
const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)\s*$/i;
|
||||
// Output cleanup for multi-episode labels:
|
||||
// remove "Teil/Part" markers and pure numeric/roman suffixes in
|
||||
// parentheses like "(1)" / "(2)".
|
||||
const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)|\(\s*(?:\d{1,2}|[ivxlcdm]{1,6})\s*\))\s*$/i;
|
||||
let normalized = source;
|
||||
let changed = false;
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button } from 'primereact/button';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
|
||||
export default function MetadataSelectionDialog({
|
||||
visible,
|
||||
@@ -13,6 +14,19 @@ export default function MetadataSelectionDialog({
|
||||
onSearch,
|
||||
busy
|
||||
}) {
|
||||
const normalizeWorkflowKind = (value) => {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
if (['film', 'movie', 'feature'].includes(raw)) {
|
||||
return 'film';
|
||||
}
|
||||
if (['series', 'tv', 'season', 'episode'].includes(raw)) {
|
||||
return 'series';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const parseDiscNumber = (rawValue) => {
|
||||
const text = String(rawValue ?? '').trim();
|
||||
if (!/^\d+$/.test(text)) {
|
||||
@@ -36,12 +50,35 @@ export default function MetadataSelectionDialog({
|
||||
const [submitError, setSubmitError] = useState('');
|
||||
const [seasonFilter, setSeasonFilter] = useState('');
|
||||
const [discValidationError, setDiscValidationError] = useState('');
|
||||
const metadataProvider = String(context?.metadataProvider || 'omdb').trim().toLowerCase() || 'omdb';
|
||||
const contextWorkflowKind = normalizeWorkflowKind(
|
||||
context?.selectedMetadata?.workflowKind
|
||||
|| context?.workflowKind
|
||||
|| null
|
||||
);
|
||||
const contextSelectedMetadataProvider = String(
|
||||
context?.selectedMetadata?.metadataProvider
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
const contextMetadataProvider = (
|
||||
contextWorkflowKind === 'series'
|
||||
? 'tmdb'
|
||||
: (contextWorkflowKind === 'film'
|
||||
? 'omdb'
|
||||
: (contextSelectedMetadataProvider || String(context?.metadataProvider || 'omdb').trim().toLowerCase()))
|
||||
) || 'omdb';
|
||||
const [selectedMetadataProvider, setSelectedMetadataProvider] = useState(contextMetadataProvider);
|
||||
const mediaProfile = String(
|
||||
context?.mediaProfile
|
||||
|| context?.selectedMetadata?.mediaProfile
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
const metadataProvider = selectedMetadataProvider === 'tmdb' ? 'tmdb' : 'omdb';
|
||||
const providerOptions = mediaProfile === 'dvd'
|
||||
? [
|
||||
{ label: 'Film (OMDb)', value: 'omdb' },
|
||||
{ label: 'Serie (TMDb)', value: 'tmdb' }
|
||||
]
|
||||
: [{ label: contextMetadataProvider === 'tmdb' ? 'TMDb' : 'OMDb', value: contextMetadataProvider }];
|
||||
const providerLabel = metadataProvider === 'tmdb' ? 'TMDb' : 'OMDb';
|
||||
const supportsExternalIdInput = metadataProvider === 'omdb';
|
||||
const requiresDiscNumber = metadataProvider === 'tmdb' && (mediaProfile === 'dvd' || !mediaProfile);
|
||||
@@ -70,6 +107,7 @@ export default function MetadataSelectionDialog({
|
||||
setSubmitError('');
|
||||
setSeasonFilter('');
|
||||
setDiscValidationError('');
|
||||
setSelectedMetadataProvider(contextMetadataProvider);
|
||||
}, [visible, context]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,7 +123,7 @@ export default function MetadataSelectionDialog({
|
||||
if (submitError) {
|
||||
setSubmitError('');
|
||||
}
|
||||
}, [manualDiscNumber, seasonFilter, selected]);
|
||||
}, [manualDiscNumber, seasonFilter, selected, metadataProvider]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const base = Array.isArray(context?.metadataCandidates)
|
||||
@@ -186,6 +224,7 @@ export default function MetadataSelectionDialog({
|
||||
poster: selected.poster && selected.poster !== 'N/A' ? selected.poster : null,
|
||||
fromOmdb: metadataProvider === 'omdb',
|
||||
metadataProvider,
|
||||
workflowKind: metadataProvider === 'tmdb' ? 'series' : 'film',
|
||||
providerId: selected.providerId || null,
|
||||
tmdbId: selected.tmdbId || null,
|
||||
metadataKind: selected.metadataKind || null,
|
||||
@@ -203,6 +242,7 @@ export default function MetadataSelectionDialog({
|
||||
poster: null,
|
||||
fromOmdb: false,
|
||||
metadataProvider,
|
||||
workflowKind: metadataProvider === 'tmdb' ? 'series' : 'film',
|
||||
seasonNumber: null,
|
||||
...(effectiveDiscNumber ? { discNumber: effectiveDiscNumber } : {})
|
||||
};
|
||||
@@ -240,6 +280,25 @@ export default function MetadataSelectionDialog({
|
||||
modal
|
||||
>
|
||||
<div className="search-row">
|
||||
{providerOptions.length > 1 ? (
|
||||
<Dropdown
|
||||
value={metadataProvider}
|
||||
options={providerOptions}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
onChange={(event) => {
|
||||
const nextProvider = String(event?.value || 'omdb').trim().toLowerCase() || 'omdb';
|
||||
setSelectedMetadataProvider(nextProvider);
|
||||
setSelected(null);
|
||||
setExtraResults([]);
|
||||
setSearchError('');
|
||||
setSubmitError('');
|
||||
setDiscValidationError('');
|
||||
}}
|
||||
disabled={searchBusy || busy}
|
||||
style={{ minWidth: '12.5rem' }}
|
||||
/>
|
||||
) : null}
|
||||
<InputText
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
|
||||
@@ -810,7 +810,10 @@ function stripEpisodePartSuffix(value) {
|
||||
if (!source) {
|
||||
return '';
|
||||
}
|
||||
const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)\s*$/i;
|
||||
// Output cleanup for multi-episode labels:
|
||||
// remove "Teil/Part" markers and pure numeric/roman suffixes in
|
||||
// parentheses like "(1)" / "(2)".
|
||||
const suffixPattern = /(?:\s*[-–—:.,]?\s*)(?:(?:\(?\s*(?:teil|part|pt)\s*(?:\d{1,2}|[ivxlcdm]{1,6})(?:\s*(?:\/|von|of)\s*(?:\d{1,2}|[ivxlcdm]{1,6}))?\s*\)?)|\(\s*(?:\d{1,2}|[ivxlcdm]{1,6})\s*\))\s*$/i;
|
||||
let normalized = source;
|
||||
let changed = false;
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
@@ -985,10 +988,10 @@ function buildDvdSeriesEpisodeOutputPathPreview(settings, metadata, title, assig
|
||||
).trim();
|
||||
const multiTemplateRaw = String(
|
||||
settings?.output_template_dvd_series_multi_episode
|
||||
|| '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}'
|
||||
|| '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})'
|
||||
).trim();
|
||||
const singleTemplate = singleTemplateRaw || '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}';
|
||||
const multiTemplate = multiTemplateRaw || '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}';
|
||||
const multiTemplate = multiTemplateRaw || '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})';
|
||||
const template = episodeRange.isMulti ? multiTemplate : singleTemplate;
|
||||
const rendered = renderTemplate(template, {
|
||||
seriesTitle,
|
||||
@@ -1936,15 +1939,56 @@ export default function PipelineStatusCard({
|
||||
|
||||
const handBrakeTitleDecisionRequired = state === 'WAITING_FOR_USER_DECISION'
|
||||
&& Boolean(pipeline?.context?.handBrakeTitleDecisionRequired);
|
||||
const handBrakeDecisionMode = String(pipeline?.context?.handBrakeDecisionMode || '').trim().toLowerCase();
|
||||
const isSeriesPlayAllDecisionMode = handBrakeDecisionMode === 'series_playall_or_double';
|
||||
const handBrakeDecisionSummary = pipeline?.context?.handBrakeDecisionSummary
|
||||
&& typeof pipeline.context.handBrakeDecisionSummary === 'object'
|
||||
? pipeline.context.handBrakeDecisionSummary
|
||||
: null;
|
||||
const ambiguousLongestTitleId = normalizeTitleId(handBrakeDecisionSummary?.longestCandidateTitleId);
|
||||
const handBrakeDecisionCandidateTitleIds = useMemo(() => normalizeTitleIdList(
|
||||
(Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
|
||||
? pipeline.context.handBrakeTitleCandidates
|
||||
: []
|
||||
).map((item) => item?.handBrakeTitleId)
|
||||
), [pipeline?.context?.handBrakeTitleCandidates]);
|
||||
const handBrakeDecisionSelectableTitleIds = useMemo(() => {
|
||||
const explicit = normalizeTitleIdList(pipeline?.context?.handBrakeDecisionSelectableTitleIds);
|
||||
if (explicit.length > 0) {
|
||||
return explicit;
|
||||
}
|
||||
if (isSeriesPlayAllDecisionMode && ambiguousLongestTitleId) {
|
||||
return [ambiguousLongestTitleId];
|
||||
}
|
||||
return handBrakeDecisionCandidateTitleIds;
|
||||
}, [
|
||||
isSeriesPlayAllDecisionMode,
|
||||
ambiguousLongestTitleId,
|
||||
handBrakeDecisionCandidateTitleIds,
|
||||
pipeline?.context?.handBrakeDecisionSelectableTitleIds
|
||||
]);
|
||||
const handBrakeDecisionSelectableTitleIdSet = useMemo(
|
||||
() => new Set(handBrakeDecisionSelectableTitleIds.map((id) => Number(id))),
|
||||
[handBrakeDecisionSelectableTitleIds]
|
||||
);
|
||||
|
||||
const waitingHandBrakeTitleRows = useMemo(() => {
|
||||
const raw = Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
|
||||
const rawCandidates = Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
|
||||
? pipeline.context.handBrakeTitleCandidates
|
||||
: [];
|
||||
return raw
|
||||
const rawAllTitles = isSeriesPlayAllDecisionMode && Array.isArray(pipeline?.context?.handBrakeAllTitles)
|
||||
? pipeline.context.handBrakeAllTitles
|
||||
: [];
|
||||
const sourceRows = rawAllTitles.length > 0 ? rawAllTitles : rawCandidates;
|
||||
const candidateIdSet = new Set(
|
||||
normalizeTitleIdList(rawCandidates.map((item) => item?.handBrakeTitleId)).map((id) => Number(id))
|
||||
);
|
||||
return sourceRows
|
||||
.map((item) => {
|
||||
const titleId = Number(item?.handBrakeTitleId);
|
||||
if (!Number.isFinite(titleId) || titleId <= 0) return null;
|
||||
const titleId = normalizeTitleId(item?.handBrakeTitleId);
|
||||
if (!titleId) {
|
||||
return null;
|
||||
}
|
||||
const durationSeconds = Number(item?.durationSeconds || 0);
|
||||
const minutes = Math.floor(durationSeconds / 60);
|
||||
const seconds = durationSeconds % 60;
|
||||
@@ -1956,12 +2000,23 @@ export default function PipelineStatusCard({
|
||||
durationMinutes: Number(item?.durationMinutes || 0),
|
||||
audioTrackCount: Number(item?.audioTrackCount || 0),
|
||||
subtitleTrackCount: Number(item?.subtitleTrackCount || 0),
|
||||
sizeBytes: Number(item?.sizeBytes || 0)
|
||||
sizeBytes: Number(item?.sizeBytes || 0),
|
||||
decisionCandidate: candidateIdSet.has(Number(titleId))
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.handBrakeTitleId - b.handBrakeTitleId);
|
||||
}, [pipeline?.context?.handBrakeTitleCandidates]);
|
||||
}, [
|
||||
isSeriesPlayAllDecisionMode,
|
||||
pipeline?.context?.handBrakeAllTitles,
|
||||
pipeline?.context?.handBrakeTitleCandidates
|
||||
]);
|
||||
const visibleWaitingHandBrakeTitleRows = useMemo(() => {
|
||||
if (!isSeriesPlayAllDecisionMode) {
|
||||
return waitingHandBrakeTitleRows;
|
||||
}
|
||||
return waitingHandBrakeTitleRows.filter((row) => row.decisionCandidate);
|
||||
}, [isSeriesPlayAllDecisionMode, waitingHandBrakeTitleRows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!handBrakeTitleDecisionRequired) {
|
||||
@@ -1984,11 +2039,24 @@ export default function PipelineStatusCard({
|
||||
setSelectedHandBrakeTitleIdsState([Math.trunc(singleContextId)]);
|
||||
return;
|
||||
}
|
||||
const best = waitingHandBrakeTitleRows[0]?.handBrakeTitleId || null;
|
||||
setSelectedHandBrakeTitleIdsState(best ? [best] : []);
|
||||
if (isSeriesPlayAllDecisionMode) {
|
||||
const lockedDefaultIds = waitingHandBrakeTitleRows
|
||||
.filter((row) => row.decisionCandidate && !handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId)))
|
||||
.map((row) => row.handBrakeTitleId);
|
||||
if (lockedDefaultIds.length > 0) {
|
||||
setSelectedHandBrakeTitleIdsState(normalizeTitleIdList(lockedDefaultIds));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const firstSelectable = waitingHandBrakeTitleRows.find((row) => (
|
||||
!isSeriesPlayAllDecisionMode || handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId))
|
||||
))?.handBrakeTitleId || null;
|
||||
setSelectedHandBrakeTitleIdsState(firstSelectable ? [firstSelectable] : []);
|
||||
}, [
|
||||
handBrakeTitleDecisionRequired,
|
||||
isSeriesPlayAllDecisionMode,
|
||||
retryJobId,
|
||||
handBrakeDecisionSelectableTitleIdSet,
|
||||
waitingHandBrakeTitleRows,
|
||||
pipeline?.context?.selectedHandBrakeTitleId,
|
||||
pipeline?.context?.selectedHandBrakeTitleIds
|
||||
@@ -2849,7 +2917,7 @@ export default function PipelineStatusCard({
|
||||
|
||||
{handBrakeTitleDecisionRequired && retryJobId && (
|
||||
<Button
|
||||
label="DVD Titel bestätigen"
|
||||
label={isSeriesPlayAllDecisionMode ? 'PlayAll/Doppelfolge bestätigen' : 'DVD Titel bestätigen'}
|
||||
icon="pi pi-check"
|
||||
severity="warning"
|
||||
outlined
|
||||
@@ -3077,20 +3145,39 @@ export default function PipelineStatusCard({
|
||||
|
||||
{handBrakeTitleDecisionRequired && !queueLocked ? (
|
||||
<div className="playlist-decision-block">
|
||||
<h3>DVD Titel-Auswahl erforderlich</h3>
|
||||
<h3>{isSeriesPlayAllDecisionMode ? 'Serien-Grenzfall: PlayAll oder Doppelfolge?' : 'DVD Titel-Auswahl erforderlich'}</h3>
|
||||
<small>
|
||||
Mehrere DVD-Titel gefunden, die die Mindestlänge erfüllen. Bitte einen oder mehrere Episoden-Titel auswählen.
|
||||
{isSeriesPlayAllDecisionMode
|
||||
? 'Min. ein Titel passt zeitlich zur Summe der kürzeren Episoden. Bitte durch Auswahl entscheiden, ob PlayAll Multi-Episode (nicht anhaken) oder Doppelfolge (anhaken).'
|
||||
: 'Mehrere DVD-Titel gefunden, die die Mindestlänge erfüllen. Bitte einen oder mehrere Episoden-Titel auswählen.'}
|
||||
</small>
|
||||
{waitingHandBrakeTitleRows.length > 0 ? (
|
||||
{visibleWaitingHandBrakeTitleRows.length > 0 ? (
|
||||
<div className="playlist-decision-list">
|
||||
{waitingHandBrakeTitleRows.map((row) => (
|
||||
{visibleWaitingHandBrakeTitleRows.map((row) => {
|
||||
const isDecisionSelectable = !isSeriesPlayAllDecisionMode
|
||||
|| handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId));
|
||||
const isAmbiguousDecisionTitle = isSeriesPlayAllDecisionMode
|
||||
&& isDecisionSelectable
|
||||
&& (
|
||||
Number(row.handBrakeTitleId) === Number(ambiguousLongestTitleId)
|
||||
|| handBrakeDecisionSelectableTitleIdSet.has(Number(row.handBrakeTitleId))
|
||||
);
|
||||
const isRecognizedLocked = isSeriesPlayAllDecisionMode
|
||||
&& row.decisionCandidate
|
||||
&& !isDecisionSelectable;
|
||||
const isOutOfDecisionScope = isSeriesPlayAllDecisionMode
|
||||
&& !row.decisionCandidate;
|
||||
return (
|
||||
<div key={row.handBrakeTitleId} className="playlist-decision-item">
|
||||
<label className="readonly-check-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedHandBrakeTitleIdsState.includes(row.handBrakeTitleId)}
|
||||
disabled={queueLocked}
|
||||
disabled={queueLocked || !isDecisionSelectable}
|
||||
onChange={() => {
|
||||
if (!isDecisionSelectable) {
|
||||
return;
|
||||
}
|
||||
setSelectedHandBrakeTitleIdsState((prev) => {
|
||||
const list = Array.isArray(prev) ? prev : [];
|
||||
if (list.includes(row.handBrakeTitleId)) {
|
||||
@@ -3101,14 +3188,18 @@ export default function PipelineStatusCard({
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{`Titel -t ${row.handBrakeTitleId}`}
|
||||
{isAmbiguousDecisionTitle ? <strong>{`Titel -t ${row.handBrakeTitleId}`}</strong> : `Titel -t ${row.handBrakeTitleId}`}
|
||||
{isAmbiguousDecisionTitle ? ' | Grenzfalltitel (PlayAll/Doppelfolge)' : ''}
|
||||
{isRecognizedLocked ? ' | automatisch erkannt (fix)' : ''}
|
||||
{isOutOfDecisionScope ? ' | nicht entscheidungsrelevant' : ''}
|
||||
{row.durationLabel ? ` | ${row.durationLabel}` : ''}
|
||||
{row.audioTrackCount > 0 ? ` | Audio: ${row.audioTrackCount}` : ''}
|
||||
{row.subtitleTrackCount > 0 ? ` | Untertitel: ${row.subtitleTrackCount}` : ''}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<small>Keine Kandidaten verfügbar. Bitte Analyse erneut ausführen.</small>
|
||||
@@ -3144,7 +3235,7 @@ export default function PipelineStatusCard({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) ? (
|
||||
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) && !(handBrakeTitleDecisionRequired && isSeriesPlayAllDecisionMode) ? (
|
||||
<div className="mediainfo-review-block">
|
||||
{!isDvdTitleSelectionRequired ? <h3>Titel-/Spurprüfung</h3> : null}
|
||||
{state === 'READY_TO_ENCODE' && !queueLocked && !isDvdTitleSelectionRequired ? (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Column } from 'primereact/column';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '../api/client';
|
||||
import { confirmModal } from '../utils/confirmModal';
|
||||
|
||||
@@ -26,6 +27,7 @@ function formatDetectedMediaType(value) {
|
||||
}
|
||||
|
||||
export default function DatabasePage() {
|
||||
const navigate = useNavigate();
|
||||
const [orphanRows, setOrphanRows] = useState([]);
|
||||
const [orphanLoading, setOrphanLoading] = useState(false);
|
||||
const [orphanImportBusyPath, setOrphanImportBusyPath] = useState(null);
|
||||
@@ -63,13 +65,21 @@ export default function DatabasePage() {
|
||||
try {
|
||||
const response = await api.importOrphanRawFolder(row.rawPath);
|
||||
const newJobId = response?.job?.id;
|
||||
const activationStarted = Boolean(response?.activation?.started);
|
||||
toastRef.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Job angelegt',
|
||||
detail: newJobId ? `Historieneintrag #${newJobId} erstellt.` : 'Historieneintrag wurde erstellt.',
|
||||
summary: activationStarted ? 'Analyse gestartet' : 'Job angelegt',
|
||||
detail: activationStarted
|
||||
? (newJobId
|
||||
? `Job #${newJobId} läuft jetzt im Ripper-View.`
|
||||
: 'Job läuft jetzt im Ripper-View.')
|
||||
: (newJobId ? `Historieneintrag #${newJobId} erstellt.` : 'Historieneintrag wurde erstellt.'),
|
||||
life: 3500
|
||||
});
|
||||
await loadOrphans();
|
||||
if (activationStarted) {
|
||||
navigate('/ripper');
|
||||
}
|
||||
} catch (error) {
|
||||
toastRef.current?.show({
|
||||
severity: 'error',
|
||||
|
||||
@@ -1112,8 +1112,20 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
: (makemkvInfo?.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
|
||||
? makemkvInfo.selectedMetadata
|
||||
: {});
|
||||
const workflowKindRaw = String(
|
||||
selectedMetadata?.workflowKind
|
||||
|| analyzeContext?.workflowKind
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
const workflowKind = ['film', 'movie', 'feature'].includes(workflowKindRaw)
|
||||
? 'film'
|
||||
: (['series', 'tv', 'season', 'episode'].includes(workflowKindRaw) ? 'series' : null);
|
||||
const isSeriesDvd = resolveMediaType(row) === 'dvd' && isSeriesDvdJob(row);
|
||||
const metadataProvider = isSeriesDvd ? 'tmdb' : 'omdb';
|
||||
const metadataProvider = workflowKind === 'series'
|
||||
? 'tmdb'
|
||||
: (workflowKind === 'film'
|
||||
? 'omdb'
|
||||
: (String(selectedMetadata?.metadataProvider || analyzeContext?.metadataProvider || (isSeriesDvd ? 'tmdb' : 'omdb')).trim().toLowerCase() || 'omdb'));
|
||||
const seasonNumber = selectedMetadata?.seasonNumber ?? analyzeContext?.seriesLookupHint?.seasonNumber ?? null;
|
||||
const discNumber = selectedMetadata?.discNumber ?? analyzeContext?.seriesLookupHint?.discNumber ?? null;
|
||||
const context = {
|
||||
|
||||
@@ -1352,7 +1352,7 @@ export default function RipperPage({
|
||||
jobId: normalizedJobId,
|
||||
detectedTitle: context?.detectedTitle || job?.detected_title || selectedMetadata?.title || '',
|
||||
selectedMetadata,
|
||||
metadataProvider: context?.metadataProvider || selectedMetadata?.metadataProvider || 'omdb',
|
||||
metadataProvider: selectedMetadata?.metadataProvider || context?.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(context?.metadataCandidates) ? context.metadataCandidates : [],
|
||||
seriesAnalysis: context?.seriesAnalysis || null,
|
||||
seriesLookupHint: context?.seriesLookupHint || null,
|
||||
@@ -1382,7 +1382,7 @@ export default function RipperPage({
|
||||
seasonNumber: null,
|
||||
discNumber: null
|
||||
},
|
||||
metadataProvider: currentContext?.metadataProvider || currentContext?.selectedMetadata?.metadataProvider || 'omdb',
|
||||
metadataProvider: currentContext?.selectedMetadata?.metadataProvider || currentContext?.metadataProvider || 'omdb',
|
||||
metadataCandidates: Array.isArray(currentContext?.metadataCandidates) ? currentContext.metadataCandidates : [],
|
||||
seriesAnalysis: currentContext?.seriesAnalysis || null,
|
||||
seriesLookupHint: currentContext?.seriesLookupHint || null,
|
||||
|
||||
@@ -217,6 +217,17 @@ export function isSeriesDvdJob(job) {
|
||||
: (job?.makemkvInfo?.selectedMetadata && typeof job.makemkvInfo.selectedMetadata === 'object'
|
||||
? job.makemkvInfo.selectedMetadata
|
||||
: {});
|
||||
const workflowKind = String(
|
||||
selectedMetadata?.workflowKind
|
||||
|| analyzeContext?.workflowKind
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
if (['film', 'movie', 'feature'].includes(workflowKind)) {
|
||||
return false;
|
||||
}
|
||||
if (['series', 'tv', 'season', 'episode'].includes(workflowKind)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const metadataProvider = String(
|
||||
analyzeContext?.metadataProvider
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster",
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"private": true,
|
||||
"version": "0.13.1-4",
|
||||
"version": "0.13.1-6",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||
"dev:backend": "npm run dev --prefix backend",
|
||||
|
||||
Reference in New Issue
Block a user