0.15.0-3 Misc Fixes
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"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.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -941,6 +941,26 @@ async function migrateSettingsSchemaMetadata(db) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metadataReadyNotifyLabel = 'Bei manueller Auswahl senden';
|
||||||
|
const metadataReadyNotifyDescription = 'Sendet, wenn ein Job eine manuelle Auswahl/Entscheidung benötigt (z.B. Metadaten, Titel oder Playlist).';
|
||||||
|
const metadataReadyNotifyResult = await db.run(
|
||||||
|
`UPDATE settings_schema
|
||||||
|
SET label = ?, description = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE key = 'pushover_notify_metadata_ready' AND (label != ? OR description != ?)`,
|
||||||
|
[
|
||||||
|
metadataReadyNotifyLabel,
|
||||||
|
metadataReadyNotifyDescription,
|
||||||
|
metadataReadyNotifyLabel,
|
||||||
|
metadataReadyNotifyDescription
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (metadataReadyNotifyResult?.changes > 0) {
|
||||||
|
logger.info('migrate:settings-schema-metadata-ready-notify-updated', {
|
||||||
|
key: 'pushover_notify_metadata_ready',
|
||||||
|
label: metadataReadyNotifyLabel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const dvdSeriesMultiEpisodeTemplateDefault = '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})';
|
const dvdSeriesMultiEpisodeTemplateDefault = '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})';
|
||||||
const legacyDvdSeriesMultiEpisodeTemplateDefaults = [
|
const legacyDvdSeriesMultiEpisodeTemplateDefaults = [
|
||||||
'{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}'
|
'{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeRange}'
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ class VideoDiscPlugin extends SourcePlugin {
|
|||||||
selectedTitleId = null,
|
selectedTitleId = null,
|
||||||
backupOutputBase = null,
|
backupOutputBase = null,
|
||||||
disableMinLengthFilter = false,
|
disableMinLengthFilter = false,
|
||||||
|
sourceArgOverride = null,
|
||||||
isCancelled,
|
isCancelled,
|
||||||
onProcessHandle
|
onProcessHandle
|
||||||
} = ctx.extra || {};
|
} = ctx.extra || {};
|
||||||
@@ -249,7 +250,8 @@ class VideoDiscPlugin extends SourcePlugin {
|
|||||||
mediaProfile: this.mediaProfile,
|
mediaProfile: this.mediaProfile,
|
||||||
settingsMap: settings,
|
settingsMap: settings,
|
||||||
backupOutputBase,
|
backupOutputBase,
|
||||||
disableMinLengthFilter
|
disableMinLengthFilter,
|
||||||
|
sourceArgOverride
|
||||||
});
|
});
|
||||||
|
|
||||||
const ripMode = String(ripConfig.ripMode || settings.makemkv_rip_mode || 'mkv')
|
const ripMode = String(ripConfig.ripMode || settings.makemkv_rip_mode || 'mkv')
|
||||||
@@ -262,7 +264,8 @@ class VideoDiscPlugin extends SourcePlugin {
|
|||||||
ripMode,
|
ripMode,
|
||||||
rawJobDir,
|
rawJobDir,
|
||||||
selectedTitleId,
|
selectedTitleId,
|
||||||
disableMinLengthFilter
|
disableMinLengthFilter,
|
||||||
|
sourceArgOverride: sourceArgOverride || null
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.emitProgress(0, ripMode === 'backup'
|
ctx.emitProgress(0, ripMode === 'backup'
|
||||||
|
|||||||
@@ -2558,6 +2558,96 @@ function deleteFilesRecursively(rootPath, keepRoot = true) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectFilesRecursively(rootPath) {
|
||||||
|
const normalizedRoot = normalizeComparablePath(rootPath);
|
||||||
|
if (!normalizedRoot) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const result = [];
|
||||||
|
const visit = (currentPath) => {
|
||||||
|
let stat = null;
|
||||||
|
try {
|
||||||
|
stat = fs.lstatSync(currentPath);
|
||||||
|
} catch (_error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
let entries = [];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
||||||
|
} catch (_error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
visit(path.join(currentPath, entry.name));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.push(normalizeComparablePath(currentPath));
|
||||||
|
};
|
||||||
|
visit(normalizedRoot);
|
||||||
|
return Array.from(new Set(result.filter(Boolean))).sort((left, right) => left.localeCompare(right, 'de'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEmptyParentDirectories(startPath, options = {}) {
|
||||||
|
const start = normalizeComparablePath(startPath);
|
||||||
|
if (!start) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const protectedRoots = new Set(
|
||||||
|
(Array.isArray(options?.protectedRoots) ? options.protectedRoots : [])
|
||||||
|
.map((entry) => normalizeComparablePath(entry))
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
const allowedRoots = Array.from(protectedRoots);
|
||||||
|
const removed = [];
|
||||||
|
const removedSet = new Set();
|
||||||
|
|
||||||
|
let current = start;
|
||||||
|
while (current && !isFilesystemRootPath(current)) {
|
||||||
|
if (protectedRoots.has(current)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (allowedRoots.length > 0 && !allowedRoots.some((rootPath) => isPathInside(rootPath, current))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let stat = null;
|
||||||
|
try {
|
||||||
|
stat = fs.lstatSync(current);
|
||||||
|
} catch (_error) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let entries = [];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(current);
|
||||||
|
} catch (_error) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (entries.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fs.rmdirSync(current);
|
||||||
|
if (!removedSet.has(current)) {
|
||||||
|
removedSet.add(current);
|
||||||
|
removed.push(current);
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const parentDir = normalizeComparablePath(path.dirname(current));
|
||||||
|
if (!parentDir || parentDir === current) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = parentDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeJobIdValue(value) {
|
function normalizeJobIdValue(value) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
@@ -6378,7 +6468,6 @@ class HistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const artifactMoviePathSet = new Set(artifactMoviePaths);
|
const artifactMoviePathSet = new Set(artifactMoviePaths);
|
||||||
const includeMovieParentCandidate = resolvedPaths?.mediaType !== 'converter';
|
|
||||||
for (const outputPath of explicitMoviePaths) {
|
for (const outputPath of explicitMoviePaths) {
|
||||||
addCandidate(
|
addCandidate(
|
||||||
movieCandidates,
|
movieCandidates,
|
||||||
@@ -6387,16 +6476,6 @@ class HistoryService {
|
|||||||
artifactMoviePathSet.has(outputPath) ? 'lineage_output_path' : 'output_path',
|
artifactMoviePathSet.has(outputPath) ? 'lineage_output_path' : 'output_path',
|
||||||
movieAllowedPaths
|
movieAllowedPaths
|
||||||
);
|
);
|
||||||
const parentDir = toNormalizedPath(path.dirname(outputPath));
|
|
||||||
if (includeMovieParentCandidate && parentDir && !movieRoots.includes(parentDir)) {
|
|
||||||
addCandidate(
|
|
||||||
movieCandidates,
|
|
||||||
'movie',
|
|
||||||
parentDir,
|
|
||||||
artifactMoviePathSet.has(outputPath) ? 'lineage_output_parent' : 'output_parent',
|
|
||||||
movieAllowedPaths
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedJobId && resolvedPaths?.mediaType !== 'cd') {
|
if (normalizedJobId && resolvedPaths?.mediaType !== 'cd') {
|
||||||
@@ -6505,16 +6584,14 @@ class HistoryService {
|
|||||||
return normalizedSources.includes('output_path') || normalizedSources.includes('lineage_output_path');
|
return normalizedSources.includes('output_path') || normalizedSources.includes('lineage_output_path');
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildList = (target) => Array.from(candidateMap.values())
|
const buildList = (target) => {
|
||||||
.filter((row) => row.target === target)
|
const rows = Array.from(candidateMap.values())
|
||||||
|
.filter((row) => row.target === target);
|
||||||
|
if (target !== 'movie') {
|
||||||
|
return rows
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
const inspection = inspectDeletionPath(row.path);
|
const inspection = inspectDeletionPath(row.path);
|
||||||
const sources = Array.from(row.sources).sort((left, right) => left.localeCompare(right));
|
const sources = Array.from(row.sources).sort((left, right) => left.localeCompare(right));
|
||||||
// Do not expose stale output file paths from DB/lineage in delete preview.
|
|
||||||
// They cannot be deleted anyway and show up as "ghost paths" in UI/QA checks.
|
|
||||||
if (target === 'movie' && !inspection.exists && hasTrackedOutputSource(sources)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
target,
|
target,
|
||||||
path: row.path,
|
path: row.path,
|
||||||
@@ -6527,6 +6604,97 @@ class HistoryService {
|
|||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.sort((left, right) => String(left.path || '').localeCompare(String(right.path || ''), 'de'));
|
.sort((left, right) => String(left.path || '').localeCompare(String(right.path || ''), 'de'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const moviePathMap = new Map();
|
||||||
|
const upsertMoviePath = ({ path: candidatePath, exists, isDirectory, isFile, jobIds, sources }) => {
|
||||||
|
const normalizedPath = normalizeComparablePath(candidatePath);
|
||||||
|
if (!normalizedPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!moviePathMap.has(normalizedPath)) {
|
||||||
|
moviePathMap.set(normalizedPath, {
|
||||||
|
target: 'movie',
|
||||||
|
path: normalizedPath,
|
||||||
|
exists: Boolean(exists),
|
||||||
|
isDirectory: Boolean(isDirectory),
|
||||||
|
isFile: Boolean(isFile),
|
||||||
|
jobIds: new Set(),
|
||||||
|
sources: new Set()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const row = moviePathMap.get(normalizedPath);
|
||||||
|
row.exists = row.exists || Boolean(exists);
|
||||||
|
row.isDirectory = row.isDirectory || Boolean(isDirectory);
|
||||||
|
row.isFile = row.isFile || Boolean(isFile);
|
||||||
|
for (const jobId of (Array.isArray(jobIds) ? jobIds : [])) {
|
||||||
|
const normalizedJobId = normalizeJobIdValue(jobId);
|
||||||
|
if (normalizedJobId) {
|
||||||
|
row.jobIds.add(normalizedJobId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const source of (Array.isArray(sources) ? sources : [])) {
|
||||||
|
const normalizedSource = String(source || '').trim();
|
||||||
|
if (normalizedSource) {
|
||||||
|
row.sources.add(normalizedSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const inspection = inspectDeletionPath(row.path);
|
||||||
|
const sources = Array.from(row.sources).sort((left, right) => left.localeCompare(right));
|
||||||
|
// Do not expose stale output paths from DB/lineage in delete preview.
|
||||||
|
if (!inspection.exists && hasTrackedOutputSource(sources)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inspection.exists) {
|
||||||
|
upsertMoviePath({
|
||||||
|
path: row.path,
|
||||||
|
exists: false,
|
||||||
|
isDirectory: false,
|
||||||
|
isFile: true,
|
||||||
|
jobIds: Array.from(row.jobIds),
|
||||||
|
sources
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inspection.isDirectory) {
|
||||||
|
const files = collectFilesRecursively(inspection.path);
|
||||||
|
for (const filePath of files) {
|
||||||
|
upsertMoviePath({
|
||||||
|
path: filePath,
|
||||||
|
exists: true,
|
||||||
|
isDirectory: false,
|
||||||
|
isFile: true,
|
||||||
|
jobIds: Array.from(row.jobIds),
|
||||||
|
sources
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
upsertMoviePath({
|
||||||
|
path: inspection.path,
|
||||||
|
exists: true,
|
||||||
|
isDirectory: false,
|
||||||
|
isFile: true,
|
||||||
|
jobIds: Array.from(row.jobIds),
|
||||||
|
sources
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(moviePathMap.values())
|
||||||
|
.map((row) => ({
|
||||||
|
target: row.target,
|
||||||
|
path: row.path,
|
||||||
|
exists: Boolean(row.exists),
|
||||||
|
isDirectory: false,
|
||||||
|
isFile: true,
|
||||||
|
jobIds: Array.from(row.jobIds).sort((left, right) => left - right),
|
||||||
|
sources: Array.from(row.sources).sort((left, right) => left.localeCompare(right))
|
||||||
|
}))
|
||||||
|
.sort((left, right) => String(left.path || '').localeCompare(String(right.path || ''), 'de'));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pathCandidates: {
|
pathCandidates: {
|
||||||
@@ -6650,7 +6818,7 @@ class HistoryService {
|
|||||||
if (targetKey === 'raw' && selectedRawPathFilter.size > 0) {
|
if (targetKey === 'raw' && selectedRawPathFilter.size > 0) {
|
||||||
summary[targetKey].reason = 'Keine ausgewählten RAW-Dateien/Ordner gefunden.';
|
summary[targetKey].reason = 'Keine ausgewählten RAW-Dateien/Ordner gefunden.';
|
||||||
} else if (targetKey === 'movie' && selectedMoviePathFilter.size > 0) {
|
} else if (targetKey === 'movie' && selectedMoviePathFilter.size > 0) {
|
||||||
summary[targetKey].reason = 'Keine ausgewählten Audio/Video-Ordner gefunden.';
|
summary[targetKey].reason = 'Keine ausgewählten Audio/Video-Dateien gefunden.';
|
||||||
} else {
|
} else {
|
||||||
summary[targetKey].reason = 'Keine passenden Dateien/Ordner gefunden.';
|
summary[targetKey].reason = 'Keine passenden Dateien/Ordner gefunden.';
|
||||||
}
|
}
|
||||||
@@ -6678,6 +6846,9 @@ class HistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (inspection.isDirectory) {
|
if (inspection.isDirectory) {
|
||||||
|
if (targetKey === 'movie') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const keepRoot = protectedRoots.has(inspection.path);
|
const keepRoot = protectedRoots.has(inspection.path);
|
||||||
const result = deleteFilesRecursively(inspection.path, keepRoot);
|
const result = deleteFilesRecursively(inspection.path, keepRoot);
|
||||||
const filesDeleted = Number(result?.filesDeleted || 0);
|
const filesDeleted = Number(result?.filesDeleted || 0);
|
||||||
@@ -6709,6 +6880,23 @@ class HistoryService {
|
|||||||
keepRoot: false,
|
keepRoot: false,
|
||||||
jobIds: Array.isArray(candidate?.jobIds) ? candidate.jobIds : []
|
jobIds: Array.isArray(candidate?.jobIds) ? candidate.jobIds : []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (targetKey === 'movie') {
|
||||||
|
const parentDir = normalizeComparablePath(path.dirname(inspection.path));
|
||||||
|
const removedParentDirs = removeEmptyParentDirectories(parentDir, {
|
||||||
|
protectedRoots: Array.from(protectedRoots)
|
||||||
|
});
|
||||||
|
for (const removedPath of removedParentDirs) {
|
||||||
|
summary[targetKey].dirsRemoved += 1;
|
||||||
|
summary.deletedPaths.push({
|
||||||
|
target: targetKey,
|
||||||
|
path: removedPath,
|
||||||
|
type: 'directory',
|
||||||
|
keepRoot: false,
|
||||||
|
jobIds: Array.isArray(candidate?.jobIds) ? candidate.jobIds : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
summary[targetKey].deleted = summary[targetKey].pathsDeleted > 0
|
summary[targetKey].deleted = summary[targetKey].pathsDeleted > 0
|
||||||
@@ -6810,38 +6998,10 @@ class HistoryService {
|
|||||||
));
|
));
|
||||||
|
|
||||||
if (summary.movie?.deleted) {
|
if (summary.movie?.deleted) {
|
||||||
const movieDeleteEntries = (Array.isArray(summary?.deletedPaths) ? summary.deletedPaths : [])
|
await this.removeMissingJobOutputFoldersFromJobs(
|
||||||
.filter((entry) => String(entry?.target || '').trim().toLowerCase() === 'movie')
|
relatedJobIds.length > 0 ? relatedJobIds : [normalizedJobId]
|
||||||
.map((entry) => ({
|
|
||||||
path: normalizeComparablePath(entry?.path),
|
|
||||||
type: String(entry?.type || '').trim().toLowerCase()
|
|
||||||
}))
|
|
||||||
.filter((entry) => Boolean(entry.path));
|
|
||||||
const previewMoviePaths = (Array.isArray(preview?.pathCandidates?.movie) ? preview.pathCandidates.movie : [])
|
|
||||||
.map((candidate) => normalizeComparablePath(candidate?.path))
|
|
||||||
.filter(Boolean);
|
|
||||||
const cleanupPaths = new Set();
|
|
||||||
|
|
||||||
for (const deletedEntry of movieDeleteEntries) {
|
|
||||||
cleanupPaths.add(deletedEntry.path);
|
|
||||||
for (const candidatePath of previewMoviePaths) {
|
|
||||||
if (deletedEntry.type === 'directory') {
|
|
||||||
if (isPathInside(deletedEntry.path, candidatePath)) {
|
|
||||||
cleanupPaths.add(candidatePath);
|
|
||||||
}
|
|
||||||
} else if (candidatePath === deletedEntry.path) {
|
|
||||||
cleanupPaths.add(candidatePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const deletedPath of cleanupPaths) {
|
|
||||||
await this.removeJobOutputFolderFromJobs(
|
|
||||||
relatedJobIds.length > 0 ? relatedJobIds : [normalizedJobId],
|
|
||||||
deletedPath
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await this.appendLog(
|
await this.appendLog(
|
||||||
normalizedJobId,
|
normalizedJobId,
|
||||||
@@ -7340,6 +7500,57 @@ class HistoryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeMissingJobOutputFoldersFromJobs(jobIds = []) {
|
||||||
|
const normalizedJobIds = Array.isArray(jobIds)
|
||||||
|
? jobIds
|
||||||
|
.map((value) => normalizeJobIdValue(value))
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
if (normalizedJobIds.length === 0) {
|
||||||
|
return { removed: 0, removedPaths: [] };
|
||||||
|
}
|
||||||
|
const db = await getDb();
|
||||||
|
const placeholders = normalizedJobIds.map(() => '?').join(', ');
|
||||||
|
const rows = await db.all(
|
||||||
|
`SELECT id, output_path FROM job_output_folders WHERE job_id IN (${placeholders})`,
|
||||||
|
normalizedJobIds
|
||||||
|
);
|
||||||
|
const removedPaths = [];
|
||||||
|
for (const row of (Array.isArray(rows) ? rows : [])) {
|
||||||
|
const normalizedPath = normalizeComparablePath(row?.output_path);
|
||||||
|
let removeEntry = false;
|
||||||
|
if (!normalizedPath) {
|
||||||
|
removeEntry = true;
|
||||||
|
} else if (!fs.existsSync(normalizedPath)) {
|
||||||
|
removeEntry = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const stat = fs.lstatSync(normalizedPath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
const entries = fs.readdirSync(normalizedPath);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
fs.rmdirSync(normalizedPath);
|
||||||
|
removeEntry = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
removeEntry = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!removeEntry) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await db.run('DELETE FROM job_output_folders WHERE id = ?', [Number(row.id)]);
|
||||||
|
if (normalizedPath) {
|
||||||
|
removedPaths.push(normalizedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
removed: removedPaths.length,
|
||||||
|
removedPaths: Array.from(new Set(removedPaths)).sort((left, right) => left.localeCompare(right, 'de'))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async deleteSpecificOutputFolders(jobId, folderPaths = []) {
|
async deleteSpecificOutputFolders(jobId, folderPaths = []) {
|
||||||
const paths = Array.isArray(folderPaths) ? folderPaths.filter((p) => typeof p === 'string' && p.trim()) : [];
|
const paths = Array.isArray(folderPaths) ? folderPaths.filter((p) => typeof p === 'string' && p.trim()) : [];
|
||||||
if (paths.length === 0) return { deleted: [], failed: [] };
|
if (paths.length === 0) return { deleted: [], failed: [] };
|
||||||
@@ -7621,6 +7832,9 @@ class HistoryService {
|
|||||||
selectedRawPaths: selectedRawPathsForDelete,
|
selectedRawPaths: selectedRawPathsForDelete,
|
||||||
selectedMoviePaths: selectedMoviePathsForDelete
|
selectedMoviePaths: selectedMoviePathsForDelete
|
||||||
});
|
});
|
||||||
|
if (fileSummary?.movie?.deleted) {
|
||||||
|
await this.removeMissingJobOutputFoldersFromJobs([normalizedJobId]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
@@ -7798,6 +8012,9 @@ class HistoryService {
|
|||||||
selectedRawPaths: selectedRawPathsForDelete,
|
selectedRawPaths: selectedRawPathsForDelete,
|
||||||
selectedMoviePaths: selectedMoviePathsForDelete
|
selectedMoviePaths: selectedMoviePathsForDelete
|
||||||
});
|
});
|
||||||
|
if (fileSummary?.movie?.deleted) {
|
||||||
|
await this.removeMissingJobOutputFoldersFromJobs(deleteJobIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.exec('BEGIN');
|
await db.exec('BEGIN');
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const logger = require('./logger').child('PIPELINE');
|
|||||||
const { spawnTrackedProcess } = require('./processRunner');
|
const { spawnTrackedProcess } = require('./processRunner');
|
||||||
const { parseMakeMkvProgress, parseHandBrakeProgress, parseMkvmergeProgress } = require('../utils/progressParsers');
|
const { parseMakeMkvProgress, parseHandBrakeProgress, parseMkvmergeProgress } = require('../utils/progressParsers');
|
||||||
const { ensureDir, sanitizeFileName, sanitizeFileNameWithExtension, renderTemplate, findMediaFiles } = require('../utils/files');
|
const { ensureDir, sanitizeFileName, sanitizeFileNameWithExtension, renderTemplate, findMediaFiles } = require('../utils/files');
|
||||||
const { buildMediainfoReview } = require('../utils/encodePlan');
|
const { buildMediainfoReview, refreshAudioTrackActionsForPlanTitles } = require('../utils/encodePlan');
|
||||||
const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis');
|
const { analyzePlaylistObfuscation, normalizePlaylistId } = require('../utils/playlistAnalysis');
|
||||||
const { errorToMeta } = require('../utils/errorMeta');
|
const { errorToMeta } = require('../utils/errorMeta');
|
||||||
const userPresetService = require('./userPresetService');
|
const userPresetService = require('./userPresetService');
|
||||||
@@ -242,11 +242,32 @@ function resolveDiscNumberFromJobLike(job = null) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeContextJobId(value) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.trunc(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveSelectedMetadataForJob(job = null, analyzeContext = null, activeContext = null) {
|
function resolveSelectedMetadataForJob(job = null, analyzeContext = null, activeContext = null) {
|
||||||
|
const jobId = normalizeContextJobId(job?.id ?? job?.jobId ?? null);
|
||||||
|
const activeContextJobId = normalizeContextJobId(
|
||||||
|
activeContext?.jobId
|
||||||
|
?? activeContext?.activeJobId
|
||||||
|
?? null
|
||||||
|
);
|
||||||
|
const allowActiveContextMetadata = (
|
||||||
|
jobId === null
|
||||||
|
? true
|
||||||
|
: (activeContextJobId !== null && activeContextJobId === jobId)
|
||||||
|
);
|
||||||
const analyzeSelectedMetadata = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
|
const analyzeSelectedMetadata = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
|
||||||
? analyzeContext.selectedMetadata
|
? analyzeContext.selectedMetadata
|
||||||
: {};
|
: {};
|
||||||
const activeSelectedMetadata = activeContext?.selectedMetadata && typeof activeContext.selectedMetadata === 'object'
|
const activeSelectedMetadata = allowActiveContextMetadata
|
||||||
|
&& activeContext?.selectedMetadata
|
||||||
|
&& typeof activeContext.selectedMetadata === 'object'
|
||||||
? activeContext.selectedMetadata
|
? activeContext.selectedMetadata
|
||||||
: {};
|
: {};
|
||||||
const merged = {
|
const merged = {
|
||||||
@@ -256,7 +277,7 @@ function resolveSelectedMetadataForJob(job = null, analyzeContext = null, active
|
|||||||
const workflowKind = normalizeDvdMetadataWorkflowKind(
|
const workflowKind = normalizeDvdMetadataWorkflowKind(
|
||||||
merged.workflowKind
|
merged.workflowKind
|
||||||
|| analyzeContext?.workflowKind
|
|| analyzeContext?.workflowKind
|
||||||
|| activeContext?.workflowKind
|
|| (allowActiveContextMetadata ? activeContext?.workflowKind : null)
|
||||||
|| null
|
|| null
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -270,7 +291,7 @@ function resolveSelectedMetadataForJob(job = null, analyzeContext = null, active
|
|||||||
metadataProvider: String(
|
metadataProvider: String(
|
||||||
merged.metadataProvider
|
merged.metadataProvider
|
||||||
|| analyzeContext?.metadataProvider
|
|| analyzeContext?.metadataProvider
|
||||||
|| activeContext?.metadataProvider
|
|| (allowActiveContextMetadata ? activeContext?.metadataProvider : null)
|
||||||
|| 'omdb'
|
|| 'omdb'
|
||||||
).trim().toLowerCase() || 'omdb'
|
).trim().toLowerCase() || 'omdb'
|
||||||
};
|
};
|
||||||
@@ -11382,7 +11403,17 @@ class PipelineService extends EventEmitter {
|
|||||||
.filter((row) => Number(row?.id) > 0 && !this.isContainerHistoryJob(row))
|
.filter((row) => Number(row?.id) > 0 && !this.isContainerHistoryJob(row))
|
||||||
.filter((row) => {
|
.filter((row) => {
|
||||||
if (isSeriesContainer) {
|
if (isSeriesContainer) {
|
||||||
return !isSeriesBatchEpisodeSubJobRow(row);
|
const encodePlan = row?.encodePlan && typeof row.encodePlan === 'object'
|
||||||
|
? row.encodePlan
|
||||||
|
: this.safeParseJson(row?.encode_plan_json);
|
||||||
|
const rowJobKind = String(row?.job_kind || row?.jobKind || '').trim().toLowerCase();
|
||||||
|
const isSeriesEpisodeSubJob = (
|
||||||
|
isSeriesBatchChildPlan(encodePlan)
|
||||||
|
|| Boolean(encodePlan?.seriesBatchVirtualEpisode)
|
||||||
|
|| rowJobKind === 'dvd_series_episode'
|
||||||
|
|| rowJobKind === 'dvd_series_virtual_episode'
|
||||||
|
);
|
||||||
|
return !isSeriesEpisodeSubJob;
|
||||||
}
|
}
|
||||||
if (isMultipartContainer) {
|
if (isMultipartContainer) {
|
||||||
return this.isMultipartMovieDiscChildHistoryJob(row) || this.isMultipartMovieMergeHistoryJob(row);
|
return this.isMultipartMovieDiscChildHistoryJob(row) || this.isMultipartMovieMergeHistoryJob(row);
|
||||||
@@ -16035,7 +16066,7 @@ class PipelineService extends EventEmitter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let effectiveDetectedTitle = detectedTitle;
|
let effectiveDetectedTitle = detectedTitle;
|
||||||
let omdbCandidates = null;
|
let omdbCandidates = [];
|
||||||
let metadataCandidates = null;
|
let metadataCandidates = null;
|
||||||
let metadataProvider = 'omdb';
|
let metadataProvider = 'omdb';
|
||||||
let seriesAnalysis = null;
|
let seriesAnalysis = null;
|
||||||
@@ -16054,9 +16085,6 @@ class PipelineService extends EventEmitter {
|
|||||||
if (pluginDetectedTitle) {
|
if (pluginDetectedTitle) {
|
||||||
effectiveDetectedTitle = pluginDetectedTitle;
|
effectiveDetectedTitle = pluginDetectedTitle;
|
||||||
}
|
}
|
||||||
if (Array.isArray(pluginResult?.omdbCandidates)) {
|
|
||||||
omdbCandidates = pluginResult.omdbCandidates;
|
|
||||||
}
|
|
||||||
logger.info('plugin:analyze:used', {
|
logger.info('plugin:analyze:used', {
|
||||||
jobId: normalizedJobId,
|
jobId: normalizedJobId,
|
||||||
pluginId: analyzePlugin.id,
|
pluginId: analyzePlugin.id,
|
||||||
@@ -16076,7 +16104,6 @@ class PipelineService extends EventEmitter {
|
|||||||
|
|
||||||
if (isSeriesDiscMediaProfile(mediaProfile) && analyzePlugin && ['dvd', 'bluray'].includes(analyzePlugin.id)) {
|
if (isSeriesDiscMediaProfile(mediaProfile) && analyzePlugin && ['dvd', 'bluray'].includes(analyzePlugin.id)) {
|
||||||
try {
|
try {
|
||||||
const seriesSettings = await settingsService.getEffectiveSettingsMap(mediaProfile);
|
|
||||||
const rawMedia = collectRawMediaCandidates(resolvedRawPath);
|
const rawMedia = collectRawMediaCandidates(resolvedRawPath);
|
||||||
const seriesScanInputPath = mediaProfile === 'dvd'
|
const seriesScanInputPath = mediaProfile === 'dvd'
|
||||||
? (resolveDvdScanInputPathFromRaw(resolvedRawPath, rawMedia) || resolvedRawPath)
|
? (resolveDvdScanInputPathFromRaw(resolvedRawPath, rawMedia) || resolvedRawPath)
|
||||||
@@ -16183,21 +16210,6 @@ class PipelineService extends EventEmitter {
|
|||||||
seriesTitle: String(seriesLookupHint?.seriesTitle || fallbackQuery).trim() || fallbackQuery
|
seriesTitle: String(seriesLookupHint?.seriesTitle || fallbackQuery).trim() || fallbackQuery
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seriesPluginResult?.providerConfigured && seriesLookupHint?.query) {
|
|
||||||
metadataCandidates = await tmdbService.searchSeriesWithSeasons(
|
|
||||||
seriesLookupHint.query,
|
|
||||||
{ language: seriesSettings?.dvd_series_language || null }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn('dvd-series:tmdb-search-failed', {
|
|
||||||
jobId: normalizedJobId,
|
|
||||||
query: seriesLookupHint?.query || null,
|
|
||||||
seasonNumber: seriesLookupHint?.seasonNumber || null,
|
|
||||||
error: errorToMeta(error)
|
|
||||||
});
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('dvd-series:analyze:done', {
|
logger.info('dvd-series:analyze:done', {
|
||||||
@@ -16220,7 +16232,7 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (metadataProvider !== 'tmdb' && !Array.isArray(omdbCandidates)) {
|
if (metadataProvider !== 'tmdb' && !Array.isArray(omdbCandidates)) {
|
||||||
omdbCandidates = await omdbService.search(effectiveDetectedTitle).catch(() => []);
|
omdbCandidates = [];
|
||||||
}
|
}
|
||||||
if (metadataProvider !== 'tmdb') {
|
if (metadataProvider !== 'tmdb') {
|
||||||
metadataCandidates = Array.isArray(omdbCandidates) ? omdbCandidates : [];
|
metadataCandidates = Array.isArray(omdbCandidates) ? omdbCandidates : [];
|
||||||
@@ -16262,8 +16274,8 @@ class PipelineService extends EventEmitter {
|
|||||||
normalizedJobId,
|
normalizedJobId,
|
||||||
'SYSTEM',
|
'SYSTEM',
|
||||||
metadataProvider === 'tmdb'
|
metadataProvider === 'tmdb'
|
||||||
? `Serien-${resolveSeriesDiscLabel(mediaProfile)} erkannt. TMDb-Metadaten-Suche vorbereitet mit Query "${seriesLookupHint?.query || effectiveDetectedTitle}"${seriesLookupHint?.seasonNumber ? ` und Staffel ${seriesLookupHint.seasonNumber}` : ''}.`
|
? `Serien-${resolveSeriesDiscLabel(mediaProfile)} erkannt. Metadaten müssen manuell zugeordnet werden (Suchvorschlag: "${seriesLookupHint?.query || effectiveDetectedTitle}"${seriesLookupHint?.seasonNumber ? `, Staffel ${seriesLookupHint.seasonNumber}` : ''}).`
|
||||||
: `Disk erkannt. Metadaten-Suche vorbereitet mit Query "${effectiveDetectedTitle}".`
|
: `Disk erkannt. Metadaten müssen manuell zugeordnet werden (Suchvorschlag: "${effectiveDetectedTitle}").`
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.setState('METADATA_SELECTION', {
|
await this.setState('METADATA_SELECTION', {
|
||||||
@@ -16294,7 +16306,7 @@ class PipelineService extends EventEmitter {
|
|||||||
|
|
||||||
void this.notifyPushover('metadata_ready', {
|
void this.notifyPushover('metadata_ready', {
|
||||||
title: 'Ripster - Metadaten bereit',
|
title: 'Ripster - Metadaten bereit',
|
||||||
message: `Job #${normalizedJobId}: ${effectiveDetectedTitle} (${Array.isArray(metadataCandidates) ? metadataCandidates.length : 0} ${metadataProvider === 'tmdb' ? 'TMDb' : 'OMDb'} Treffer)`
|
message: `Job #${normalizedJobId}: Metadaten müssen manuell zugeordnet werden`
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -16448,7 +16460,7 @@ class PipelineService extends EventEmitter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let effectiveDetectedTitle = detectedTitle;
|
let effectiveDetectedTitle = detectedTitle;
|
||||||
let omdbCandidates = null;
|
let omdbCandidates = [];
|
||||||
let metadataCandidates = null;
|
let metadataCandidates = null;
|
||||||
let metadataProvider = 'omdb';
|
let metadataProvider = 'omdb';
|
||||||
let seriesAnalysis = null;
|
let seriesAnalysis = null;
|
||||||
@@ -16466,9 +16478,6 @@ class PipelineService extends EventEmitter {
|
|||||||
if (pluginDetectedTitle) {
|
if (pluginDetectedTitle) {
|
||||||
effectiveDetectedTitle = pluginDetectedTitle;
|
effectiveDetectedTitle = pluginDetectedTitle;
|
||||||
}
|
}
|
||||||
if (Array.isArray(pluginResult?.omdbCandidates)) {
|
|
||||||
omdbCandidates = pluginResult.omdbCandidates;
|
|
||||||
}
|
|
||||||
logger.info('plugin:analyze:used', {
|
logger.info('plugin:analyze:used', {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
pluginId: analyzePlugin.id,
|
pluginId: analyzePlugin.id,
|
||||||
@@ -16486,7 +16495,6 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
if (isSeriesDiscMediaProfile(mediaProfile) && analyzePlugin && ['dvd', 'bluray'].includes(analyzePlugin.id)) {
|
if (isSeriesDiscMediaProfile(mediaProfile) && analyzePlugin && ['dvd', 'bluray'].includes(analyzePlugin.id)) {
|
||||||
try {
|
try {
|
||||||
const seriesSettings = await settingsService.getEffectiveSettingsMap(mediaProfile);
|
|
||||||
const reviewResult = await this.runPluginReviewScan(analyzePlugin, job, {
|
const reviewResult = await this.runPluginReviewScan(analyzePlugin, job, {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
deviceInfo: deviceWithProfile,
|
deviceInfo: deviceWithProfile,
|
||||||
@@ -16586,21 +16594,6 @@ class PipelineService extends EventEmitter {
|
|||||||
seriesTitle: String(seriesLookupHint?.seriesTitle || fallbackQuery).trim() || fallbackQuery
|
seriesTitle: String(seriesLookupHint?.seriesTitle || fallbackQuery).trim() || fallbackQuery
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seriesPluginResult?.providerConfigured && seriesLookupHint?.query) {
|
|
||||||
metadataCandidates = await tmdbService.searchSeriesWithSeasons(
|
|
||||||
seriesLookupHint.query,
|
|
||||||
{ language: seriesSettings?.dvd_series_language || null }
|
|
||||||
).catch((error) => {
|
|
||||||
logger.warn('dvd-series:tmdb-search-failed', {
|
|
||||||
jobId: job.id,
|
|
||||||
query: seriesLookupHint?.query || null,
|
|
||||||
seasonNumber: seriesLookupHint?.seasonNumber || null,
|
|
||||||
error: errorToMeta(error)
|
|
||||||
});
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('dvd-series:analyze:done', {
|
logger.info('dvd-series:analyze:done', {
|
||||||
@@ -16621,7 +16614,7 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (metadataProvider !== 'tmdb' && !Array.isArray(omdbCandidates)) {
|
if (metadataProvider !== 'tmdb' && !Array.isArray(omdbCandidates)) {
|
||||||
omdbCandidates = await omdbService.search(effectiveDetectedTitle).catch(() => []);
|
omdbCandidates = [];
|
||||||
}
|
}
|
||||||
if (metadataProvider !== 'tmdb') {
|
if (metadataProvider !== 'tmdb') {
|
||||||
metadataCandidates = Array.isArray(omdbCandidates) ? omdbCandidates : [];
|
metadataCandidates = Array.isArray(omdbCandidates) ? omdbCandidates : [];
|
||||||
@@ -16662,8 +16655,8 @@ class PipelineService extends EventEmitter {
|
|||||||
job.id,
|
job.id,
|
||||||
'SYSTEM',
|
'SYSTEM',
|
||||||
metadataProvider === 'tmdb'
|
metadataProvider === 'tmdb'
|
||||||
? `Serien-${resolveSeriesDiscLabel(mediaProfile)} erkannt. TMDb-Metadaten-Suche vorbereitet mit Query "${seriesLookupHint?.query || effectiveDetectedTitle}"${seriesLookupHint?.seasonNumber ? ` und Staffel ${seriesLookupHint.seasonNumber}` : ''}.`
|
? `Serien-${resolveSeriesDiscLabel(mediaProfile)} erkannt. Metadaten müssen manuell zugeordnet werden (Suchvorschlag: "${seriesLookupHint?.query || effectiveDetectedTitle}"${seriesLookupHint?.seasonNumber ? `, Staffel ${seriesLookupHint.seasonNumber}` : ''}).`
|
||||||
: `Disk erkannt. Metadaten-Suche vorbereitet mit Query "${effectiveDetectedTitle}".`
|
: `Disk erkannt. Metadaten müssen manuell zugeordnet werden (Suchvorschlag: "${effectiveDetectedTitle}").`
|
||||||
);
|
);
|
||||||
|
|
||||||
const keepCurrentPipelineSession = this._hasForeignInteractiveSession(job.id);
|
const keepCurrentPipelineSession = this._hasForeignInteractiveSession(job.id);
|
||||||
@@ -16705,7 +16698,7 @@ class PipelineService extends EventEmitter {
|
|||||||
|
|
||||||
void this.notifyPushover('metadata_ready', {
|
void this.notifyPushover('metadata_ready', {
|
||||||
title: 'Ripster - Metadaten bereit',
|
title: 'Ripster - Metadaten bereit',
|
||||||
message: `Job #${job.id}: ${effectiveDetectedTitle} (${Array.isArray(metadataCandidates) ? metadataCandidates.length : 0} ${metadataProvider === 'tmdb' ? 'TMDb' : 'OMDb'} Treffer)`
|
message: `Job #${job.id}: Metadaten müssen manuell zugeordnet werden`
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -21500,6 +21493,25 @@ class PipelineService extends EventEmitter {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
|
const confirmSettings = await settingsService.getEffectiveSettingsMap(readyMediaProfile);
|
||||||
|
const actionDisplaySettings = {
|
||||||
|
...(confirmSettings && typeof confirmSettings === 'object' ? confirmSettings : {}),
|
||||||
|
handbrake_preset: String(
|
||||||
|
resolvedUserPreset?.handbrakePreset
|
||||||
|
|| confirmSettings?.handbrake_preset
|
||||||
|
|| ''
|
||||||
|
).trim() || null,
|
||||||
|
handbrake_extra_args: String(
|
||||||
|
resolvedUserPreset?.extraArgs
|
||||||
|
|| confirmSettings?.handbrake_extra_args
|
||||||
|
|| ''
|
||||||
|
).trim()
|
||||||
|
};
|
||||||
|
confirmedPlan.titles = refreshAudioTrackActionsForPlanTitles(
|
||||||
|
confirmedPlan.titles,
|
||||||
|
actionDisplaySettings,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
const resolvedConfirmRawPath = this.resolveCurrentRawPathForSettings(
|
const resolvedConfirmRawPath = this.resolveCurrentRawPathForSettings(
|
||||||
confirmSettings,
|
confirmSettings,
|
||||||
readyMediaProfile,
|
readyMediaProfile,
|
||||||
@@ -25168,6 +25180,7 @@ class PipelineService extends EventEmitter {
|
|||||||
selectedTitleId: effectiveSelectedTitleId,
|
selectedTitleId: effectiveSelectedTitleId,
|
||||||
disableMinLengthFilter,
|
disableMinLengthFilter,
|
||||||
backupOutputBase,
|
backupOutputBase,
|
||||||
|
sourceArgOverride: null,
|
||||||
runCommand: this.runCommand.bind(this)
|
runCommand: this.runCommand.bind(this)
|
||||||
});
|
});
|
||||||
makemkvInfo = await ripPlugin.rip(job, ripCtx);
|
makemkvInfo = await ripPlugin.rip(job, ripCtx);
|
||||||
@@ -25325,7 +25338,6 @@ class PipelineService extends EventEmitter {
|
|||||||
// Retry starts processing immediately (Rip/Merge) and bypasses the encode queue.
|
// Retry starts processing immediately (Rip/Merge) and bypasses the encode queue.
|
||||||
return this.retry(jobId, { ...options, immediate: true });
|
return this.retry(jobId, { ...options, immediate: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedRetryJob = await this.resolveExistingJobForAction(jobId, 'retry');
|
const resolvedRetryJob = await this.resolveExistingJobForAction(jobId, 'retry');
|
||||||
jobId = resolvedRetryJob.resolvedJobId;
|
jobId = resolvedRetryJob.resolvedJobId;
|
||||||
this.ensureNotBusy('retry', jobId);
|
this.ensureNotBusy('retry', jobId);
|
||||||
@@ -25357,7 +25369,9 @@ class PipelineService extends EventEmitter {
|
|||||||
const isCdRetry = mediaProfile === 'cd';
|
const isCdRetry = mediaProfile === 'cd';
|
||||||
const isAudiobookRetry = mediaProfile === 'audiobook';
|
const isAudiobookRetry = mediaProfile === 'audiobook';
|
||||||
const isMultipartMergeRetry = sourceJobKind === 'multipart_movie_merge';
|
const isMultipartMergeRetry = sourceJobKind === 'multipart_movie_merge';
|
||||||
|
const isVideoDiscRetry = mediaProfile === 'dvd' || mediaProfile === 'bluray';
|
||||||
const sourceParentJobId = this.normalizeQueueJobId(sourceJob?.parent_job_id);
|
const sourceParentJobId = this.normalizeQueueJobId(sourceJob?.parent_job_id);
|
||||||
|
|
||||||
let mergeContainerJobId = null;
|
let mergeContainerJobId = null;
|
||||||
if (isMultipartMergeRetry) {
|
if (isMultipartMergeRetry) {
|
||||||
mergeContainerJobId = this.normalizeQueueJobId(sourceEncodePlan?.containerJobId);
|
mergeContainerJobId = this.normalizeQueueJobId(sourceEncodePlan?.containerJobId);
|
||||||
@@ -25391,12 +25405,21 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CD-Jobs dürfen auch im FINISHED-Status neu gerippt werden.
|
// CD-Jobs dürfen auch im FINISHED-Status neu gerippt werden.
|
||||||
const retryable = ['ERROR', 'CANCELLED'].includes(sourceStatus)
|
const retryableByStatus = ['ERROR', 'CANCELLED'].includes(sourceStatus)
|
||||||
|| ['ERROR', 'CANCELLED'].includes(sourceLastState)
|
|| ['ERROR', 'CANCELLED'].includes(sourceLastState)
|
||||||
|| (isCdRetry && ['FINISHED', 'ERROR', 'CANCELLED'].includes(sourceStatus));
|
|| (isCdRetry && ['FINISHED', 'ERROR', 'CANCELLED'].includes(sourceStatus));
|
||||||
|
const incompleteRipRetryableStates = ['WAITING_FOR_USER_DECISION', 'READY_TO_ENCODE', 'MEDIAINFO_CHECK'];
|
||||||
|
const retryableByIncompleteVideoRipState = isVideoDiscRetry
|
||||||
|
&& !isMultipartMergeRetry
|
||||||
|
&& (
|
||||||
|
incompleteRipRetryableStates.includes(sourceStatus)
|
||||||
|
|| incompleteRipRetryableStates.includes(sourceLastState)
|
||||||
|
);
|
||||||
|
const retryable = retryableByStatus || retryableByIncompleteVideoRipState;
|
||||||
if (!retryable) {
|
if (!retryable) {
|
||||||
|
const currentStateLabel = sourceStatus || sourceLastState || '-';
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
`Retry nicht möglich: Job ${jobId} ist nicht im Status ERROR/CANCELLED (aktuell ${sourceStatus || sourceLastState || '-'}).`
|
`Retry nicht möglich: Job ${jobId} ist nicht in einem retry-fähigen Status (aktuell ${currentStateLabel}).`
|
||||||
);
|
);
|
||||||
error.statusCode = 409;
|
error.statusCode = 409;
|
||||||
throw error;
|
throw error;
|
||||||
@@ -25690,7 +25713,11 @@ class PipelineService extends EventEmitter {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.startRipEncode(retryJobId).catch((error) => {
|
this.startRipEncode(retryJobId).catch((error) => {
|
||||||
logger.error('retry:background-failed', { jobId: retryJobId, sourceJobId: jobId, error: errorToMeta(error) });
|
logger.error('retry:background-failed', {
|
||||||
|
jobId: retryJobId,
|
||||||
|
sourceJobId: jobId,
|
||||||
|
error: errorToMeta(error)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1176,7 +1176,8 @@ class SettingsService {
|
|||||||
const ripMode = String(map.makemkv_rip_mode || 'mkv').trim().toLowerCase() === 'backup'
|
const ripMode = String(map.makemkv_rip_mode || 'mkv').trim().toLowerCase() === 'backup'
|
||||||
? 'backup'
|
? 'backup'
|
||||||
: 'mkv';
|
: 'mkv';
|
||||||
const sourceArg = this.resolveSourceArg(map, deviceInfo);
|
const sourceArgOverride = String(options?.sourceArgOverride || '').trim();
|
||||||
|
const sourceArg = sourceArgOverride || this.resolveSourceArg(map, deviceInfo);
|
||||||
const rawSelectedTitleId = normalizeNonNegativeInteger(options?.selectedTitleId);
|
const rawSelectedTitleId = normalizeNonNegativeInteger(options?.selectedTitleId);
|
||||||
const disableMinLengthFilter = Boolean(options?.disableMinLengthFilter);
|
const disableMinLengthFilter = Boolean(options?.disableMinLengthFilter);
|
||||||
const parsedExtra = splitArgs(map.makemkv_rip_extra_args);
|
const parsedExtra = splitArgs(map.makemkv_rip_extra_args);
|
||||||
@@ -1210,6 +1211,7 @@ class SettingsService {
|
|||||||
if (hasExplicitTitle) {
|
if (hasExplicitTitle) {
|
||||||
baseArgs = [
|
baseArgs = [
|
||||||
'-r', '--progress=-same',
|
'-r', '--progress=-same',
|
||||||
|
'--decrypt',
|
||||||
'mkv',
|
'mkv',
|
||||||
sourceArg,
|
sourceArg,
|
||||||
targetTitle,
|
targetTitle,
|
||||||
@@ -1221,6 +1223,7 @@ class SettingsService {
|
|||||||
: [];
|
: [];
|
||||||
baseArgs = [
|
baseArgs = [
|
||||||
'-r', '--progress=-same',
|
'-r', '--progress=-same',
|
||||||
|
'--decrypt',
|
||||||
...minLengthArgs,
|
...minLengthArgs,
|
||||||
'mkv',
|
'mkv',
|
||||||
sourceArg,
|
sourceArg,
|
||||||
|
|||||||
@@ -1243,6 +1243,66 @@ function computeAudioTrackActions(track, selectedIndex, selector) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshAudioTrackActionsForPlanTitles(titles, settings = {}, presetProfile = null) {
|
||||||
|
const sourceTitles = Array.isArray(titles) ? titles : [];
|
||||||
|
if (sourceTitles.length === 0) {
|
||||||
|
return sourceTitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectors = buildTrackSelectors(settings || {}, presetProfile || null);
|
||||||
|
const audioSelector = selectors?.audio && typeof selectors.audio === 'object'
|
||||||
|
? selectors.audio
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return sourceTitles.map((title) => {
|
||||||
|
const audioTracks = Array.isArray(title?.audioTracks) ? title.audioTracks : [];
|
||||||
|
if (audioTracks.length === 0) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedTracks = audioTracks.filter((track) => Boolean(track?.selectedForEncode));
|
||||||
|
const selectedIndexById = new Map(
|
||||||
|
selectedTracks
|
||||||
|
.map((track, index) => {
|
||||||
|
const trackId = Number(track?.id);
|
||||||
|
if (!Number.isFinite(trackId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [Math.trunc(trackId), index];
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextAudioTracks = audioTracks.map((track) => {
|
||||||
|
if (!track?.selectedForEncode) {
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
encodeActions: [],
|
||||||
|
encodeActionSummary: 'Nicht übernommen'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const numericTrackId = Number(track?.id);
|
||||||
|
const normalizedTrackId = Number.isFinite(numericTrackId) ? Math.trunc(numericTrackId) : null;
|
||||||
|
const selectedIndex = (normalizedTrackId !== null && selectedIndexById.has(normalizedTrackId))
|
||||||
|
? selectedIndexById.get(normalizedTrackId)
|
||||||
|
: 0;
|
||||||
|
const actions = computeAudioTrackActions(track, selectedIndex, audioSelector);
|
||||||
|
return {
|
||||||
|
...track,
|
||||||
|
encodePreviewActions: actions.actions,
|
||||||
|
encodePreviewSummary: actions.summary,
|
||||||
|
encodeActions: actions.actions,
|
||||||
|
encodeActionSummary: actions.summary
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...title,
|
||||||
|
audioTracks: nextAudioTracks
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function computeSubtitleFlags(trackId, selectedTrackIds, selector) {
|
function computeSubtitleFlags(trackId, selectedTrackIds, selector) {
|
||||||
const selected = selectedTrackIds.includes(trackId);
|
const selected = selectedTrackIds.includes(trackId);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
@@ -1526,5 +1586,6 @@ function buildMediainfoReview({
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parseDurationSeconds,
|
parseDurationSeconds,
|
||||||
buildMediainfoReview
|
buildMediainfoReview,
|
||||||
|
refreshAudioTrackActionsForPlanTitles
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -647,7 +647,7 @@ VALUES ('pushover_timeout_ms', 'Benachrichtigungen', 'PushOver Timeout (ms)', 'n
|
|||||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_timeout_ms', '7000');
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_timeout_ms', '7000');
|
||||||
|
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
||||||
VALUES ('pushover_notify_metadata_ready', 'Benachrichtigungen', 'Bei Metadaten-Auswahl senden', 'boolean', 1, 'Sendet wenn Metadaten zur Auswahl bereitstehen.', 'true', '[]', '{}', 570);
|
VALUES ('pushover_notify_metadata_ready', 'Benachrichtigungen', 'Bei manueller Auswahl senden', 'boolean', 1, 'Sendet, wenn ein Job eine manuelle Auswahl/Entscheidung benötigt (z.B. Metadaten, Titel oder Playlist).', 'true', '[]', '{}', 570);
|
||||||
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_metadata_ready', 'true');
|
INSERT OR IGNORE INTO settings_values (key, value) VALUES ('pushover_notify_metadata_ready', 'true');
|
||||||
|
|
||||||
INSERT OR IGNORE INTO settings_schema (key, category, label, type, required, description, default_value, options_json, validation_json, order_index)
|
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",
|
"name": "ripster-frontend",
|
||||||
"version": "0.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"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.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -184,6 +184,78 @@ function trackSizeFormat(bytes) {
|
|||||||
return `${size.toFixed(2)} ${units[i]}`;
|
return `${size.toFixed(2)} ${units[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveManualTrackSelectionForTitle(encodePlan, titleId) {
|
||||||
|
const plan = encodePlan && typeof encodePlan === 'object' ? encodePlan : {};
|
||||||
|
const normalizedTitleId = normalizePositiveInteger(titleId);
|
||||||
|
if (!normalizedTitleId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const byTitle = plan?.manualTrackSelectionByTitle && typeof plan.manualTrackSelectionByTitle === 'object'
|
||||||
|
? plan.manualTrackSelectionByTitle
|
||||||
|
: {};
|
||||||
|
const directSelection = byTitle[normalizedTitleId] || byTitle[String(normalizedTitleId)] || null;
|
||||||
|
if (directSelection && typeof directSelection === 'object') {
|
||||||
|
return directSelection;
|
||||||
|
}
|
||||||
|
const fallbackSelection = plan?.manualTrackSelection && typeof plan.manualTrackSelection === 'object'
|
||||||
|
? plan.manualTrackSelection
|
||||||
|
: null;
|
||||||
|
if (!fallbackSelection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fallbackTitleId = normalizePositiveInteger(fallbackSelection?.titleId);
|
||||||
|
return fallbackTitleId === normalizedTitleId ? fallbackSelection : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEffectiveTitleTrackSelection(encodePlan, title) {
|
||||||
|
const manualSelection = resolveManualTrackSelectionForTitle(encodePlan, title?.id);
|
||||||
|
const hasManualAudio = Array.isArray(manualSelection?.audioTrackIds);
|
||||||
|
const hasManualSubtitle = Array.isArray(manualSelection?.subtitleTrackIdsOrdered) || Array.isArray(manualSelection?.subtitleTrackIds);
|
||||||
|
const selectedAudioIds = hasManualAudio
|
||||||
|
? normalizeIdList(manualSelection.audioTrackIds)
|
||||||
|
: normalizeIdList((Array.isArray(title?.audioTracks) ? title.audioTracks : [])
|
||||||
|
.filter((track) => Boolean(track?.selectedForEncode))
|
||||||
|
.map((track) => track?.id));
|
||||||
|
const selectedSubtitleIds = hasManualSubtitle
|
||||||
|
? normalizeIdList(
|
||||||
|
Array.isArray(manualSelection?.subtitleTrackIdsOrdered) && manualSelection.subtitleTrackIdsOrdered.length > 0
|
||||||
|
? manualSelection.subtitleTrackIdsOrdered
|
||||||
|
: manualSelection?.subtitleTrackIds
|
||||||
|
)
|
||||||
|
: normalizeIdList((Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : [])
|
||||||
|
.filter((track) => Boolean(track?.selectedForEncode))
|
||||||
|
.map((track) => track?.id));
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedAudioSet: new Set(selectedAudioIds.map((id) => String(id))),
|
||||||
|
selectedSubtitleSet: new Set(selectedSubtitleIds.map((id) => String(id))),
|
||||||
|
hasManualAudio,
|
||||||
|
hasManualSubtitle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrackActionLabel({
|
||||||
|
selected = false,
|
||||||
|
summary = '',
|
||||||
|
manualSelection = false,
|
||||||
|
fallback = 'Übernehmen'
|
||||||
|
}) {
|
||||||
|
if (!selected) {
|
||||||
|
return 'Nicht übernommen';
|
||||||
|
}
|
||||||
|
const rawSummary = String(summary || '').trim();
|
||||||
|
if (!rawSummary) {
|
||||||
|
return manualSelection ? `${fallback} (manuell)` : fallback;
|
||||||
|
}
|
||||||
|
if (/^nicht übernommen/i.test(rawSummary)) {
|
||||||
|
return manualSelection ? `${fallback} (manuell)` : fallback;
|
||||||
|
}
|
||||||
|
if (/^preset-default\b/i.test(rawSummary) || /Preset-Default \(HandBrake\)/i.test(rawSummary)) {
|
||||||
|
return manualSelection ? `${fallback} (manuell)` : 'Übernehmen';
|
||||||
|
}
|
||||||
|
return rawSummary;
|
||||||
|
}
|
||||||
|
|
||||||
function buildExecutedHandBrakeCommand(handbrakeInfo) {
|
function buildExecutedHandBrakeCommand(handbrakeInfo) {
|
||||||
const cmd = String(handbrakeInfo?.cmd || '').trim();
|
const cmd = String(handbrakeInfo?.cmd || '').trim();
|
||||||
const args = Array.isArray(handbrakeInfo?.args) ? handbrakeInfo.args : [];
|
const args = Array.isArray(handbrakeInfo?.args) ? handbrakeInfo.args : [];
|
||||||
@@ -1378,6 +1450,7 @@ export default function JobDetailDialog({
|
|||||||
const selectedEncodeTitles = encodePlanTitles.filter(
|
const selectedEncodeTitles = encodePlanTitles.filter(
|
||||||
(title) => title.selectedForEncode || title.encodeInput || String(title.id) === String(reviewSelectedEncodeTitleId)
|
(title) => title.selectedForEncode || title.encodeInput || String(title.id) === String(reviewSelectedEncodeTitleId)
|
||||||
);
|
);
|
||||||
|
const trackDisplayTitles = selectedEncodeTitles.length > 0 ? selectedEncodeTitles : encodePlanTitles;
|
||||||
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
||||||
const posterUrl = String(
|
const posterUrl = String(
|
||||||
job?.poster_url
|
job?.poster_url
|
||||||
@@ -1390,7 +1463,41 @@ export default function JobDetailDialog({
|
|||||||
const resolvedRawPath = job?.rawStatus?.path || job?.raw_path || null;
|
const resolvedRawPath = job?.rawStatus?.path || job?.raw_path || null;
|
||||||
const canDownloadRaw = Boolean(resolvedRawPath && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
const canDownloadRaw = Boolean(resolvedRawPath && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
||||||
const canDownloadOutput = Boolean(job?.output_path && job?.outputStatus?.exists && typeof onDownloadArchive === 'function');
|
const canDownloadOutput = Boolean(job?.output_path && job?.outputStatus?.exists && typeof onDownloadArchive === 'function');
|
||||||
const outputFolders = Array.isArray(job?.outputFolders) ? job.outputFolders : [];
|
const outputFolders = (() => {
|
||||||
|
const baseFolders = Array.isArray(job?.outputFolders) ? job.outputFolders : [];
|
||||||
|
if (!isDiskContainer) {
|
||||||
|
return baseFolders;
|
||||||
|
}
|
||||||
|
const merged = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const addFolder = (folder, fallbackId = null) => {
|
||||||
|
const outputPath = String(folder?.output_path || '').trim();
|
||||||
|
if (!outputPath || seen.has(outputPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(outputPath);
|
||||||
|
merged.push({
|
||||||
|
id: folder?.id ?? fallbackId ?? outputPath,
|
||||||
|
output_path: outputPath
|
||||||
|
});
|
||||||
|
};
|
||||||
|
for (const folder of baseFolders) {
|
||||||
|
addFolder(folder);
|
||||||
|
}
|
||||||
|
for (const child of childJobs) {
|
||||||
|
const childOutputFolders = Array.isArray(child?.outputFolders) ? child.outputFolders : [];
|
||||||
|
for (const childFolder of childOutputFolders) {
|
||||||
|
addFolder(childFolder, `child-folder-${child?.id || 'x'}-${String(childFolder?.output_path || '')}`);
|
||||||
|
}
|
||||||
|
if (child?.output_path) {
|
||||||
|
addFolder(
|
||||||
|
{ output_path: child.output_path },
|
||||||
|
`child-output-${child?.id || 'x'}-${String(child.output_path || '')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
})();
|
||||||
const mergeOutputPathCandidates = new Set(
|
const mergeOutputPathCandidates = new Set(
|
||||||
[
|
[
|
||||||
...(isMultipartMergeJob ? [job?.output_path] : []),
|
...(isMultipartMergeJob ? [job?.output_path] : []),
|
||||||
@@ -2083,7 +2190,7 @@ export default function JobDetailDialog({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* Spurauswahl – read-only */}
|
{/* Spurauswahl – read-only */}
|
||||||
{selectedEncodeTitles.length > 0 ? (
|
{trackDisplayTitles.length > 0 ? (
|
||||||
isDvdSeries ? (
|
isDvdSeries ? (
|
||||||
<details className="episode-track-accordion">
|
<details className="episode-track-accordion">
|
||||||
<summary className="series-batch-episode-head">
|
<summary className="series-batch-episode-head">
|
||||||
@@ -2091,9 +2198,10 @@ export default function JobDetailDialog({
|
|||||||
</summary>
|
</summary>
|
||||||
<div className="spurauswahl-block">
|
<div className="spurauswahl-block">
|
||||||
<div className="spurauswahl-label">Spurauswahl</div>
|
<div className="spurauswahl-label">Spurauswahl</div>
|
||||||
{selectedEncodeTitles.map((title) => {
|
{trackDisplayTitles.map((title) => {
|
||||||
const audioTracks = Array.isArray(title.audioTracks) ? title.audioTracks : [];
|
const audioTracks = Array.isArray(title.audioTracks) ? title.audioTracks : [];
|
||||||
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
||||||
|
const trackSelection = buildEffectiveTitleTrackSelection(job?.encodePlan, title);
|
||||||
return (
|
return (
|
||||||
<div key={title.id} className="track-title-block">
|
<div key={title.id} className="track-title-block">
|
||||||
<div className="track-title-row">
|
<div className="track-title-row">
|
||||||
@@ -2109,9 +2217,16 @@ export default function JobDetailDialog({
|
|||||||
const chLayout = trackChLayout(track.channels);
|
const chLayout = trackChLayout(track.channels);
|
||||||
let displayText = `#${track.id} | ${lang} | ${codec}`;
|
let displayText = `#${track.id} | ${lang} | ${codec}`;
|
||||||
if (chLayout) displayText += ` | ${chLayout}`;
|
if (chLayout) displayText += ` | ${chLayout}`;
|
||||||
const actionInfo = track.selectedForEncode
|
const normalizedTrackId = normalizePositiveInteger(track?.id);
|
||||||
? (String(track.encodePreviewSummary || track.encodeActionSummary || '').trim() || 'Copy')
|
const selected = normalizedTrackId
|
||||||
: 'Nicht übernommen';
|
? trackSelection.selectedAudioSet.has(String(normalizedTrackId))
|
||||||
|
: Boolean(track?.selectedForEncode);
|
||||||
|
const actionInfo = getTrackActionLabel({
|
||||||
|
selected,
|
||||||
|
summary: track.encodeActionSummary || track.encodePreviewSummary,
|
||||||
|
manualSelection: trackSelection.hasManualAudio,
|
||||||
|
fallback: 'Übernehmen'
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div key={track.id} className="track-item">
|
<div key={track.id} className="track-item">
|
||||||
<span>{displayText}</span>
|
<span>{displayText}</span>
|
||||||
@@ -2128,10 +2243,16 @@ export default function JobDetailDialog({
|
|||||||
const lang = trackLang(track.language || track.languageLabel);
|
const lang = trackLang(track.language || track.languageLabel);
|
||||||
const codec = trackCodec('subtitle', track.format);
|
const codec = trackCodec('subtitle', track.format);
|
||||||
const displayText = `#${track.id} | ${lang} | ${codec}`;
|
const displayText = `#${track.id} | ${lang} | ${codec}`;
|
||||||
const rawAction = String(track.subtitlePreviewSummary || track.subtitleActionSummary || '').trim();
|
const normalizedTrackId = normalizePositiveInteger(track?.id);
|
||||||
const actionInfo = track.selectedForEncode
|
const selected = normalizedTrackId
|
||||||
? (/^nicht übernommen$/i.test(rawAction) ? 'Übernehmen' : (rawAction || 'Übernehmen'))
|
? trackSelection.selectedSubtitleSet.has(String(normalizedTrackId))
|
||||||
: 'Nicht übernommen';
|
: Boolean(track?.selectedForEncode);
|
||||||
|
const actionInfo = getTrackActionLabel({
|
||||||
|
selected,
|
||||||
|
summary: track.subtitleActionSummary || track.subtitlePreviewSummary,
|
||||||
|
manualSelection: trackSelection.hasManualSubtitle,
|
||||||
|
fallback: 'Übernehmen'
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div key={track.id} className="track-item">
|
<div key={track.id} className="track-item">
|
||||||
<span>{displayText}</span>
|
<span>{displayText}</span>
|
||||||
@@ -2150,9 +2271,10 @@ export default function JobDetailDialog({
|
|||||||
) : (
|
) : (
|
||||||
<div className="spurauswahl-block">
|
<div className="spurauswahl-block">
|
||||||
<div className="spurauswahl-label">Spurauswahl</div>
|
<div className="spurauswahl-label">Spurauswahl</div>
|
||||||
{selectedEncodeTitles.map((title) => {
|
{trackDisplayTitles.map((title) => {
|
||||||
const audioTracks = Array.isArray(title.audioTracks) ? title.audioTracks : [];
|
const audioTracks = Array.isArray(title.audioTracks) ? title.audioTracks : [];
|
||||||
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
const subtitleTracks = Array.isArray(title.subtitleTracks) ? title.subtitleTracks : [];
|
||||||
|
const trackSelection = buildEffectiveTitleTrackSelection(job?.encodePlan, title);
|
||||||
return (
|
return (
|
||||||
<div key={title.id} className="track-title-block">
|
<div key={title.id} className="track-title-block">
|
||||||
<div className="track-title-row">
|
<div className="track-title-row">
|
||||||
@@ -2168,9 +2290,16 @@ export default function JobDetailDialog({
|
|||||||
const chLayout = trackChLayout(track.channels);
|
const chLayout = trackChLayout(track.channels);
|
||||||
let displayText = `#${track.id} | ${lang} | ${codec}`;
|
let displayText = `#${track.id} | ${lang} | ${codec}`;
|
||||||
if (chLayout) displayText += ` | ${chLayout}`;
|
if (chLayout) displayText += ` | ${chLayout}`;
|
||||||
const actionInfo = track.selectedForEncode
|
const normalizedTrackId = normalizePositiveInteger(track?.id);
|
||||||
? (String(track.encodePreviewSummary || track.encodeActionSummary || '').trim() || 'Copy')
|
const selected = normalizedTrackId
|
||||||
: 'Nicht übernommen';
|
? trackSelection.selectedAudioSet.has(String(normalizedTrackId))
|
||||||
|
: Boolean(track?.selectedForEncode);
|
||||||
|
const actionInfo = getTrackActionLabel({
|
||||||
|
selected,
|
||||||
|
summary: track.encodeActionSummary || track.encodePreviewSummary,
|
||||||
|
manualSelection: trackSelection.hasManualAudio,
|
||||||
|
fallback: 'Übernehmen'
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div key={track.id} className="track-item">
|
<div key={track.id} className="track-item">
|
||||||
<span>{displayText}</span>
|
<span>{displayText}</span>
|
||||||
@@ -2187,10 +2316,16 @@ export default function JobDetailDialog({
|
|||||||
const lang = trackLang(track.language || track.languageLabel);
|
const lang = trackLang(track.language || track.languageLabel);
|
||||||
const codec = trackCodec('subtitle', track.format);
|
const codec = trackCodec('subtitle', track.format);
|
||||||
const displayText = `#${track.id} | ${lang} | ${codec}`;
|
const displayText = `#${track.id} | ${lang} | ${codec}`;
|
||||||
const rawAction = String(track.subtitlePreviewSummary || track.subtitleActionSummary || '').trim();
|
const normalizedTrackId = normalizePositiveInteger(track?.id);
|
||||||
const actionInfo = track.selectedForEncode
|
const selected = normalizedTrackId
|
||||||
? (/^nicht übernommen$/i.test(rawAction) ? 'Übernehmen' : (rawAction || 'Übernehmen'))
|
? trackSelection.selectedSubtitleSet.has(String(normalizedTrackId))
|
||||||
: 'Nicht übernommen';
|
: Boolean(track?.selectedForEncode);
|
||||||
|
const actionInfo = getTrackActionLabel({
|
||||||
|
selected,
|
||||||
|
summary: track.subtitleActionSummary || track.subtitlePreviewSummary,
|
||||||
|
manualSelection: trackSelection.hasManualSubtitle,
|
||||||
|
fallback: 'Übernehmen'
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div key={track.id} className="track-item">
|
<div key={track.id} className="track-item">
|
||||||
<span>{displayText}</span>
|
<span>{displayText}</span>
|
||||||
|
|||||||
@@ -179,6 +179,85 @@ function resolvePipelineMediaProfile(pipeline, mediaInfoReview) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMakeMkvRipIssueMessage(value) {
|
||||||
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMakeMkvRipIssueLine(rawLine) {
|
||||||
|
const line = String(rawLine || '').trim();
|
||||||
|
if (!line) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceMatch = line.match(/^\[[^\]]+\]\s+\[([A-Z0-9_]+)\]\s+/i);
|
||||||
|
const source = String(sourceMatch?.[1] || '').trim().toUpperCase();
|
||||||
|
if (source && source !== 'MAKEMKV_RIP') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgCodeMatch = line.match(/\bMSG:(\d+),/i);
|
||||||
|
const parsedCode = msgCodeMatch ? Number(msgCodeMatch[1]) : null;
|
||||||
|
const code = Number.isFinite(parsedCode) ? Math.trunc(parsedCode) : null;
|
||||||
|
const quotedMessageMatch = line.match(/\bMSG:\d+,[^,]*,[^,]*,"((?:[^"\\]|\\.)*)"/i);
|
||||||
|
const fallback = line
|
||||||
|
.replace(/^\[[^\]]+\]\s+\[[^\]]+\]\s*/i, '')
|
||||||
|
.trim();
|
||||||
|
const rawMessage = quotedMessageMatch?.[1]
|
||||||
|
? quotedMessageMatch[1].replace(/\\"/g, '"')
|
||||||
|
: (fallback || line);
|
||||||
|
const message = normalizeMakeMkvRipIssueMessage(rawMessage);
|
||||||
|
if (!message) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isErrorLine = /\berror\b|\bfail(?:ed|ure)?\b|\bhash check\b|\buncorrectable\b/i.test(message);
|
||||||
|
if (!isErrorLine) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMakeMkvRipIssues(makemkvInfo) {
|
||||||
|
if (!makemkvInfo || typeof makemkvInfo !== 'object') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceLines = [];
|
||||||
|
if (Array.isArray(makemkvInfo?.highlights)) {
|
||||||
|
sourceLines.push(...makemkvInfo.highlights);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdoutTail = String(makemkvInfo?.stdoutTail || '').trim();
|
||||||
|
if (stdoutTail) {
|
||||||
|
sourceLines.push(...stdoutTail.split('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const stderrTail = String(makemkvInfo?.stderrTail || '').trim();
|
||||||
|
if (stderrTail) {
|
||||||
|
sourceLines.push(...stderrTail.split('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const issues = [];
|
||||||
|
for (const line of sourceLines) {
|
||||||
|
const parsed = parseMakeMkvRipIssueLine(line);
|
||||||
|
if (!parsed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = `${parsed.code ?? '-'}|${parsed.message.toLowerCase()}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
issues.push(parsed);
|
||||||
|
if (issues.length >= 12) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
function isBurnedSubtitleTrack(track) {
|
function isBurnedSubtitleTrack(track) {
|
||||||
const flags = Array.isArray(track?.subtitlePreviewFlags)
|
const flags = Array.isArray(track?.subtitlePreviewFlags)
|
||||||
? track.subtitlePreviewFlags
|
? track.subtitlePreviewFlags
|
||||||
@@ -2880,6 +2959,33 @@ export default function PipelineStatusCard({
|
|||||||
+ `${runningSeriesEpisode?.title ? ` - ${runningSeriesEpisode.title}` : ''}`
|
+ `${runningSeriesEpisode?.title ? ` - ${runningSeriesEpisode.title}` : ''}`
|
||||||
)
|
)
|
||||||
: (pipeline?.statusText || 'Bereit');
|
: (pipeline?.statusText || 'Bereit');
|
||||||
|
const ripIssueEntries = useMemo(
|
||||||
|
() => extractMakeMkvRipIssues(jobRow?.makemkvInfo),
|
||||||
|
[jobRow?.makemkvInfo]
|
||||||
|
);
|
||||||
|
const ripIssueVisibleEntries = ripIssueEntries.slice(0, 8);
|
||||||
|
const hiddenRipIssueCount = Math.max(0, ripIssueEntries.length - ripIssueVisibleEntries.length);
|
||||||
|
const ripIssueSummaryLabel = ripIssueEntries.length === 1
|
||||||
|
? '1 Rip-Fehler erkannt'
|
||||||
|
: `${ripIssueEntries.length} Rip-Fehler erkannt`;
|
||||||
|
const showRipIssueNotice = Boolean(
|
||||||
|
(jobMediaProfile === 'dvd' || jobMediaProfile === 'bluray')
|
||||||
|
&& ripCompleted
|
||||||
|
&& ripIssueEntries.length > 0
|
||||||
|
&& !running
|
||||||
|
&& (
|
||||||
|
state === 'WAITING_FOR_USER_DECISION'
|
||||||
|
|| state === 'READY_TO_ENCODE'
|
||||||
|
|| state === 'MEDIAINFO_CHECK'
|
||||||
|
|| Boolean(mediaInfoReview)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const canTriggerRipRetry = Boolean(
|
||||||
|
showRipIssueNotice
|
||||||
|
&& retryJobId
|
||||||
|
&& typeof onRetry === 'function'
|
||||||
|
&& !queueLocked
|
||||||
|
);
|
||||||
const isSeriesBatchParentReview = Boolean(
|
const isSeriesBatchParentReview = Boolean(
|
||||||
mediaInfoReview?.seriesBatchParent
|
mediaInfoReview?.seriesBatchParent
|
||||||
|| pipeline?.context?.mediaInfoReview?.seriesBatchParent
|
|| pipeline?.context?.mediaInfoReview?.seriesBatchParent
|
||||||
@@ -3672,6 +3778,42 @@ export default function PipelineStatusCard({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showRipIssueNotice ? (
|
||||||
|
<div className="rip-error-notice" role="alert" aria-live="polite">
|
||||||
|
<div className="rip-error-notice-head">
|
||||||
|
<i className="pi pi-exclamation-triangle" aria-hidden="true" />
|
||||||
|
<strong>Hinweis: Beim Rip sind Fehler aufgetreten.</strong>
|
||||||
|
</div>
|
||||||
|
<small>{ripIssueSummaryLabel}. Bitte vor der Bestätigung von Playlist-/Spurauswahl prüfen.</small>
|
||||||
|
<div className="rip-error-notice-list">
|
||||||
|
{ripIssueVisibleEntries.map((entry, index) => (
|
||||||
|
<div key={`rip-issue-${entry.code ?? 'none'}-${index}`} className="rip-error-notice-item">
|
||||||
|
<span className="rip-error-notice-code">{entry.code !== null ? `MSG:${entry.code}` : 'Fehler'}</span>
|
||||||
|
<span className="rip-error-notice-message">{entry.message}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<small className="rip-error-notice-more">
|
||||||
|
{hiddenRipIssueCount > 0
|
||||||
|
? `+${hiddenRipIssueCount} weitere Meldung(en) im Job-Log.`
|
||||||
|
: 'Vollständige Details stehen im Job-Log.'}
|
||||||
|
</small>
|
||||||
|
{canTriggerRipRetry ? (
|
||||||
|
<div className="rip-error-notice-actions">
|
||||||
|
<Button
|
||||||
|
label="Retry Rippen"
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
severity="warning"
|
||||||
|
outlined
|
||||||
|
onClick={() => onRetry?.(retryJobId)}
|
||||||
|
loading={busy}
|
||||||
|
disabled={busy || queueLocked || !retryJobId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{rawDecisionRequiredBeforeStart && !queueLocked ? (
|
{rawDecisionRequiredBeforeStart && !queueLocked ? (
|
||||||
<div className="playlist-decision-block">
|
<div className="playlist-decision-block">
|
||||||
<h3>RAW Entscheidung erforderlich</h3>
|
<h3>RAW Entscheidung erforderlich</h3>
|
||||||
|
|||||||
@@ -1581,8 +1581,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
) {
|
) {
|
||||||
toastRef.current?.show({
|
toastRef.current?.show({
|
||||||
severity: 'warn',
|
severity: 'warn',
|
||||||
summary: `${deleteEntryOutputShortLabel}-Ordner auswählen`,
|
summary: `${deleteEntryOutputShortLabel}-Dateien auswählen`,
|
||||||
detail: `Bitte mindestens einen ${deleteEntryOutputShortLabel}-Ordner zum Löschen auswählen.`,
|
detail: `Bitte mindestens eine ${deleteEntryOutputShortLabel}-Datei zum Löschen auswählen.`,
|
||||||
life: 3500
|
life: 3500
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -2471,7 +2471,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4>{deleteEntryOutputShortLabel}</h4>
|
<h4>{deleteEntryOutputShortLabel} (Dateien)</h4>
|
||||||
{previewMoviePaths.length > 0 ? (() => {
|
{previewMoviePaths.length > 0 ? (() => {
|
||||||
return (
|
return (
|
||||||
<ul className="history-delete-preview-list">
|
<ul className="history-delete-preview-list">
|
||||||
@@ -2502,7 +2502,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
})() : (
|
})() : (
|
||||||
<small className="history-dv-subtle">Keine Movie-Pfade.</small>
|
<small className="history-dv-subtle">Keine Output-Dateien.</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1079,7 +1079,7 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
|
|||||||
tracks: (Array.isArray(existingContext.tracks) && existingContext.tracks.length > 0)
|
tracks: (Array.isArray(existingContext.tracks) && existingContext.tracks.length > 0)
|
||||||
? existingContext.tracks
|
? existingContext.tracks
|
||||||
: computedContext.tracks,
|
: computedContext.tracks,
|
||||||
selectedMetadata: existingContext.selectedMetadata || computedContext.selectedMetadata,
|
selectedMetadata: computedContext.selectedMetadata || existingContext.selectedMetadata,
|
||||||
canRestartEncodeFromLastSettings:
|
canRestartEncodeFromLastSettings:
|
||||||
existingContext.canRestartEncodeFromLastSettings ?? computedContext.canRestartEncodeFromLastSettings,
|
existingContext.canRestartEncodeFromLastSettings ?? computedContext.canRestartEncodeFromLastSettings,
|
||||||
canRestartReviewFromRaw:
|
canRestartReviewFromRaw:
|
||||||
@@ -1116,7 +1116,7 @@ function buildPipelineFromJob(job, currentPipeline, currentPipelineJobId) {
|
|||||||
tracks: (Array.isArray(liveContext.tracks) && liveContext.tracks.length > 0)
|
tracks: (Array.isArray(liveContext.tracks) && liveContext.tracks.length > 0)
|
||||||
? liveContext.tracks
|
? liveContext.tracks
|
||||||
: computedContext.tracks,
|
: computedContext.tracks,
|
||||||
selectedMetadata: liveContext.selectedMetadata || computedContext.selectedMetadata,
|
selectedMetadata: computedContext.selectedMetadata || liveContext.selectedMetadata,
|
||||||
cdRipConfig: liveContext.cdRipConfig || computedContext.cdRipConfig
|
cdRipConfig: liveContext.cdRipConfig || computedContext.cdRipConfig
|
||||||
}
|
}
|
||||||
: computedContext;
|
: computedContext;
|
||||||
|
|||||||
@@ -3705,6 +3705,74 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rip-error-notice {
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: 1px solid #d48c7c;
|
||||||
|
border-left: 4px solid #b6452c;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: #fff1ec;
|
||||||
|
color: #4e2418;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rip-error-notice-head {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rip-error-notice-head .pi {
|
||||||
|
color: #b6452c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rip-error-notice-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rip-error-notice-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.35rem 0.45rem;
|
||||||
|
border: 1px dashed #d8a79b;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: #fff8f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rip-error-notice-code {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 4.9rem;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
background: #b6452c;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rip-error-notice-message {
|
||||||
|
font-size: 0.83rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rip-error-notice-more {
|
||||||
|
color: #7a3a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rip-error-notice-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.playlist-decision-list {
|
.playlist-decision-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|||||||
+2
-1
@@ -645,7 +645,8 @@ apt-get install -y \
|
|||||||
util-linux udev \
|
util-linux udev \
|
||||||
ca-certificates gnupg \
|
ca-certificates gnupg \
|
||||||
lsb-release \
|
lsb-release \
|
||||||
mkvtoolnix
|
mkvtoolnix \
|
||||||
|
coreutils
|
||||||
|
|
||||||
ok "Basispakete installiert"
|
ok "Basispakete installiert"
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"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.15.0-2",
|
"version": "0.15.0-3",
|
||||||
"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