0.10.2-25 Fixes

This commit is contained in:
2026-03-17 16:01:10 +00:00
parent d7772df75d
commit 16443c8a45
9 changed files with 253 additions and 159 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster-backend",
"version": "0.10.2-24",
"version": "0.10.2-25",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-backend",
"version": "0.10.2-24",
"version": "0.10.2-25",
"dependencies": {
"archiver": "^7.0.1",
"cors": "^2.8.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ripster-backend",
"version": "0.10.2-24",
"version": "0.10.2-25",
"private": true,
"type": "commonjs",
"scripts": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster-frontend",
"version": "0.10.2-24",
"version": "0.10.2-25",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-frontend",
"version": "0.10.2-24",
"version": "0.10.2-25",
"dependencies": {
"primeicons": "^7.0.0",
"primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ripster-frontend",
"version": "0.10.2-24",
"version": "0.10.2-25",
"private": true,
"type": "module",
"scripts": {
+166 -81
View File
@@ -774,42 +774,43 @@ export default function JobDetailDialog({
</div>
<section className="job-meta-block job-meta-block-full">
<h4>Job-Infos</h4>
<div className="job-meta-grid job-meta-grid-compact">
<div>
<strong>Status:</strong>{' '}
<span
className={`job-status-icon tone-${statusMeta.tone}`}
title={statusMeta.label}
aria-label={statusMeta.label}
>
<i className={`pi ${statusMeta.icon}`} aria-hidden="true" />
<div className="job-infos-header">
<h4>Job-Infos</h4>
<div className="job-infos-badges">
<span>
<strong>Status:</strong>{' '}
<span
className={`job-status-icon tone-${statusMeta.tone}`}
title={statusMeta.label}
aria-label={statusMeta.label}
>
<i className={`pi ${statusMeta.icon}`} aria-hidden="true" />
</span>
</span>
{' '}<span className="job-meta-subtle">{statusMeta.label}</span>
<span className="job-infos-sep">|</span>
{isCd ? (
<>
<span><strong>Rip:</strong> <BoolState value={job?.ripSuccessful} /></span>
<span className="job-infos-sep">|</span>
<span><strong>Backup:</strong> <BoolState value={job?.backupSuccess} /></span>
</>
) : (
<>
<span><strong>{isAudiobook ? 'Import:' : 'Backup:'}</strong> <BoolState value={job?.backupSuccess} /></span>
<span className="job-infos-sep">|</span>
<span><strong>Encode:</strong> <BoolState value={job?.encodeSuccess} /></span>
</>
)}
</div>
<div>
<strong>Start:</strong> {job.start_time || '-'}
</div>
<div>
<strong>Ende:</strong> {job.end_time || '-'}
</div>
{isCd ? (
<div>
<strong>Rip:</strong> <BoolState value={job?.ripSuccessful} />
</div>
) : (
<>
<div>
<strong>{isAudiobook ? 'Import:' : 'Backup:'}</strong> <BoolState value={job?.backupSuccess} />
</div>
<div>
<strong>Encode:</strong> <BoolState value={job?.encodeSuccess} />
</div>
</>
)}
</div>
<div className="job-infos-layout">
{/* Zeile 1: Start | Ende */}
<div><strong>Start:</strong> {job.start_time || '-'}</div>
<div><strong>Ende:</strong> {job.end_time || '-'}</div>
{/* Zeile 3+4: Pfade */}
<PathField
label={isCd ? 'WAV:' : 'RAW:'}
value={job.raw_path}
value={isCd ? (job.raw_path || job.output_path) : job.raw_path}
onDownload={canDownloadRaw ? () => onDownloadArchive?.(job, 'raw') : null}
downloadDisabled={!canDownloadRaw}
downloadLoading={downloadBusyTarget === 'raw'}
@@ -822,12 +823,10 @@ export default function JobDetailDialog({
downloadLoading={downloadBusyTarget === 'output'}
/>
{job.error_message ? (
<div className="job-meta-col-span-2">
<strong>Fehler:</strong> {job.error_message}
</div>
<div><strong>Fehler:</strong> {job.error_message}</div>
) : null}
{expertMode ? (
<div className="job-meta-col-span-2">
<div>
<Link to={`/database?table=jobs&id=${job.id}`} className="job-db-link" onClick={onHide}>
<i className="pi pi-database" /> DB-Eintrag ansehen
</Link>
@@ -885,50 +884,51 @@ export default function JobDetailDialog({
return (
<div key={title.id} className="track-title-block">
<div className="track-title-row">
<span>#{title.id} | {title.fileName} | {title.durationMinutes != null ? `${Number(title.durationMinutes).toFixed(2)} min` : '-'} | {trackSizeFormat(title.sizeBytes)}</span>
{title.encodeInput ? <span className="track-input-badge">Encode-Input</span> : null}
<span>#{title.id} | {title.fileName} | {title.durationMinutes != null ? `${Number(title.durationMinutes).toFixed(2)} min` : '-'}</span>
</div>
<div className="track-groups-row">
{audioTracks.length > 0 ? (
<div className="track-group">
<div className="track-group-label">Tonspuren (Titel #{title.id})</div>
{audioTracks.map((track) => {
const lang = trackLang(track.language || track.languageLabel);
const codec = trackCodec('audio', track.format, track.description || track.title);
const chLayout = trackChLayout(track.channels);
let displayText = `#${track.id} | ${lang} | ${codec}`;
if (chLayout) displayText += ` | ${chLayout}`;
const actionInfo = track.selectedForEncode
? (String(track.encodePreviewSummary || track.encodeActionSummary || '').trim() || 'Copy')
: 'Nicht übernommen';
return (
<div key={track.id} className="track-item">
<span>{displayText}</span>
<small className="track-action-note">Encode: {actionInfo}</small>
</div>
);
})}
</div>
) : null}
{subtitleTracks.length > 0 ? (
<div className="track-group">
<div className="track-group-label">Subtitles (Titel #{title.id})</div>
{subtitleTracks.map((track) => {
const lang = trackLang(track.language || track.languageLabel);
const codec = trackCodec('subtitle', track.format);
const displayText = `#${track.id} | ${lang} | ${codec}`;
const rawAction = String(track.subtitlePreviewSummary || track.subtitleActionSummary || '').trim();
const actionInfo = track.selectedForEncode
? (/^nicht übernommen$/i.test(rawAction) ? 'Übernehmen' : (rawAction || 'Übernehmen'))
: 'Nicht übernommen';
return (
<div key={track.id} className="track-item">
<span>{displayText}</span>
<small className="track-action-note">Encode: {actionInfo}</small>
</div>
);
})}
</div>
) : null}
</div>
{audioTracks.length > 0 ? (
<div className="track-group">
<div className="track-group-label">Tonspuren (Titel #{title.id})</div>
{audioTracks.map((track) => {
const lang = trackLang(track.language || track.languageLabel);
const codec = trackCodec('audio', track.format, track.description || track.title);
const chLayout = trackChLayout(track.channels);
let displayText = `#${track.id} | ${lang} | ${codec}`;
if (chLayout) displayText += ` | ${chLayout}`;
const actionInfo = track.selectedForEncode
? (String(track.encodePreviewSummary || track.encodeActionSummary || '').trim() || 'Copy')
: 'Nicht übernommen';
return (
<div key={track.id} className="track-item">
<span>{displayText}</span>
<small className="track-action-note">Encode: {actionInfo}</small>
</div>
);
})}
</div>
) : null}
{subtitleTracks.length > 0 ? (
<div className="track-group">
<div className="track-group-label">Subtitles (Titel #{title.id})</div>
{subtitleTracks.map((track) => {
const lang = trackLang(track.language || track.languageLabel);
const codec = trackCodec('subtitle', track.format);
const displayText = `#${track.id} | ${lang} | ${codec}`;
const rawAction = String(track.subtitlePreviewSummary || track.subtitleActionSummary || '').trim();
const actionInfo = track.selectedForEncode
? (/^nicht übernommen$/i.test(rawAction) ? 'Übernehmen' : (rawAction || 'Übernehmen'))
: 'Nicht übernommen';
return (
<div key={track.id} className="track-item">
<span>{displayText}</span>
<small className="track-action-note">Encode: {actionInfo}</small>
</div>
);
})}
</div>
) : null}
</div>
);
})
@@ -938,14 +938,99 @@ export default function JobDetailDialog({
</section>
) : null}
{(isCd || isAudiobook) && job.encodePlan ? (
<section className="job-meta-block job-meta-block-full">
<h4>{isCd ? 'Titelauswahl' : 'Kapitelauswahl'}</h4>
{isCd ? (() => {
const tracksRaw = Array.isArray(job.makemkvInfo?.tracks) && job.makemkvInfo.tracks.length > 0
? job.makemkvInfo.tracks
: (Array.isArray(job.encodePlan?.tracks) ? job.encodePlan.tracks : []);
const selectedSet = Array.isArray(job.encodePlan?.selectedTracks) && job.encodePlan.selectedTracks.length > 0
? new Set(job.encodePlan.selectedTracks.map(Number))
: null;
if (tracksRaw.length === 0) return <p className="job-meta-subtle">Keine Tracks vorhanden.</p>;
return (
<div className="track-group">
{tracksRaw.map((track, i) => {
const pos = Number(track.position ?? (i + 1));
const label = String(track.title || track.name || `Track ${pos}`).trim();
const artist = String(track.artist || '').trim();
const durationSec = Number(track.durationMs || 0) > 0
? Number(track.durationMs) / 1000
: Number(track.durationSec || 0);
const durLabel = durationSec > 0
? `${Math.floor(durationSec / 60)}:${String(Math.floor(durationSec % 60)).padStart(2, '0')} min`
: '-';
const selected = selectedSet ? selectedSet.has(pos) : track.selected !== false;
const ripDone = job?.ripSuccessful != null;
const encodeLabel = !selected ? 'Nicht übernommen'
: ripDone ? (job.ripSuccessful ? 'Erfolgreich' : 'Fehler')
: 'Übernommen';
const encodeClass = !selected ? '' : ripDone ? (job.ripSuccessful ? 'tone-ok' : 'tone-no') : '';
return (
<div key={pos} className="track-item">
<span>#{pos} | {label}{artist ? ` | ${artist}` : ''} | {durLabel}</span>
<small className={`track-action-note ${encodeClass}`}>Encode: {encodeLabel}</small>
</div>
);
})}
</div>
);
})() : (() => {
const chapters = Array.isArray(job.handbrakeInfo?.metadata?.chapters) && job.handbrakeInfo.metadata.chapters.length > 0
? job.handbrakeInfo.metadata.chapters
: (Array.isArray(job.makemkvInfo?.chapters) && job.makemkvInfo.chapters.length > 0
? job.makemkvInfo.chapters
: (Array.isArray(job.encodePlan?.metadata?.chapters) ? job.encodePlan.metadata.chapters : []));
const stepsByIndex = new Map(
(Array.isArray(job.handbrakeInfo?.steps) ? job.handbrakeInfo.steps : [])
.map((s) => [Number(s.chapterIndex), s])
);
if (chapters.length === 0) return <p className="job-meta-subtle">Keine Kapitelinformationen vorhanden.</p>;
return (
<div className="track-group">
{chapters.map((chapter, i) => {
const id = Number(chapter.id ?? chapter.index ?? chapter.position ?? (i + 1));
const label = String(chapter.title || chapter.name || `Kapitel ${id}`).trim();
const durationSec = Number(chapter.durationMs || 0) > 0
? Number(chapter.durationMs) / 1000
: Number(chapter.durationSec || 0);
const durLabel = durationSec > 0
? `${Math.floor(durationSec / 60)}:${String(Math.floor(durationSec % 60)).padStart(2, '0')} min`
: '-';
const step = stepsByIndex.get(id);
const stepStatus = step ? String(step.status || '').toUpperCase() : null;
const encodeLabel = stepStatus === 'SUCCESS' ? 'Erfolgreich'
: stepStatus === 'ERROR' ? 'Fehler'
: stepStatus === 'CANCELLED' ? 'Abgebrochen'
: stepStatus ? stepStatus
: 'Übernommen';
const encodeClass = stepStatus === 'SUCCESS' ? 'tone-ok'
: stepStatus === 'ERROR' ? 'tone-no'
: '';
return (
<div key={id} className="track-item">
<span>#{id} | {label} | {durLabel}</span>
<small className={`track-action-note ${encodeClass}`}>Encode: {encodeLabel}</small>
</div>
);
})}
</div>
);
})()}
</section>
) : null}
<section className="job-meta-block job-meta-block-full">
<h4>Rohdaten</h4>
<h4>Logs</h4>
<div className="job-json-grid">
{!isCd && !isAudiobook ? <JsonView title="OMDb Info" value={job.omdbInfo} /> : null}
{isCd ? <JsonView title="MusicBrainz" value={job.makemkvInfo?.selectedMetadata ?? null} /> : null}
{isAudiobook ? <JsonView title="Metadaten" value={job.handbrakeInfo?.metadata ?? job.makemkvInfo?.selectedMetadata ?? null} /> : null}
<JsonView title={isCd ? 'cdparanoia Info' : (isAudiobook ? 'Audiobook Info' : 'MakeMKV Info')} value={job.makemkvInfo} />
{!isCd && !isAudiobook ? <JsonView title="Mediainfo Info" value={job.mediainfoInfo} /> : null}
<JsonView title={isCd ? 'Rip-Plan' : 'Encode Plan'} value={job.encodePlan} />
{!isCd ? <JsonView title={isAudiobook ? 'FFmpeg Info' : 'HandBrake Info'} value={job.handbrakeInfo} /> : null}
<JsonView title={isCd ? 'Rip-Info' : (isAudiobook ? 'FFmpeg Info' : 'HandBrake Info')} value={job.handbrakeInfo} />
</div>
</section>
+9 -55
View File
@@ -867,9 +867,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
const renderPoster = (row, className = 'history-dv-poster') => {
const mediaMeta = resolveMediaTypeMeta(row);
const isAudioSquare = ['cd', 'audiobook'].includes(mediaMeta.mediaType);
const imgClass = className === 'history-dv-poster-grid' && isAudioSquare
? 'history-dv-poster-grid-audio'
: className;
const audioClass = className === 'history-dv-poster-grid' ? 'history-dv-poster-grid-audio' : 'history-dv-poster-audio';
const imgClass = isAudioSquare ? audioClass : className;
const title = row?.title || row?.detected_title || 'Poster';
if (row?.poster_url && row.poster_url !== 'N/A') {
return <img src={row.poster_url} alt={title} className={imgClass} loading="lazy" />;
@@ -903,9 +902,6 @@ export default function HistoryPage({ refreshToken = 0 }) {
if (cdDetails.totalDurationLabel) {
infoItems.push({ key: 'duration', label: 'Dauer', value: cdDetails.totalDurationLabel });
}
if (cdDetails.mbId) {
infoItems.push({ key: 'mb', label: 'MusicBrainz', value: 'gesetzt' });
}
if (infoItems.length === 0) {
return <span className="history-dv-subtle">Keine CD-Details</span>;
}
@@ -1013,35 +1009,15 @@ export default function HistoryPage({ refreshToken = 0 }) {
</div>
<div className="history-dv-flags-row">
{isCdJob ? (
<>
{renderPresenceChip('Audio', Boolean(row?.outputStatus?.exists))}
{renderPresenceChip('Rip', Boolean(row?.ripSuccessful))}
{renderPresenceChip('Metadaten', Boolean(cdDetails?.artist || cdDetails?.mbId))}
</>
) : (
<>
{renderPresenceChip('RAW', Boolean(row?.rawStatus?.exists))}
{renderPresenceChip('Movie', Boolean(row?.outputStatus?.exists))}
{renderPresenceChip('Encode', Boolean(row?.encodeSuccess))}
</>
)}
{renderPresenceChip('RAW', Boolean(row?.rawStatus?.exists))}
{renderPresenceChip(isCdJob || mediaMeta.mediaType === 'audiobook' ? 'Audio' : 'Movie', Boolean(row?.outputStatus?.exists))}
{renderPresenceChip('Encode', isCdJob || mediaMeta.mediaType === 'audiobook' ? Boolean(row?.ripSuccessful) : Boolean(row?.encodeSuccess))}
</div>
<div className="history-dv-ratings-row">{renderSupplementalInfo(row)}</div>
</div>
<div className="history-dv-actions">
<Button
label="Details"
icon="pi pi-search"
size="small"
onClick={(event) => {
event.stopPropagation();
void openDetail(row);
}}
/>
</div>
</div>
</div>
);
@@ -1096,35 +1072,13 @@ export default function HistoryPage({ refreshToken = 0 }) {
</div>
<div className="history-dv-flags-row">
{isCdJob ? (
<>
{renderPresenceChip('Audio', Boolean(row?.outputStatus?.exists))}
{renderPresenceChip('Rip', Boolean(row?.ripSuccessful))}
{renderPresenceChip('Metadaten', Boolean(cdDetails?.artist || cdDetails?.mbId))}
</>
) : (
<>
{renderPresenceChip('RAW', Boolean(row?.rawStatus?.exists))}
{renderPresenceChip('Movie', Boolean(row?.outputStatus?.exists))}
{renderPresenceChip('Encode', Boolean(row?.encodeSuccess))}
</>
)}
{renderPresenceChip('RAW', Boolean(row?.rawStatus?.exists))}
{renderPresenceChip(isCdJob || mediaMeta.mediaType === 'audiobook' ? 'Audio' : 'Movie', Boolean(row?.outputStatus?.exists))}
{renderPresenceChip('Encode', isCdJob || mediaMeta.mediaType === 'audiobook' ? Boolean(row?.ripSuccessful) : Boolean(row?.encodeSuccess))}
</div>
<div className="history-dv-ratings-row">{renderSupplementalInfo(row)}</div>
</div>
<div className="history-dv-actions history-dv-actions-grid">
<Button
label="Details"
icon="pi pi-search"
size="small"
onClick={(event) => {
event.stopPropagation();
void openDetail(row);
}}
/>
</div>
</div>
</div>
);
+69 -14
View File
@@ -2334,6 +2334,7 @@ body {
}
.history-dv-poster,
.history-dv-poster-audio,
.history-dv-poster-grid,
.history-dv-poster-grid-audio {
display: block;
@@ -2348,9 +2349,13 @@ body {
object-fit: cover;
}
.history-dv-poster-audio {
aspect-ratio: 1;
object-fit: cover;
}
.history-dv-poster-grid {
height: 164px;
object-fit: contain;
height: 180px;
}
.history-dv-poster-grid-audio {
@@ -2360,7 +2365,7 @@ body {
.history-dv-grid-poster-wrap {
width: min(120px, 100%);
height: 164px;
height: 180px;
margin: 0 auto;
display: flex;
align-items: flex-start;
@@ -2517,6 +2522,7 @@ body {
.job-film-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.55rem;
}
@@ -2572,16 +2578,18 @@ body {
.job-path-field {
min-width: 0;
display: grid;
gap: 0.2rem;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.15rem 0.4rem;
}
.job-path-field-value {
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
flex: 1 1 auto;
display: flex;
align-items: baseline;
gap: 0.35rem;
align-items: start;
}
.job-path-field-value > span {
@@ -2598,6 +2606,47 @@ body {
grid-column: 1 / -1;
}
.job-infos-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.job-infos-header h4 {
margin: 0;
}
.job-infos-badges {
display: flex;
align-items: baseline;
gap: 0.4rem;
font-size: 0.84rem;
flex-wrap: wrap;
}
.job-infos-sep {
color: var(--rip-border);
font-weight: 300;
}
.job-infos-layout {
display: grid;
gap: 0.4rem;
font-size: 0.84rem;
}
.job-infos-layout strong {
font-weight: 600;
}
.job-infos-row-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
.dashboard-downloads-row {
border-top: 1px solid var(--surface-border);
padding-top: 0.75rem;
@@ -2762,7 +2811,7 @@ body {
color: #f8e2b8;
padding: 0.75rem;
border-radius: 0.5rem;
max-height: 16rem;
height: 16rem;
overflow: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
@@ -2847,7 +2896,7 @@ body {
color: #f6ddb2;
padding: 0.75rem;
border-radius: 0.5rem;
max-height: 25rem;
height: 25rem;
max-width: 100%;
min-width: 0;
overflow: auto;
@@ -3050,10 +3099,6 @@ body {
}
.track-title-block {
border: 1px solid var(--rip-border);
border-radius: 0.45rem;
padding: 0.55rem 0.65rem;
background: var(--rip-panel-soft);
display: grid;
gap: 0.5rem;
}
@@ -3077,6 +3122,16 @@ body {
border: 1px solid var(--rip-success, #2d7a3a);
}
.track-groups-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.5rem;
align-items: start;
border-top: 1px solid var(--rip-border);
padding-top: 0.5rem;
margin-top: 0.1rem;
}
.track-group {
display: grid;
gap: 0.25rem;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster",
"version": "0.10.2-24",
"version": "0.10.2-25",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster",
"version": "0.10.2-24",
"version": "0.10.2-25",
"devDependencies": {
"concurrently": "^9.1.2"
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "ripster",
"private": true,
"version": "0.10.2-24",
"version": "0.10.2-25",
"scripts": {
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
"dev:backend": "npm run dev --prefix backend",