0.10.2 Download Files
This commit is contained in:
978
backend/package-lock.json
generated
978
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.10.1-1",
|
"version": "0.10.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"dev": "nodemon src/index.js"
|
"dev": "nodemon src/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const archiver = require('archiver');
|
||||||
const asyncHandler = require('../middleware/asyncHandler');
|
const asyncHandler = require('../middleware/asyncHandler');
|
||||||
const historyService = require('../services/historyService');
|
const historyService = require('../services/historyService');
|
||||||
const pipelineService = require('../services/pipelineService');
|
const pipelineService = require('../services/pipelineService');
|
||||||
const logger = require('../services/logger').child('HISTORY_ROUTE');
|
const logger = require('../services/logger').child('HISTORY_ROUTE');
|
||||||
|
const { errorToMeta } = require('../utils/errorMeta');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -180,6 +182,72 @@ router.post(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:id/download',
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
const id = Number(req.params.id);
|
||||||
|
const target = String(req.query.target || '').trim();
|
||||||
|
const descriptor = await historyService.getJobArchiveDescriptor(id, target);
|
||||||
|
|
||||||
|
logger.info('get:job:download', {
|
||||||
|
reqId: req.reqId,
|
||||||
|
id,
|
||||||
|
target: descriptor.target,
|
||||||
|
sourceType: descriptor.sourceType,
|
||||||
|
sourcePath: descriptor.sourcePath
|
||||||
|
});
|
||||||
|
|
||||||
|
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||||
|
|
||||||
|
const handleArchiveError = (error) => {
|
||||||
|
logger.error('get:job:download:failed', {
|
||||||
|
reqId: req.reqId,
|
||||||
|
id,
|
||||||
|
target: descriptor.target,
|
||||||
|
error: errorToMeta(error)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: {
|
||||||
|
message: 'ZIP-Download fehlgeschlagen.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.destroy(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
archive.on('warning', handleArchiveError);
|
||||||
|
archive.on('error', handleArchiveError);
|
||||||
|
res.on('close', () => {
|
||||||
|
if (!res.writableEnded) {
|
||||||
|
archive.abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/zip');
|
||||||
|
res.attachment(descriptor.archiveName);
|
||||||
|
archive.pipe(res);
|
||||||
|
|
||||||
|
if (descriptor.sourceType === 'directory') {
|
||||||
|
archive.directory(descriptor.sourcePath, descriptor.entryName);
|
||||||
|
} else {
|
||||||
|
archive.file(descriptor.sourcePath, { name: descriptor.entryName });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const finalizeResult = archive.finalize();
|
||||||
|
if (finalizeResult && typeof finalizeResult.catch === 'function') {
|
||||||
|
finalizeResult.catch(handleArchiveError);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleArchiveError(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:id',
|
'/:id',
|
||||||
asyncHandler(async (req, res) => {
|
asyncHandler(async (req, res) => {
|
||||||
|
|||||||
@@ -764,6 +764,36 @@ function normalizeJobIdValue(value) {
|
|||||||
return Math.trunc(parsed);
|
return Math.trunc(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeArchiveTarget(value) {
|
||||||
|
const raw = String(value || '').trim().toLowerCase();
|
||||||
|
if (raw === 'raw') {
|
||||||
|
return 'raw';
|
||||||
|
}
|
||||||
|
if (raw === 'output' || raw === 'movie' || raw === 'encode') {
|
||||||
|
return 'output';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeArchiveNamePart(value, fallback = 'job') {
|
||||||
|
const normalized = String(value || '')
|
||||||
|
.normalize('NFKD')
|
||||||
|
.replace(/[^\x00-\x7F]+/g, '');
|
||||||
|
const safe = normalized
|
||||||
|
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.replace(/^[_-]+|[_-]+$/g, '')
|
||||||
|
.slice(0, 80);
|
||||||
|
return safe || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildJobArchiveName(job, target) {
|
||||||
|
const jobId = normalizeJobIdValue(job?.id) || 'unknown';
|
||||||
|
const titlePart = sanitizeArchiveNamePart(job?.title || job?.detected_title || '', 'job');
|
||||||
|
const targetPart = target === 'raw' ? 'raw' : 'encode';
|
||||||
|
return `job-${jobId}-${titlePart}-${targetPart}.zip`;
|
||||||
|
}
|
||||||
|
|
||||||
function parseSourceJobIdFromPlan(encodePlanRaw) {
|
function parseSourceJobIdFromPlan(encodePlanRaw) {
|
||||||
const plan = parseInfoFromValue(encodePlanRaw, null);
|
const plan = parseInfoFromValue(encodePlanRaw, null);
|
||||||
const sourceJobId = normalizeJobIdValue(plan?.sourceJobId);
|
const sourceJobId = normalizeJobIdValue(plan?.sourceJobId);
|
||||||
@@ -1427,6 +1457,76 @@ class HistoryService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getJobArchiveDescriptor(jobId, target) {
|
||||||
|
const normalizedJobId = normalizeJobIdValue(jobId);
|
||||||
|
if (!normalizedJobId) {
|
||||||
|
const error = new Error('Ungültige Job-ID.');
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTarget = normalizeArchiveTarget(target);
|
||||||
|
if (!normalizedTarget) {
|
||||||
|
const error = new Error('Ungültiges Download-Ziel. Erlaubt sind raw und output.');
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [job, settings] = await Promise.all([
|
||||||
|
this.getJobById(normalizedJobId),
|
||||||
|
settingsService.getSettingsMap()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!job) {
|
||||||
|
const error = new Error('Job nicht gefunden.');
|
||||||
|
error.statusCode = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPaths = resolveEffectiveStoragePathsForJob(settings, job);
|
||||||
|
const sourcePath = normalizedTarget === 'raw'
|
||||||
|
? resolvedPaths.effectiveRawPath
|
||||||
|
: resolvedPaths.effectiveOutputPath;
|
||||||
|
|
||||||
|
if (!sourcePath) {
|
||||||
|
const error = new Error(
|
||||||
|
normalizedTarget === 'raw'
|
||||||
|
? 'Kein RAW-Pfad für diesen Job vorhanden.'
|
||||||
|
: 'Kein Output-Pfad für diesen Job vorhanden.'
|
||||||
|
);
|
||||||
|
error.statusCode = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceStat;
|
||||||
|
try {
|
||||||
|
sourceStat = await fs.promises.stat(sourcePath);
|
||||||
|
} catch (_error) {
|
||||||
|
const error = new Error(
|
||||||
|
normalizedTarget === 'raw'
|
||||||
|
? 'RAW-Pfad wurde nicht gefunden.'
|
||||||
|
: 'Output-Pfad wurde nicht gefunden.'
|
||||||
|
);
|
||||||
|
error.statusCode = 404;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
|
||||||
|
const error = new Error('Nur Dateien oder Verzeichnisse können als ZIP heruntergeladen werden.');
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
jobId: normalizedJobId,
|
||||||
|
target: normalizedTarget,
|
||||||
|
sourcePath,
|
||||||
|
sourceType: sourceStat.isDirectory() ? 'directory' : 'file',
|
||||||
|
entryName: path.basename(sourcePath) || (normalizedTarget === 'raw' ? 'raw' : 'output'),
|
||||||
|
archiveName: buildJobArchiveName(job, normalizedTarget)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async getDatabaseRows(filters = {}) {
|
async getDatabaseRows(filters = {}) {
|
||||||
const jobs = await this.getJobs(filters);
|
const jobs = await this.getJobs(filters);
|
||||||
return jobs.map((job) => ({
|
return jobs.map((job) => ({
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.1-1",
|
"version": "0.10.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.10.1-1",
|
"version": "0.10.2",
|
||||||
"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.1-1",
|
"version": "0.10.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -111,6 +111,69 @@ async function request(path, options = {}) {
|
|||||||
return response.text();
|
return response.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveFilenameFromDisposition(contentDisposition, fallback = 'download.zip') {
|
||||||
|
const raw = String(contentDisposition || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedMatch = raw.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
|
||||||
|
if (encodedMatch?.[1]) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(encodedMatch[1]);
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore malformed content-disposition values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainMatch = raw.match(/filename\s*=\s*"([^"]+)"/i) || raw.match(/filename\s*=\s*([^;]+)/i);
|
||||||
|
if (plainMatch?.[1]) {
|
||||||
|
return String(plainMatch[1]).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download(path, options = {}) {
|
||||||
|
const response = await fetch(`${API_BASE}${path}`, {
|
||||||
|
headers: options?.headers || {},
|
||||||
|
method: options?.method || 'GET'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorPayload = null;
|
||||||
|
let message = `HTTP ${response.status}`;
|
||||||
|
try {
|
||||||
|
errorPayload = await response.json();
|
||||||
|
message = errorPayload?.error?.message || message;
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
const error = new Error(message);
|
||||||
|
error.status = response.status;
|
||||||
|
error.details = errorPayload?.error?.details || null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const fallbackFilename = String(options?.filename || 'download.zip').trim() || 'download.zip';
|
||||||
|
const filename = resolveFilenameFromDisposition(response.headers.get('content-disposition'), fallbackFilename);
|
||||||
|
|
||||||
|
link.href = objectUrl;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
sizeBytes: blob.size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function requestWithXhr(path, options = {}) {
|
async function requestWithXhr(path, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
@@ -630,6 +693,11 @@ export const api = {
|
|||||||
afterMutationInvalidate(['/history', '/pipeline/queue']);
|
afterMutationInvalidate(['/history', '/pipeline/queue']);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
downloadJobArchive(jobId, target = 'raw') {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
query.set('target', String(target || 'raw').trim());
|
||||||
|
return download(`/history/${jobId}/download?${query.toString()}`);
|
||||||
|
},
|
||||||
getJob(jobId, options = {}) {
|
getJob(jobId, options = {}) {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
const includeLiveLog = Boolean(options.includeLiveLog);
|
const includeLiveLog = Boolean(options.includeLiveLog);
|
||||||
|
|||||||
@@ -416,6 +416,42 @@ function BoolState({ value }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PathField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onDownload = null,
|
||||||
|
downloadDisabled = false,
|
||||||
|
downloadLoading = false
|
||||||
|
}) {
|
||||||
|
const hasValue = Boolean(String(value || '').trim());
|
||||||
|
const canDownload = hasValue && typeof onDownload === 'function' && !downloadDisabled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="job-path-field">
|
||||||
|
<strong>{label}</strong>
|
||||||
|
<div className="job-path-field-value">
|
||||||
|
<span>{hasValue ? value : '-'}</span>
|
||||||
|
{canDownload ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
icon="pi pi-download"
|
||||||
|
text
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
className="job-path-download-button"
|
||||||
|
aria-label={`${label} als ZIP herunterladen`}
|
||||||
|
tooltip={`${label} als ZIP herunterladen`}
|
||||||
|
tooltipOptions={{ position: 'top' }}
|
||||||
|
onClick={onDownload}
|
||||||
|
disabled={downloadDisabled || downloadLoading}
|
||||||
|
loading={downloadLoading}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function JobDetailDialog({
|
export default function JobDetailDialog({
|
||||||
visible,
|
visible,
|
||||||
job,
|
job,
|
||||||
@@ -432,13 +468,15 @@ export default function JobDetailDialog({
|
|||||||
onRetry,
|
onRetry,
|
||||||
onDeleteFiles,
|
onDeleteFiles,
|
||||||
onDeleteEntry,
|
onDeleteEntry,
|
||||||
|
onDownloadArchive,
|
||||||
onRemoveFromQueue,
|
onRemoveFromQueue,
|
||||||
isQueued = false,
|
isQueued = false,
|
||||||
omdbAssignBusy = false,
|
omdbAssignBusy = false,
|
||||||
cdMetadataAssignBusy = false,
|
cdMetadataAssignBusy = false,
|
||||||
actionBusy = false,
|
actionBusy = false,
|
||||||
reencodeBusy = false,
|
reencodeBusy = false,
|
||||||
deleteEntryBusy = false
|
deleteEntryBusy = false,
|
||||||
|
downloadBusyTarget = null
|
||||||
}) {
|
}) {
|
||||||
const mkDone = Boolean(job?.ripSuccessful) || !job?.makemkvInfo || job?.makemkvInfo?.status === 'SUCCESS';
|
const mkDone = Boolean(job?.ripSuccessful) || !job?.makemkvInfo || job?.makemkvInfo?.status === 'SUCCESS';
|
||||||
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'].includes(job?.status);
|
const running = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'].includes(job?.status);
|
||||||
@@ -510,6 +548,8 @@ export default function JobDetailDialog({
|
|||||||
const encodePlanUserPresetId = Number(encodePlanUserPreset?.id);
|
const encodePlanUserPresetId = Number(encodePlanUserPreset?.id);
|
||||||
const reviewUserPresets = encodePlanUserPreset ? [encodePlanUserPreset] : [];
|
const reviewUserPresets = encodePlanUserPreset ? [encodePlanUserPreset] : [];
|
||||||
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
const executedHandBrakeCommand = buildExecutedHandBrakeCommand(job?.handbrakeInfo);
|
||||||
|
const canDownloadRaw = Boolean(job?.raw_path && job?.rawStatus?.exists && typeof onDownloadArchive === 'function');
|
||||||
|
const canDownloadOutput = Boolean(job?.output_path && job?.outputStatus?.exists && typeof onDownloadArchive === 'function');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -701,12 +741,20 @@ export default function JobDetailDialog({
|
|||||||
<div>
|
<div>
|
||||||
<strong>Ende:</strong> {job.end_time || '-'}
|
<strong>Ende:</strong> {job.end_time || '-'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<PathField
|
||||||
<strong>{isCd ? 'WAV Pfad:' : 'RAW Pfad:'}</strong> {job.raw_path || '-'}
|
label={isCd ? 'WAV Pfad:' : 'RAW Pfad:'}
|
||||||
</div>
|
value={job.raw_path}
|
||||||
<div>
|
onDownload={canDownloadRaw ? () => onDownloadArchive?.(job, 'raw') : null}
|
||||||
<strong>Output:</strong> {job.output_path || '-'}
|
downloadDisabled={!canDownloadRaw}
|
||||||
</div>
|
downloadLoading={downloadBusyTarget === 'raw'}
|
||||||
|
/>
|
||||||
|
<PathField
|
||||||
|
label="Output:"
|
||||||
|
value={job.output_path}
|
||||||
|
onDownload={canDownloadOutput ? () => onDownloadArchive?.(job, 'output') : null}
|
||||||
|
downloadDisabled={!canDownloadOutput}
|
||||||
|
downloadLoading={downloadBusyTarget === 'output'}
|
||||||
|
/>
|
||||||
{!isCd ? (
|
{!isCd ? (
|
||||||
<div>
|
<div>
|
||||||
<strong>Encode Input:</strong> {job.encode_input_path || '-'}
|
<strong>Encode Input:</strong> {job.encode_input_path || '-'}
|
||||||
|
|||||||
@@ -370,6 +370,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
const [deleteEntryPreview, setDeleteEntryPreview] = useState(null);
|
const [deleteEntryPreview, setDeleteEntryPreview] = useState(null);
|
||||||
const [deleteEntryPreviewLoading, setDeleteEntryPreviewLoading] = useState(false);
|
const [deleteEntryPreviewLoading, setDeleteEntryPreviewLoading] = useState(false);
|
||||||
const [deleteEntryTargetBusy, setDeleteEntryTargetBusy] = useState(null);
|
const [deleteEntryTargetBusy, setDeleteEntryTargetBusy] = useState(null);
|
||||||
|
const [downloadBusyTarget, setDownloadBusyTarget] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [queuedJobIds, setQueuedJobIds] = useState([]);
|
const [queuedJobIds, setQueuedJobIds] = useState([]);
|
||||||
const toastRef = useRef(null);
|
const toastRef = useRef(null);
|
||||||
@@ -557,6 +558,28 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadArchive = async (row, target) => {
|
||||||
|
const jobId = Number(row?.id || selectedJob?.id || 0);
|
||||||
|
const normalizedTarget = String(target || '').trim().toLowerCase();
|
||||||
|
if (!jobId || !['raw', 'output'].includes(normalizedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDownloadBusyTarget(normalizedTarget);
|
||||||
|
try {
|
||||||
|
await api.downloadJobArchive(jobId, normalizedTarget);
|
||||||
|
} catch (error) {
|
||||||
|
toastRef.current?.show({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Download fehlgeschlagen',
|
||||||
|
detail: error.message,
|
||||||
|
life: 4500
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setDownloadBusyTarget(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleReencode = async (row) => {
|
const handleReencode = async (row) => {
|
||||||
const title = row.title || row.detected_title || `Job #${row.id}`;
|
const title = row.title || row.detected_title || `Job #${row.id}`;
|
||||||
const confirmed = window.confirm(`RAW neu encodieren für "${title}" starten?`);
|
const confirmed = window.confirm(`RAW neu encodieren für "${title}" starten?`);
|
||||||
@@ -1169,15 +1192,18 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
|||||||
onRetry={handleRetry}
|
onRetry={handleRetry}
|
||||||
onDeleteFiles={handleDeleteFiles}
|
onDeleteFiles={handleDeleteFiles}
|
||||||
onDeleteEntry={handleDeleteEntry}
|
onDeleteEntry={handleDeleteEntry}
|
||||||
|
onDownloadArchive={handleDownloadArchive}
|
||||||
onRemoveFromQueue={handleRemoveFromQueue}
|
onRemoveFromQueue={handleRemoveFromQueue}
|
||||||
isQueued={Boolean(selectedJob?.id && queuedJobIdSet.has(normalizeJobId(selectedJob.id)))}
|
isQueued={Boolean(selectedJob?.id && queuedJobIdSet.has(normalizeJobId(selectedJob.id)))}
|
||||||
actionBusy={actionBusy}
|
actionBusy={actionBusy}
|
||||||
reencodeBusy={reencodeBusyJobId === selectedJob?.id}
|
reencodeBusy={reencodeBusyJobId === selectedJob?.id}
|
||||||
deleteEntryBusy={deleteEntryBusy}
|
deleteEntryBusy={deleteEntryBusy}
|
||||||
|
downloadBusyTarget={downloadBusyTarget}
|
||||||
onHide={() => {
|
onHide={() => {
|
||||||
setDetailVisible(false);
|
setDetailVisible(false);
|
||||||
setDetailLoading(false);
|
setDetailLoading(false);
|
||||||
setLogLoadingMode(null);
|
setLogLoadingMode(null);
|
||||||
|
setDownloadBusyTarget(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -2205,6 +2205,30 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.job-path-field {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-path-field-value {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 0.35rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-path-field-value > span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-path-download-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.job-meta-col-span-2 {
|
.job-meta-col-span-2 {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.1-1",
|
"version": "0.10.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.10.1-1",
|
"version": "0.10.2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.10.1-1",
|
"version": "0.10.2",
|
||||||
"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