0.10.2-10 DVD Title Scan
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"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.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
'/select-metadata',
|
'/select-metadata',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
const { jobId, title, year, imdbId, poster, fromOmdb, selectedPlaylist } = req.body;
|
const { jobId, title, year, imdbId, poster, fromOmdb, selectedPlaylist, selectedHandBrakeTitleId } = req.body;
|
||||||
|
|
||||||
if (!jobId) {
|
if (!jobId) {
|
||||||
const error = new Error('jobId fehlt.');
|
const error = new Error('jobId fehlt.');
|
||||||
@@ -213,7 +213,8 @@ router.post(
|
|||||||
imdbId,
|
imdbId,
|
||||||
poster,
|
poster,
|
||||||
fromOmdb,
|
fromOmdb,
|
||||||
selectedPlaylist
|
selectedPlaylist,
|
||||||
|
selectedHandBrakeTitleId
|
||||||
});
|
});
|
||||||
|
|
||||||
const job = await pipelineService.selectMetadata({
|
const job = await pipelineService.selectMetadata({
|
||||||
@@ -223,7 +224,8 @@ router.post(
|
|||||||
imdbId,
|
imdbId,
|
||||||
poster,
|
poster,
|
||||||
fromOmdb,
|
fromOmdb,
|
||||||
selectedPlaylist
|
selectedPlaylist,
|
||||||
|
selectedHandBrakeTitleId
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ job });
|
res.json({ job });
|
||||||
|
|||||||
@@ -6959,9 +6959,9 @@ class PipelineService extends EventEmitter {
|
|||||||
return this.runMediainfoReviewForJob(jobId, rawPath, options);
|
return this.runMediainfoReviewForJob(jobId, rawPath, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectMetadata({ jobId, title, year, imdbId, poster, fromOmdb = null, selectedPlaylist = null }) {
|
async selectMetadata({ jobId, title, year, imdbId, poster, fromOmdb = null, selectedPlaylist = null, selectedHandBrakeTitleId = null }) {
|
||||||
this.ensureNotBusy('selectMetadata', jobId);
|
this.ensureNotBusy('selectMetadata', jobId);
|
||||||
logger.info('metadata:selected', { jobId, title, year, imdbId, poster, fromOmdb, selectedPlaylist });
|
logger.info('metadata:selected', { jobId, title, year, imdbId, poster, fromOmdb, selectedPlaylist, selectedHandBrakeTitleId });
|
||||||
|
|
||||||
const job = await historyService.getJobById(jobId);
|
const job = await historyService.getJobById(jobId);
|
||||||
if (!job) {
|
if (!job) {
|
||||||
@@ -6973,6 +6973,7 @@ class PipelineService extends EventEmitter {
|
|||||||
const mediaProfile = this.resolveMediaProfileForJob(job, { makemkvInfo: mkInfo });
|
const mediaProfile = this.resolveMediaProfileForJob(job, { makemkvInfo: mkInfo });
|
||||||
|
|
||||||
const normalizedSelectedPlaylist = normalizePlaylistId(selectedPlaylist);
|
const normalizedSelectedPlaylist = normalizePlaylistId(selectedPlaylist);
|
||||||
|
const normalizedSelectedHandBrakeTitleId = normalizeReviewTitleId(selectedHandBrakeTitleId);
|
||||||
const waitingForPlaylistSelection = (
|
const waitingForPlaylistSelection = (
|
||||||
job.status === 'WAITING_FOR_USER_DECISION'
|
job.status === 'WAITING_FOR_USER_DECISION'
|
||||||
|| job.last_state === 'WAITING_FOR_USER_DECISION'
|
|| job.last_state === 'WAITING_FOR_USER_DECISION'
|
||||||
@@ -6984,6 +6985,52 @@ class PipelineService extends EventEmitter {
|
|||||||
|| poster !== undefined
|
|| poster !== undefined
|
||||||
|| (fromOmdb !== null && fromOmdb !== undefined)
|
|| (fromOmdb !== null && fromOmdb !== undefined)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (normalizedSelectedHandBrakeTitleId && waitingForPlaylistSelection && job.raw_path && !hasExplicitMetadataPayload && !normalizedSelectedPlaylist) {
|
||||||
|
const currentMkInfo = mkInfo;
|
||||||
|
const currentAnalyzeContext = currentMkInfo?.analyzeContext || {};
|
||||||
|
const updatedMkInfo = this.withAnalyzeContextMediaProfile({
|
||||||
|
...currentMkInfo,
|
||||||
|
analyzeContext: {
|
||||||
|
...currentAnalyzeContext,
|
||||||
|
selectedHandBrakeTitleId: normalizedSelectedHandBrakeTitleId
|
||||||
|
}
|
||||||
|
}, mediaProfile);
|
||||||
|
|
||||||
|
await historyService.updateJob(jobId, {
|
||||||
|
status: 'MEDIAINFO_CHECK',
|
||||||
|
last_state: 'MEDIAINFO_CHECK',
|
||||||
|
error_message: null,
|
||||||
|
makemkv_info_json: JSON.stringify(updatedMkInfo),
|
||||||
|
mediainfo_info_json: null,
|
||||||
|
encode_plan_json: null,
|
||||||
|
encode_input_path: null,
|
||||||
|
encode_review_confirmed: 0
|
||||||
|
});
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'USER_ACTION',
|
||||||
|
`DVD Titel-Auswahl gesetzt: -t ${normalizedSelectedHandBrakeTitleId}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.runMediainfoReviewForJob(jobId, job.raw_path, {
|
||||||
|
mode: 'rip',
|
||||||
|
mediaProfile,
|
||||||
|
selectedHandBrakeTitleId: normalizedSelectedHandBrakeTitleId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('metadata:handbrake-title-selection:review-failed', {
|
||||||
|
jobId,
|
||||||
|
selectedHandBrakeTitleId: normalizedSelectedHandBrakeTitleId,
|
||||||
|
error: errorToMeta(error)
|
||||||
|
});
|
||||||
|
await this.failJob(jobId, 'MEDIAINFO_CHECK', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return historyService.getJobById(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
if (normalizedSelectedPlaylist && waitingForPlaylistSelection && job.raw_path && !hasExplicitMetadataPayload) {
|
if (normalizedSelectedPlaylist && waitingForPlaylistSelection && job.raw_path && !hasExplicitMetadataPayload) {
|
||||||
const currentMkInfo = mkInfo;
|
const currentMkInfo = mkInfo;
|
||||||
const currentAnalyzeContext = currentMkInfo?.analyzeContext || {};
|
const currentAnalyzeContext = currentMkInfo?.analyzeContext || {};
|
||||||
@@ -8222,9 +8269,156 @@ class PipelineService extends EventEmitter {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
enrichedReview = reviewPrefillResult.plan;
|
enrichedReview = reviewPrefillResult.plan;
|
||||||
|
|
||||||
|
// HandBrake title scan for DVD structures (VIDEO_TS directories / ISO images)
|
||||||
|
const needsHandBrakeTitleScan = rawMedia.source === 'dvd'
|
||||||
|
|| rawMedia.source === 'dvd_image'
|
||||||
|
|| rawMedia.source === 'single_extensionless';
|
||||||
|
if (needsHandBrakeTitleScan) {
|
||||||
|
const dvdScanInputPath = rawMedia.source === 'dvd' ? rawPath : mediaFiles[0].path;
|
||||||
|
const preSelectedHandBrakeTitleId = normalizeReviewTitleId(options?.selectedHandBrakeTitleId);
|
||||||
|
if (preSelectedHandBrakeTitleId) {
|
||||||
|
enrichedReview = {
|
||||||
|
...enrichedReview,
|
||||||
|
handBrakeTitleId: preSelectedHandBrakeTitleId,
|
||||||
|
encodeInputPath: dvdScanInputPath
|
||||||
|
};
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
`HandBrake Titel-Auswahl (manuell) übernommen: -t ${preSelectedHandBrakeTitleId}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
'HandBrake Titel-Scan für DVD-Struktur wird gestartet...'
|
||||||
|
);
|
||||||
|
await this.updateProgress('MEDIAINFO_CHECK', 100, null, 'HandBrake DVD Titel-Scan läuft', jobId);
|
||||||
|
const dvdTitleScanLines = [];
|
||||||
|
const dvdTitleScanConfig = await settingsService.buildHandBrakeScanConfigForInput(dvdScanInputPath, {
|
||||||
|
mediaProfile,
|
||||||
|
settingsMap: settings
|
||||||
|
});
|
||||||
|
logger.info('mediainfo:review:dvd-title-scan:command', {
|
||||||
|
jobId,
|
||||||
|
cmd: dvdTitleScanConfig.cmd,
|
||||||
|
args: dvdTitleScanConfig.args,
|
||||||
|
dvdScanInputPath
|
||||||
|
});
|
||||||
|
await this.runCommand({
|
||||||
|
jobId,
|
||||||
|
stage: 'MEDIAINFO_CHECK',
|
||||||
|
source: 'HANDBRAKE_SCAN_DVD_TITLES',
|
||||||
|
cmd: dvdTitleScanConfig.cmd,
|
||||||
|
args: dvdTitleScanConfig.args,
|
||||||
|
collectLines: dvdTitleScanLines,
|
||||||
|
collectStderrLines: false
|
||||||
|
});
|
||||||
|
const dvdTitleScanJson = parseMediainfoJsonOutput(dvdTitleScanLines.join('\n'));
|
||||||
|
if (dvdTitleScanJson) {
|
||||||
|
const allTitles = parseHandBrakeTitleList(dvdTitleScanJson);
|
||||||
|
const minLengthMinutes = Number(settings?.makemkv_min_length_minutes || 0);
|
||||||
|
const minLengthSeconds = Math.max(0, Math.round(minLengthMinutes * 60));
|
||||||
|
const titleCandidates = allTitles.filter((t) => t.durationSeconds >= minLengthSeconds);
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
`HandBrake DVD Titel-Scan: ${allTitles.length} gesamt, ${titleCandidates.length} ≥ ${minLengthMinutes} min.`
|
||||||
|
);
|
||||||
|
if (titleCandidates.length === 1) {
|
||||||
|
enrichedReview = {
|
||||||
|
...enrichedReview,
|
||||||
|
handBrakeTitleId: titleCandidates[0].handBrakeTitleId,
|
||||||
|
encodeInputPath: dvdScanInputPath
|
||||||
|
};
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
`HandBrake DVD Titel automatisch gewählt: -t ${titleCandidates[0].handBrakeTitleId}`
|
||||||
|
);
|
||||||
|
} else if (titleCandidates.length > 1) {
|
||||||
|
const candidatesData = titleCandidates.map((t) => ({
|
||||||
|
handBrakeTitleId: t.handBrakeTitleId,
|
||||||
|
durationSeconds: t.durationSeconds,
|
||||||
|
durationMinutes: Number((t.durationSeconds / 60).toFixed(1)),
|
||||||
|
audioTrackCount: t.audioTrackCount,
|
||||||
|
subtitleTrackCount: t.subtitleTrackCount,
|
||||||
|
sizeBytes: t.sizeBytes || 0
|
||||||
|
}));
|
||||||
|
enrichedReview = {
|
||||||
|
...enrichedReview,
|
||||||
|
handBrakeTitleDecisionRequired: true,
|
||||||
|
handBrakeTitleCandidates: candidatesData,
|
||||||
|
handBrakeDvdInputPath: dvdScanInputPath,
|
||||||
|
encodeInputPath: null
|
||||||
|
};
|
||||||
|
await historyService.updateJob(jobId, {
|
||||||
|
status: 'WAITING_FOR_USER_DECISION',
|
||||||
|
last_state: 'WAITING_FOR_USER_DECISION',
|
||||||
|
error_message: null,
|
||||||
|
mediainfo_info_json: JSON.stringify({
|
||||||
|
generatedAt: nowIso(),
|
||||||
|
files: mediaInfoRuns
|
||||||
|
}),
|
||||||
|
encode_plan_json: JSON.stringify(enrichedReview),
|
||||||
|
encode_input_path: null,
|
||||||
|
encode_review_confirmed: 0
|
||||||
|
});
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
`HandBrake DVD Titelauswahl erforderlich: ${titleCandidates.length} Kandidaten. Nutzer muss Titel wählen.`
|
||||||
|
);
|
||||||
|
if (this.isPrimaryJob(jobId)) {
|
||||||
|
await this.setState('WAITING_FOR_USER_DECISION', {
|
||||||
|
activeJobId: jobId,
|
||||||
|
progress: 0,
|
||||||
|
eta: null,
|
||||||
|
statusText: `DVD Titelauswahl: ${titleCandidates.length} Kandidaten gefunden`,
|
||||||
|
context: {
|
||||||
|
jobId,
|
||||||
|
rawPath,
|
||||||
|
handBrakeTitleDecisionRequired: true,
|
||||||
|
handBrakeTitleCandidates: candidatesData,
|
||||||
|
handBrakeDvdInputPath: dvdScanInputPath,
|
||||||
|
reviewConfirmed: false,
|
||||||
|
mode: options.mode || 'rip',
|
||||||
|
mediaProfile,
|
||||||
|
sourceJobId: options.sourceJobId || null,
|
||||||
|
mediaInfoReview: enrichedReview,
|
||||||
|
selectedMetadata
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
void this.notifyPushover('metadata_ready', {
|
||||||
|
title: 'Ripster - DVD Titelauswahl',
|
||||||
|
message: `Job #${jobId}: ${titleCandidates.length} DVD-Titel gefunden, manuelle Auswahl erforderlich`
|
||||||
|
});
|
||||||
|
return enrichedReview;
|
||||||
|
} else {
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
`HandBrake DVD Titel-Scan: Kein Titel ≥ ${minLengthMinutes} min. Ohne Titel-ID fortgefahren.`
|
||||||
|
);
|
||||||
|
enrichedReview = { ...enrichedReview, encodeInputPath: dvdScanInputPath };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('mediainfo:review:dvd-title-scan:parse-failed', { jobId });
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
'HandBrake DVD Titel-Scan: Ausgabe nicht lesbar. Ohne Titel-ID fortgefahren.'
|
||||||
|
);
|
||||||
|
enrichedReview = { ...enrichedReview, encodeInputPath: dvdScanInputPath };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hasEncodableTitle = Boolean(enrichedReview.encodeInputPath);
|
const hasEncodableTitle = Boolean(enrichedReview.encodeInputPath);
|
||||||
const titleSelectionRequired = Boolean(enrichedReview.titleSelectionRequired);
|
const titleSelectionRequired = Boolean(enrichedReview.titleSelectionRequired);
|
||||||
if (!hasEncodableTitle && !titleSelectionRequired) {
|
if (!hasEncodableTitle && !titleSelectionRequired && !enrichedReview.handBrakeTitleDecisionRequired) {
|
||||||
enrichedReview.notes = [
|
enrichedReview.notes = [
|
||||||
...(Array.isArray(enrichedReview.notes) ? enrichedReview.notes : []),
|
...(Array.isArray(enrichedReview.notes) ? enrichedReview.notes : []),
|
||||||
'Kein Titel erfüllt aktuell MIN_LENGTH_MINUTES. Bitte Konfiguration prüfen.'
|
'Kein Titel erfüllt aktuell MIN_LENGTH_MINUTES. Bitte Konfiguration prüfen.'
|
||||||
@@ -8991,7 +9185,61 @@ class PipelineService extends EventEmitter {
|
|||||||
`HandBrake Titel-Mapping: ${selectedPlaylistId}.mpls -> -t ${handBrakeTitleId}`
|
`HandBrake Titel-Mapping: ${selectedPlaylistId}.mpls -> -t ${handBrakeTitleId}`
|
||||||
);
|
);
|
||||||
} else if (!handBrakeTitleId) {
|
} else if (!handBrakeTitleId) {
|
||||||
handBrakeTitleId = normalizeReviewTitleId(encodePlan?.handBrakeTitleId ?? encodePlan?.encodeInputTitleId);
|
const dvdResolveScanLines = [];
|
||||||
|
const dvdResolveScanConfig = await settingsService.buildHandBrakeScanConfigForInput(inputPath, {
|
||||||
|
mediaProfile,
|
||||||
|
settingsMap: settings
|
||||||
|
});
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
'HandBrake Titel-Scan für Verzeichnis ohne Playlist wird gestartet...'
|
||||||
|
);
|
||||||
|
logger.info('encoding:dvd-title-resolve-scan:command', {
|
||||||
|
jobId,
|
||||||
|
cmd: dvdResolveScanConfig.cmd,
|
||||||
|
args: dvdResolveScanConfig.args
|
||||||
|
});
|
||||||
|
const dvdResolveRunInfo = await this.runCommand({
|
||||||
|
jobId,
|
||||||
|
stage: 'ENCODING',
|
||||||
|
source: 'HANDBRAKE_SCAN_TITLE_RESOLVE',
|
||||||
|
cmd: dvdResolveScanConfig.cmd,
|
||||||
|
args: dvdResolveScanConfig.args,
|
||||||
|
collectLines: dvdResolveScanLines,
|
||||||
|
collectStderrLines: false
|
||||||
|
});
|
||||||
|
const dvdResolveParsed = parseMediainfoJsonOutput(dvdResolveScanLines.join('\n'));
|
||||||
|
if (dvdResolveParsed) {
|
||||||
|
const dvdTitleInfo = parseHandBrakeSelectedTitleInfo(dvdResolveParsed, {
|
||||||
|
handBrakeTitleId: null,
|
||||||
|
playlistId: null
|
||||||
|
});
|
||||||
|
if (dvdTitleInfo?.handBrakeTitleId) {
|
||||||
|
handBrakeTitleId = dvdTitleInfo.handBrakeTitleId;
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
`HandBrake Titel-Scan: längster Titel gefunden -> -t ${handBrakeTitleId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn('encoding:dvd-title-resolve-scan:parse-failed', {
|
||||||
|
jobId,
|
||||||
|
runInfo: dvdResolveRunInfo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handBrakeTitleId) {
|
||||||
|
const fallbackTitleId = normalizeReviewTitleId(encodePlan?.handBrakeTitleId);
|
||||||
|
if (fallbackTitleId) {
|
||||||
|
handBrakeTitleId = fallbackTitleId;
|
||||||
|
await historyService.appendLog(
|
||||||
|
jobId,
|
||||||
|
'SYSTEM',
|
||||||
|
`HandBrake Titel-ID aus Encode-Plan übernommen: -t ${handBrakeTitleId}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handBrakeConfig = await settingsService.buildHandBrakeConfig(inputPath, incompleteOutputPath, {
|
const handBrakeConfig = await settingsService.buildHandBrakeConfig(inputPath, incompleteOutputPath, {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"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.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -706,7 +706,13 @@ export const api = {
|
|||||||
return request('/downloads/summary');
|
return request('/downloads/summary');
|
||||||
},
|
},
|
||||||
downloadPreparedArchive(downloadId) {
|
downloadPreparedArchive(downloadId) {
|
||||||
return download(`/downloads/${encodeURIComponent(downloadId)}/file`);
|
const link = document.createElement('a');
|
||||||
|
link.href = `${API_BASE}/downloads/${encodeURIComponent(downloadId)}/file`;
|
||||||
|
link.download = '';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
deleteDownload(downloadId) {
|
deleteDownload(downloadId) {
|
||||||
return request(`/downloads/${encodeURIComponent(downloadId)}`, {
|
return request(`/downloads/${encodeURIComponent(downloadId)}`, {
|
||||||
|
|||||||
@@ -547,6 +547,23 @@ export default function JobDetailDialog({
|
|||||||
: null;
|
: null;
|
||||||
const encodePlanUserPresetId = Number(encodePlanUserPreset?.id);
|
const encodePlanUserPresetId = Number(encodePlanUserPreset?.id);
|
||||||
const reviewUserPresets = encodePlanUserPreset ? [encodePlanUserPreset] : [];
|
const reviewUserPresets = encodePlanUserPreset ? [encodePlanUserPreset] : [];
|
||||||
|
const encodePlanManualTrackSelection = job?.encodePlan?.manualTrackSelection
|
||||||
|
&& typeof job.encodePlan.manualTrackSelection === 'object'
|
||||||
|
? job.encodePlan.manualTrackSelection
|
||||||
|
: null;
|
||||||
|
const reviewTrackSelectionByTitle = encodePlanManualTrackSelection?.titleId != null
|
||||||
|
? {
|
||||||
|
[encodePlanManualTrackSelection.titleId]: {
|
||||||
|
audioTrackIds: Array.isArray(encodePlanManualTrackSelection.audioTrackIds)
|
||||||
|
? encodePlanManualTrackSelection.audioTrackIds
|
||||||
|
: [],
|
||||||
|
subtitleTrackIds: Array.isArray(encodePlanManualTrackSelection.subtitleTrackIds)
|
||||||
|
? encodePlanManualTrackSelection.subtitleTrackIds
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
const reviewSelectedEncodeTitleId = job?.encodePlan?.encodeInputTitleId ?? null;
|
||||||
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
||||||
const canDownloadRaw = Boolean(job?.raw_path && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
const canDownloadRaw = Boolean(job?.raw_path && 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');
|
||||||
@@ -846,6 +863,8 @@ export default function JobDetailDialog({
|
|||||||
<MediaInfoReviewPanel
|
<MediaInfoReviewPanel
|
||||||
review={job.encodePlan}
|
review={job.encodePlan}
|
||||||
commandOutputPath={job.output_path || null}
|
commandOutputPath={job.output_path || null}
|
||||||
|
selectedEncodeTitleId={reviewSelectedEncodeTitleId}
|
||||||
|
trackSelectionByTitle={reviewTrackSelectionByTitle}
|
||||||
availableScripts={configuredSelection.scriptCatalog}
|
availableScripts={configuredSelection.scriptCatalog}
|
||||||
availableChains={configuredSelection.chainCatalog}
|
availableChains={configuredSelection.chainCatalog}
|
||||||
preEncodeItems={reviewPreEncodeItems}
|
preEncodeItems={reviewPreEncodeItems}
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ export default function PipelineStatusCard({
|
|||||||
onRestartReview,
|
onRestartReview,
|
||||||
onConfirmReview,
|
onConfirmReview,
|
||||||
onSelectPlaylist,
|
onSelectPlaylist,
|
||||||
|
onSelectHandBrakeTitle,
|
||||||
onCancel,
|
onCancel,
|
||||||
onRetry,
|
onRetry,
|
||||||
isQueued = false,
|
isQueued = false,
|
||||||
@@ -331,6 +332,7 @@ export default function PipelineStatusCard({
|
|||||||
const jobMediaProfile = resolvePipelineMediaProfile(pipeline, mediaInfoReview);
|
const jobMediaProfile = resolvePipelineMediaProfile(pipeline, mediaInfoReview);
|
||||||
const [selectedEncodeTitleId, setSelectedEncodeTitleId] = useState(null);
|
const [selectedEncodeTitleId, setSelectedEncodeTitleId] = useState(null);
|
||||||
const [selectedPlaylistId, setSelectedPlaylistId] = useState(null);
|
const [selectedPlaylistId, setSelectedPlaylistId] = useState(null);
|
||||||
|
const [selectedHandBrakeTitleIdState, setSelectedHandBrakeTitleIdState] = useState(null);
|
||||||
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
|
const [trackSelectionByTitle, setTrackSelectionByTitle] = useState({});
|
||||||
const [settingsMap, setSettingsMap] = useState({});
|
const [settingsMap, setSettingsMap] = useState({});
|
||||||
const [presetDisplayMap, setPresetDisplayMap] = useState({});
|
const [presetDisplayMap, setPresetDisplayMap] = useState({});
|
||||||
@@ -527,6 +529,44 @@ export default function PipelineStatusCard({
|
|||||||
return dedup;
|
return dedup;
|
||||||
}, [playlistAnalysis, pipeline?.context?.playlistCandidates]);
|
}, [playlistAnalysis, pipeline?.context?.playlistCandidates]);
|
||||||
|
|
||||||
|
const handBrakeTitleDecisionRequired = state === 'WAITING_FOR_USER_DECISION'
|
||||||
|
&& Boolean(pipeline?.context?.handBrakeTitleDecisionRequired);
|
||||||
|
|
||||||
|
const waitingHandBrakeTitleRows = useMemo(() => {
|
||||||
|
const raw = Array.isArray(pipeline?.context?.handBrakeTitleCandidates)
|
||||||
|
? pipeline.context.handBrakeTitleCandidates
|
||||||
|
: [];
|
||||||
|
return raw
|
||||||
|
.map((item) => {
|
||||||
|
const titleId = Number(item?.handBrakeTitleId);
|
||||||
|
if (!Number.isFinite(titleId) || titleId <= 0) return null;
|
||||||
|
const durationSeconds = Number(item?.durationSeconds || 0);
|
||||||
|
const minutes = Math.floor(durationSeconds / 60);
|
||||||
|
const seconds = durationSeconds % 60;
|
||||||
|
const durationLabel = `${minutes}:${String(seconds).padStart(2, '0')} min`;
|
||||||
|
return {
|
||||||
|
handBrakeTitleId: Math.trunc(titleId),
|
||||||
|
durationSeconds,
|
||||||
|
durationLabel,
|
||||||
|
durationMinutes: Number(item?.durationMinutes || 0),
|
||||||
|
audioTrackCount: Number(item?.audioTrackCount || 0),
|
||||||
|
subtitleTrackCount: Number(item?.subtitleTrackCount || 0),
|
||||||
|
sizeBytes: Number(item?.sizeBytes || 0)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((a, b) => b.durationSeconds - a.durationSeconds);
|
||||||
|
}, [pipeline?.context?.handBrakeTitleCandidates]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!handBrakeTitleDecisionRequired) {
|
||||||
|
setSelectedHandBrakeTitleIdState(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const best = waitingHandBrakeTitleRows[0]?.handBrakeTitleId || null;
|
||||||
|
setSelectedHandBrakeTitleIdState(best);
|
||||||
|
}, [handBrakeTitleDecisionRequired, retryJobId, waitingHandBrakeTitleRows]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state !== 'WAITING_FOR_USER_DECISION') {
|
if (state !== 'WAITING_FOR_USER_DECISION') {
|
||||||
setSelectedPlaylistId(null);
|
setSelectedPlaylistId(null);
|
||||||
@@ -551,7 +591,7 @@ export default function PipelineStatusCard({
|
|||||||
pipeline?.context?.selectedPlaylist
|
pipeline?.context?.selectedPlaylist
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const playlistDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION';
|
const playlistDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && !handBrakeTitleDecisionRequired;
|
||||||
const commandOutputPath = useMemo(
|
const commandOutputPath = useMemo(
|
||||||
() => buildOutputPathPreview(settingsMap, jobMediaProfile, selectedMetadata, retryJobId),
|
() => buildOutputPathPreview(settingsMap, jobMediaProfile, selectedMetadata, retryJobId),
|
||||||
[settingsMap, jobMediaProfile, selectedMetadata, retryJobId]
|
[settingsMap, jobMediaProfile, selectedMetadata, retryJobId]
|
||||||
@@ -696,6 +736,18 @@ export default function PipelineStatusCard({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{handBrakeTitleDecisionRequired && retryJobId && (
|
||||||
|
<Button
|
||||||
|
label="DVD Titel bestätigen"
|
||||||
|
icon="pi pi-check"
|
||||||
|
severity="warning"
|
||||||
|
outlined
|
||||||
|
onClick={() => onSelectHandBrakeTitle?.(retryJobId, selectedHandBrakeTitleIdState)}
|
||||||
|
loading={busy}
|
||||||
|
disabled={!selectedHandBrakeTitleIdState}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{state === 'READY_TO_ENCODE' && retryJobId ? (
|
{state === 'READY_TO_ENCODE' && retryJobId ? (
|
||||||
<Button
|
<Button
|
||||||
label={isPreRipReview ? 'Backup + Encoding starten' : 'Encoding starten'}
|
label={isPreRipReview ? 'Backup + Encoding starten' : 'Encoding starten'}
|
||||||
@@ -851,6 +903,42 @@ export default function PipelineStatusCard({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{handBrakeTitleDecisionRequired && !queueLocked ? (
|
||||||
|
<div className="playlist-decision-block">
|
||||||
|
<h3>DVD Titel-Auswahl erforderlich</h3>
|
||||||
|
<small>
|
||||||
|
Mehrere DVD-Titel gefunden, die die Mindestlänge erfüllen. Bitte den gewünschten Titel auswählen.
|
||||||
|
</small>
|
||||||
|
{waitingHandBrakeTitleRows.length > 0 ? (
|
||||||
|
<div className="playlist-decision-list">
|
||||||
|
{waitingHandBrakeTitleRows.map((row) => (
|
||||||
|
<div key={row.handBrakeTitleId} className="playlist-decision-item">
|
||||||
|
<label className="readonly-check-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedHandBrakeTitleIdState === row.handBrakeTitleId}
|
||||||
|
disabled={queueLocked}
|
||||||
|
onChange={() => {
|
||||||
|
const next = selectedHandBrakeTitleIdState === row.handBrakeTitleId ? null : row.handBrakeTitleId;
|
||||||
|
setSelectedHandBrakeTitleIdState(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{`Titel -t ${row.handBrakeTitleId}`}
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{selectedMetadata ? (
|
{selectedMetadata ? (
|
||||||
<div className="pipeline-meta-inline">
|
<div className="pipeline-meta-inline">
|
||||||
{selectedMetadata.poster ? (
|
{selectedMetadata.poster ? (
|
||||||
|
|||||||
@@ -1611,6 +1611,24 @@ export default function DashboardPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectHandBrakeTitle = async (jobId, selectedHandBrakeTitleId = null) => {
|
||||||
|
const normalizedJobId = normalizeJobId(jobId);
|
||||||
|
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
||||||
|
try {
|
||||||
|
await api.selectMetadata({
|
||||||
|
jobId,
|
||||||
|
selectedHandBrakeTitleId: selectedHandBrakeTitleId || null
|
||||||
|
});
|
||||||
|
await refreshPipeline();
|
||||||
|
await loadDashboardJobs();
|
||||||
|
setExpandedJobId(normalizedJobId);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error);
|
||||||
|
} finally {
|
||||||
|
if (normalizedJobId) setJobBusy(normalizedJobId, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleRetry = async (jobId) => {
|
const handleRetry = async (jobId) => {
|
||||||
const normalizedJobId = normalizeJobId(jobId);
|
const normalizedJobId = normalizeJobId(jobId);
|
||||||
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
if (normalizedJobId) setJobBusy(normalizedJobId, true);
|
||||||
@@ -2095,8 +2113,7 @@ export default function DashboardPage({
|
|||||||
<div className="page-grid">
|
<div className="page-grid">
|
||||||
<Toast ref={toastRef} />
|
<Toast ref={toastRef} />
|
||||||
|
|
||||||
<div className="dashboard-3col-grid">
|
{monitoringState.enabled ? (
|
||||||
<div className="dashboard-col dashboard-col-left">
|
|
||||||
<Card title="Hardware Monitoring">
|
<Card title="Hardware Monitoring">
|
||||||
<div className="hardware-monitor-head">
|
<div className="hardware-monitor-head">
|
||||||
<Tag value={`Letztes Update: ${formatUpdatedAt(monitoringState.updatedAt)}`} severity="warning" />
|
<Tag value={`Letztes Update: ${formatUpdatedAt(monitoringState.updatedAt)}`} severity="warning" />
|
||||||
@@ -2253,6 +2270,78 @@ export default function DashboardPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="dashboard-3col-grid">
|
||||||
|
<div className="dashboard-col dashboard-col-left">
|
||||||
|
<Card title="Audiobook Upload">
|
||||||
|
<FileUpload
|
||||||
|
ref={audiobookFileUploadRef}
|
||||||
|
accept=".aax"
|
||||||
|
maxFileSize={10737418240}
|
||||||
|
customUpload
|
||||||
|
uploadHandler={() => void handleAudiobookUpload()}
|
||||||
|
disabled={audiobookUploadBusy}
|
||||||
|
onSelect={(e) => setAudiobookUploadFile(e.files[0] || null)}
|
||||||
|
onClear={() => setAudiobookUploadFile(null)}
|
||||||
|
onRemove={() => setAudiobookUploadFile(null)}
|
||||||
|
chooseOptions={{ icon: 'pi pi-images', iconOnly: true, className: 'p-button-rounded p-button-outlined' }}
|
||||||
|
uploadOptions={{ icon: 'pi pi-cloud-upload', iconOnly: true, className: 'p-button-rounded p-button-outlined p-button-success' }}
|
||||||
|
cancelOptions={{ icon: 'pi pi-times', iconOnly: true, className: 'p-button-rounded p-button-outlined p-button-danger' }}
|
||||||
|
itemTemplate={(file, options) => (
|
||||||
|
<div className="aax-file-item">
|
||||||
|
<i className="pi pi-headphones aax-file-icon" />
|
||||||
|
<div className="aax-file-info">
|
||||||
|
<span className="aax-file-name" title={file.name}>{file.name}</span>
|
||||||
|
<small>{options.formatSize}</small>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-times"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
severity="danger"
|
||||||
|
size="small"
|
||||||
|
onClick={options.onRemove}
|
||||||
|
disabled={audiobookUploadBusy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
emptyTemplate={() => (
|
||||||
|
<div className="aax-drop-zone">
|
||||||
|
<i className="pi pi-headphones aax-drop-icon" />
|
||||||
|
<p>AAX-Datei hier ablegen</p>
|
||||||
|
<small>oder oben „Auswählen" klicken</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{audiobookUploadStatusVisible ? (
|
||||||
|
<div className={`audiobook-upload-status tone-${audiobookUploadStatusTone}`}>
|
||||||
|
<div className="audiobook-upload-status-head">
|
||||||
|
<strong>{audiobookUploadStatusLabel}</strong>
|
||||||
|
<Tag value={audiobookUploadStatusLabel} severity={audiobookUploadStatusTone} />
|
||||||
|
</div>
|
||||||
|
{audiobookUpload?.statusText ? <small>{audiobookUpload.statusText}</small> : null}
|
||||||
|
{audiobookUploadFileName ? (
|
||||||
|
<small className="audiobook-upload-file" title={audiobookUploadFileName}>
|
||||||
|
Datei: {audiobookUploadFileName}
|
||||||
|
</small>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className="dashboard-job-row-progress audiobook-upload-progress"
|
||||||
|
aria-label={`Audiobook Upload ${Math.round(audiobookUploadProgress)} Prozent`}
|
||||||
|
>
|
||||||
|
<ProgressBar value={audiobookUploadProgress} showValue={false} />
|
||||||
|
<small>
|
||||||
|
{audiobookUploadPhase === 'processing'
|
||||||
|
? '100% | Upload fertig, Job wird vorbereitet ...'
|
||||||
|
: audiobookUploadTotalBytes > 0
|
||||||
|
? `${Math.round(audiobookUploadProgress)}% | ${formatBytes(audiobookUploadLoadedBytes)} / ${formatBytes(audiobookUploadTotalBytes)}`
|
||||||
|
: `${Math.round(audiobookUploadProgress)}%`}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card title="Disk-Information">
|
<Card title="Disk-Information">
|
||||||
<div className="actions-row">
|
<div className="actions-row">
|
||||||
@@ -2452,6 +2541,7 @@ export default function DashboardPage({
|
|||||||
onRestartReview={handleRestartReviewFromRaw}
|
onRestartReview={handleRestartReviewFromRaw}
|
||||||
onConfirmReview={handleConfirmReview}
|
onConfirmReview={handleConfirmReview}
|
||||||
onSelectPlaylist={handleSelectPlaylist}
|
onSelectPlaylist={handleSelectPlaylist}
|
||||||
|
onSelectHandBrakeTitle={handleSelectHandBrakeTitle}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
onRetry={handleRetry}
|
onRetry={handleRetry}
|
||||||
onRemoveFromQueue={handleRemoveQueuedJob}
|
onRemoveFromQueue={handleRemoveQueuedJob}
|
||||||
@@ -2525,75 +2615,6 @@ export default function DashboardPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dashboard-col dashboard-col-right">
|
<div className="dashboard-col dashboard-col-right">
|
||||||
<Card title="Audiobook Upload">
|
|
||||||
<FileUpload
|
|
||||||
ref={audiobookFileUploadRef}
|
|
||||||
accept=".aax"
|
|
||||||
maxFileSize={10737418240}
|
|
||||||
customUpload
|
|
||||||
uploadHandler={() => void handleAudiobookUpload()}
|
|
||||||
disabled={audiobookUploadBusy}
|
|
||||||
onSelect={(e) => setAudiobookUploadFile(e.files[0] || null)}
|
|
||||||
onClear={() => setAudiobookUploadFile(null)}
|
|
||||||
onRemove={() => setAudiobookUploadFile(null)}
|
|
||||||
chooseOptions={{ icon: 'pi pi-images', iconOnly: true, className: 'p-button-rounded p-button-outlined' }}
|
|
||||||
uploadOptions={{ icon: 'pi pi-cloud-upload', iconOnly: true, className: 'p-button-rounded p-button-outlined p-button-success' }}
|
|
||||||
cancelOptions={{ icon: 'pi pi-times', iconOnly: true, className: 'p-button-rounded p-button-outlined p-button-danger' }}
|
|
||||||
itemTemplate={(file, options) => (
|
|
||||||
<div className="aax-file-item">
|
|
||||||
<i className="pi pi-headphones aax-file-icon" />
|
|
||||||
<div className="aax-file-info">
|
|
||||||
<span className="aax-file-name" title={file.name}>{file.name}</span>
|
|
||||||
<small>{options.formatSize}</small>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
icon="pi pi-times"
|
|
||||||
text
|
|
||||||
rounded
|
|
||||||
severity="danger"
|
|
||||||
size="small"
|
|
||||||
onClick={options.onRemove}
|
|
||||||
disabled={audiobookUploadBusy}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
emptyTemplate={() => (
|
|
||||||
<div className="aax-drop-zone">
|
|
||||||
<i className="pi pi-headphones aax-drop-icon" />
|
|
||||||
<p>AAX-Datei hier ablegen</p>
|
|
||||||
<small>oder oben „Auswählen" klicken</small>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{audiobookUploadStatusVisible ? (
|
|
||||||
<div className={`audiobook-upload-status tone-${audiobookUploadStatusTone}`}>
|
|
||||||
<div className="audiobook-upload-status-head">
|
|
||||||
<strong>{audiobookUploadStatusLabel}</strong>
|
|
||||||
<Tag value={audiobookUploadStatusLabel} severity={audiobookUploadStatusTone} />
|
|
||||||
</div>
|
|
||||||
{audiobookUpload?.statusText ? <small>{audiobookUpload.statusText}</small> : null}
|
|
||||||
{audiobookUploadFileName ? (
|
|
||||||
<small className="audiobook-upload-file" title={audiobookUploadFileName}>
|
|
||||||
Datei: {audiobookUploadFileName}
|
|
||||||
</small>
|
|
||||||
) : null}
|
|
||||||
<div
|
|
||||||
className="dashboard-job-row-progress audiobook-upload-progress"
|
|
||||||
aria-label={`Audiobook Upload ${Math.round(audiobookUploadProgress)} Prozent`}
|
|
||||||
>
|
|
||||||
<ProgressBar value={audiobookUploadProgress} showValue={false} />
|
|
||||||
<small>
|
|
||||||
{audiobookUploadPhase === 'processing'
|
|
||||||
? '100% | Upload fertig, Job wird vorbereitet ...'
|
|
||||||
: audiobookUploadTotalBytes > 0
|
|
||||||
? `${Math.round(audiobookUploadProgress)}% | ${formatBytes(audiobookUploadLoadedBytes)} / ${formatBytes(audiobookUploadTotalBytes)}`
|
|
||||||
: `${Math.round(audiobookUploadProgress)}%`}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="Job Queue" subTitle="Elemente können per Drag-and-Drop umsortiert werden.">
|
<Card title="Job Queue" subTitle="Elemente können per Drag-and-Drop umsortiert werden.">
|
||||||
<div className="pipeline-queue-meta">
|
<div className="pipeline-queue-meta">
|
||||||
<Tag value={`Film max.: ${queueState?.maxParallelJobs || 1}`} severity="info" />
|
<Tag value={`Film max.: ${queueState?.maxParallelJobs || 1}`} severity="info" />
|
||||||
|
|||||||
@@ -1056,11 +1056,12 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
void openDetail(row);
|
void openDetail(row);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="history-dv-grid-poster-wrap">
|
|
||||||
{renderPoster(row, 'history-dv-poster-grid')}
|
|
||||||
<div className="history-dv-grid-status-overlay">
|
<div className="history-dv-grid-status-overlay">
|
||||||
{renderStatusTag(row)}
|
{renderStatusTag(row)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="history-dv-grid-poster-wrap">
|
||||||
|
{renderPoster(row, 'history-dv-poster-grid')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="history-dv-grid-main">
|
<div className="history-dv-grid-main">
|
||||||
|
|||||||
@@ -2258,6 +2258,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-item-grid {
|
.history-dv-item-grid {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.65rem;
|
padding: 0.65rem;
|
||||||
@@ -2266,22 +2267,19 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-grid-poster-wrap {
|
.history-dv-grid-poster-wrap {
|
||||||
position: relative;
|
|
||||||
width: min(120px, 100%);
|
width: min(120px, 100%);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-grid-status-overlay {
|
.history-dv-grid-status-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.4rem;
|
top: 0.5rem;
|
||||||
left: 0;
|
left: 0.5rem;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-grid-status-overlay .p-tag {
|
.history-dv-grid-status-overlay .p-tag {
|
||||||
pointer-events: auto;
|
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-dv-grid-main {
|
.history-dv-grid-main {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"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.10.2-9",
|
"version": "0.10.2-10",
|
||||||
"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