0.10.2-9 Layout

This commit is contained in:
2026-03-16 10:02:41 +00:00
parent 45b2272f1a
commit 38a82706ce
14 changed files with 159 additions and 43 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-8", "version": "0.10.2-9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-8", "version": "0.10.2-9",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.10.2-8", "version": "0.10.2-9",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
+40
View File
@@ -131,6 +131,45 @@ function normalizeAudnexChapter(entry, index) {
}; };
} }
function normalizeNameList(items) {
if (!Array.isArray(items) || items.length === 0) {
return null;
}
const names = items
.map((item) => String(item?.name || '').trim())
.filter(Boolean);
return names.length > 0 ? names.join(', ') : null;
}
async function fetchBookByAsin(asin, region = 'de') {
const normalizedAsin = normalizeAsin(asin);
if (!normalizedAsin) {
return null;
}
const url = new URL(`${AUDNEX_BASE_URL}/books/${normalizedAsin}`);
url.searchParams.set('region', String(region || 'de').trim() || 'de');
logger.info('book:fetch:start', { asin: normalizedAsin, url: url.toString() });
const payload = await audnexFetch(url.toString());
if (!payload || typeof payload !== 'object') {
return null;
}
const narrator = normalizeNameList(payload.narrators);
const author = normalizeNameList(payload.authors);
const title = String(payload.title || '').trim() || null;
const series = String(payload.seriesName || payload.series || '').trim() || null;
const part = String(payload.seriesPart || payload.part || '').trim() || null;
const description = String(payload.summary || payload.description || '').trim() || null;
const year = payload.releaseDate
? String(payload.releaseDate).slice(0, 4)
: null;
logger.info('book:fetch:done', { asin: normalizedAsin, narrator, author, title });
return { narrator, author, title, series, part, description, year };
}
async function fetchChaptersByAsin(asin, region = 'de') { async function fetchChaptersByAsin(asin, region = 'de') {
const normalizedAsin = normalizeAsin(asin); const normalizedAsin = normalizeAsin(asin);
if (!normalizedAsin) { if (!normalizedAsin) {
@@ -152,5 +191,6 @@ async function fetchChaptersByAsin(asin, region = 'de') {
module.exports = { module.exports = {
extractAsinFromAaxFile, extractAsinFromAaxFile,
fetchBookByAsin,
fetchChaptersByAsin fetchChaptersByAsin
}; };
+27 -3
View File
@@ -11081,21 +11081,37 @@ class PipelineService extends EventEmitter {
let detectedAsin = null; let detectedAsin = null;
let audnexChapters = []; let audnexChapters = [];
let audnexBook = null;
try { try {
detectedAsin = await audnexService.extractAsinFromAaxFile(storagePaths.rawFilePath); detectedAsin = await audnexService.extractAsinFromAaxFile(storagePaths.rawFilePath);
if (detectedAsin) { if (detectedAsin) {
await historyService.appendLog(job.id, 'SYSTEM', `ASIN erkannt: ${detectedAsin}`); await historyService.appendLog(job.id, 'SYSTEM', `ASIN erkannt: ${detectedAsin}`);
audnexChapters = await audnexService.fetchChaptersByAsin(detectedAsin, 'de'); const [chapters, book] = await Promise.allSettled([
audnexService.fetchChaptersByAsin(detectedAsin, 'de'),
audnexService.fetchBookByAsin(detectedAsin, 'de')
]);
if (chapters.status === 'fulfilled') {
audnexChapters = chapters.value;
if (audnexChapters.length > 0) { if (audnexChapters.length > 0) {
await historyService.appendLog(job.id, 'SYSTEM', `Audnex-Kapitel geladen: ${audnexChapters.length}`); await historyService.appendLog(job.id, 'SYSTEM', `Audnex-Kapitel geladen: ${audnexChapters.length}`);
} else { } else {
await historyService.appendLog(job.id, 'SYSTEM', `Keine Audnex-Kapitel fuer ASIN ${detectedAsin} gefunden.`); await historyService.appendLog(job.id, 'SYSTEM', `Keine Audnex-Kapitel fuer ASIN ${detectedAsin} gefunden.`);
} }
} else {
logger.warn('audiobook:upload:audnex-chapters-failed', { jobId: job.id, asin: detectedAsin, error: errorToMeta(chapters.reason) });
await historyService.appendLog(job.id, 'SYSTEM', `Audnex-Kapitel konnten nicht geladen werden: ${chapters.reason?.message || 'unknown'}`).catch(() => {});
}
if (book.status === 'fulfilled' && book.value) {
audnexBook = book.value;
await historyService.appendLog(job.id, 'SYSTEM', `Audnex-Buchmetadaten geladen: ${audnexBook.narrator ? `Sprecher: ${audnexBook.narrator}` : 'kein Sprecher'}`);
} else if (book.status === 'rejected') {
logger.warn('audiobook:upload:audnex-book-failed', { jobId: job.id, asin: detectedAsin, error: errorToMeta(book.reason) });
}
} else { } else {
await historyService.appendLog(job.id, 'SYSTEM', 'Keine ASIN in der AAX-Datei gefunden, verwende eingebettete Kapitel.'); await historyService.appendLog(job.id, 'SYSTEM', 'Keine ASIN in der AAX-Datei gefunden, verwende eingebettete Kapitel.');
} }
} catch (audnexError) { } catch (audnexError) {
logger.warn('audiobook:upload:audnex-chapters-failed', { logger.warn('audiobook:upload:audnex-failed', {
jobId: job.id, jobId: job.id,
stagedRawFilePath: storagePaths.rawFilePath, stagedRawFilePath: storagePaths.rawFilePath,
asin: detectedAsin, asin: detectedAsin,
@@ -11104,7 +11120,7 @@ class PipelineService extends EventEmitter {
await historyService.appendLog( await historyService.appendLog(
job.id, job.id,
'SYSTEM', 'SYSTEM',
`Audnex-Kapitel konnten nicht geladen werden: ${audnexError?.message || 'unknown'}` `Audnex-Daten konnten nicht geladen werden: ${audnexError?.message || 'unknown'}`
).catch(() => {}); ).catch(() => {});
} }
@@ -11140,6 +11156,14 @@ class PipelineService extends EventEmitter {
const resolvedMetadata = { const resolvedMetadata = {
...metadata, ...metadata,
// Audnex book metadata takes precedence over embedded tags where available
...(audnexBook?.narrator ? { narrator: audnexBook.narrator } : {}),
...(audnexBook?.author ? { author: audnexBook.author, artist: audnexBook.author } : {}),
...(audnexBook?.title ? { title: audnexBook.title, album: audnexBook.title } : {}),
...(audnexBook?.description ? { description: audnexBook.description } : {}),
...(audnexBook?.series ? { series: audnexBook.series } : {}),
...(audnexBook?.part ? { part: audnexBook.part } : {}),
...(audnexBook?.year ? { year: audnexBook.year } : {}),
asin: detectedAsin || null, asin: detectedAsin || null,
chapterSource: audnexChapters.length > 0 ? 'audnex' : 'probe', chapterSource: audnexChapters.length > 0 ? 'audnex' : 'probe',
chapters: audnexChapters.length > 0 chapters: audnexChapters.length > 0
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-8", "version": "0.10.2-9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-8", "version": "0.10.2-9",
"dependencies": { "dependencies": {
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.2", "primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.10.2-8", "version": "0.10.2-9",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Audiobook">
<circle cx="32" cy="32" r="30" fill="#f4f1da"/>
<path d="M32 14c-9.9 0-18 7.4-18 16.5 0 2.7.7 5.2 2 7.5v5c0 1.1.9 2 2 2h3c1.7 0 3-1.3 3-3v-6c0-1.7-1.3-3-3-3h-1.6C21.6 29.9 26.4 26 32 26s10.4 3.9 12.6 7h-1.6c-1.7 0-3 1.3-3 3v6c0 1.7 1.3 3 3 3h3c1.1 0 2-.9 2-2v-5c1.3-2.3 2-4.8 2-7.5C50 21.4 41.9 14 32 14z" fill="#2f3440"/>
</svg>

After

Width:  |  Height:  |  Size: 429 B

+1 -1
View File
@@ -9,5 +9,5 @@
<circle cx="32" cy="32" r="30" fill="url(#cdg)"/> <circle cx="32" cy="32" r="30" fill="url(#cdg)"/>
<circle cx="32" cy="32" r="9" fill="#f7f9fc" stroke="#9ca6b5" stroke-width="2"/> <circle cx="32" cy="32" r="9" fill="#f7f9fc" stroke="#9ca6b5" stroke-width="2"/>
<path d="M15 25 A20 20 0 0 1 48 18" fill="none" stroke="#ffffff" stroke-width="3" stroke-linecap="round" opacity="0.85"/> <path d="M15 25 A20 20 0 0 1 48 18" fill="none" stroke="#ffffff" stroke-width="3" stroke-linecap="round" opacity="0.85"/>
<text x="32" y="54" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" font-weight="700" fill="#3f4a5d">CD</text> <text x="32" y="54" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" font-weight="700" fill="#3f4a5d">DVD</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 741 B

After

Width:  |  Height:  |  Size: 742 B

@@ -190,8 +190,6 @@ export default function AudiobookConfigPanel({
<div><strong>Titel:</strong> {metadata?.title || '-'}</div> <div><strong>Titel:</strong> {metadata?.title || '-'}</div>
<div><strong>Autor:</strong> {metadata?.author || '-'}</div> <div><strong>Autor:</strong> {metadata?.author || '-'}</div>
<div><strong>Sprecher:</strong> {metadata?.narrator || '-'}</div> <div><strong>Sprecher:</strong> {metadata?.narrator || '-'}</div>
<div><strong>Serie:</strong> {metadata?.series || '-'}</div>
<div><strong>Teil:</strong> {metadata?.part || '-'}</div>
<div><strong>Jahr:</strong> {metadata?.year || '-'}</div> <div><strong>Jahr:</strong> {metadata?.year || '-'}</div>
<div><strong>Kapitel:</strong> {editableChapters.length || '-'}</div> <div><strong>Kapitel:</strong> {editableChapters.length || '-'}</div>
{descriptionPreview ? ( {descriptionPreview ? (
+5 -4
View File
@@ -19,6 +19,7 @@ import AudiobookConfigPanel from '../components/AudiobookConfigPanel';
import blurayIndicatorIcon from '../assets/media-bluray.svg'; import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg'; import discIndicatorIcon from '../assets/media-disc.svg';
import otherIndicatorIcon from '../assets/media-other.svg'; import otherIndicatorIcon from '../assets/media-other.svg';
import audiobookIndicatorIcon from '../assets/media-audiobook.svg';
import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation'; import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation';
const processingStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING']; const processingStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING'];
@@ -475,7 +476,7 @@ function mediaIndicatorMeta(job) {
return { mediaType, src: otherIndicatorIcon, alt: 'Audio CD', title: 'Audio CD' }; return { mediaType, src: otherIndicatorIcon, alt: 'Audio CD', title: 'Audio CD' };
} }
if (mediaType === 'audiobook') { if (mediaType === 'audiobook') {
return { mediaType, src: otherIndicatorIcon, alt: 'Audiobook', title: 'Audiobook' }; return { mediaType, src: audiobookIndicatorIcon, alt: 'Audiobook', title: 'Audiobook' };
} }
return { mediaType, src: otherIndicatorIcon, alt: 'Sonstiges Medium', title: 'Sonstiges Medium' }; return { mediaType, src: otherIndicatorIcon, alt: 'Sonstiges Medium', title: 'Sonstiges Medium' };
} }
@@ -2524,7 +2525,7 @@ export default function DashboardPage({
</div> </div>
<div className="dashboard-col dashboard-col-right"> <div className="dashboard-col dashboard-col-right">
<Card title="Audiobook Upload" subTitle="AAX-Datei hochladen, analysieren und danach Format/Qualität vor dem Start auswählen."> <Card title="Audiobook Upload">
<FileUpload <FileUpload
ref={audiobookFileUploadRef} ref={audiobookFileUploadRef}
accept=".aax" accept=".aax"
@@ -2593,7 +2594,7 @@ export default function DashboardPage({
) : null} ) : null}
</Card> </Card>
<Card title="Job Queue" subTitle="Starts werden nach Typ- und Gesamtlimit abgearbeitet. Queue-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" />
<Tag value={`CD max.: ${queueState?.maxParallelCdEncodes || 2}`} severity="info" /> <Tag value={`CD max.: ${queueState?.maxParallelCdEncodes || 2}`} severity="info" />
@@ -2755,7 +2756,7 @@ export default function DashboardPage({
</div> </div>
</Card> </Card>
<Card title="Skript- / Cron-Status" subTitle="Laufende und zuletzt abgeschlossene Skript-, Ketten- und Cron-Ausführungen."> <Card title="Skript- / Cron-Status">
<div className="runtime-activity-meta pipeline-queue-meta"> <div className="runtime-activity-meta pipeline-queue-meta">
<Tag value={`Laufend: ${runtimeActiveItems.length}`} severity={runtimeActiveItems.length > 0 ? 'warning' : 'success'} /> <Tag value={`Laufend: ${runtimeActiveItems.length}`} severity={runtimeActiveItems.length > 0 ? 'warning' : 'success'} />
<Tag value={`Zuletzt: ${runtimeRecentItems.length}`} severity="info" /> <Tag value={`Zuletzt: ${runtimeRecentItems.length}`} severity="info" />
+3 -1
View File
@@ -1058,12 +1058,14 @@ export default function HistoryPage({ refreshToken = 0 }) {
> >
<div className="history-dv-grid-poster-wrap"> <div className="history-dv-grid-poster-wrap">
{renderPoster(row, 'history-dv-poster-grid')} {renderPoster(row, 'history-dv-poster-grid')}
<div className="history-dv-grid-status-overlay">
{renderStatusTag(row)}
</div>
</div> </div>
<div className="history-dv-grid-main"> <div className="history-dv-grid-main">
<div className="history-dv-head"> <div className="history-dv-head">
<strong className="history-dv-title">{row?.title || row?.detected_title || '-'}</strong> <strong className="history-dv-title">{row?.title || row?.detected_title || '-'}</strong>
{renderStatusTag(row)}
</div> </div>
<small className="history-dv-subtle">{subtitle}</small> <small className="history-dv-subtle">{subtitle}</small>
+67 -20
View File
@@ -419,54 +419,62 @@ body {
.hardware-cpu-summary { .hardware-cpu-summary {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 0.45rem; gap: 0.35rem;
align-items: stretch; align-items: center;
min-width: 0;
overflow: hidden;
} }
.hardware-cpu-chip { .hardware-cpu-chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.35rem; gap: 0.28rem;
border: 1px solid var(--rip-border); border: 1px solid var(--rip-border);
border-radius: 999px; border-radius: 999px;
background: var(--rip-panel); background: var(--rip-panel);
padding: 0.22rem 0.55rem; padding: 0.18rem 0.42rem;
font-size: 0.8rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
min-height: 2rem;
height: 2rem;
white-space: nowrap; white-space: nowrap;
min-width: 0;
flex-shrink: 1;
} }
.hardware-cpu-chip span { .hardware-cpu-chip span {
line-height: 1; line-height: 1;
overflow: hidden;
text-overflow: ellipsis;
} }
.hardware-cpu-chip .pi { .hardware-cpu-chip .pi {
font-size: 0.82rem; font-size: 0.75rem;
color: var(--rip-brown-700); color: var(--rip-brown-700);
flex-shrink: 0;
} }
.hardware-cpu-load-group { .hardware-cpu-load-group {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.2rem;
min-width: 0;
flex-shrink: 1;
} }
.hardware-cpu-core-toggle-btn { .hardware-cpu-core-toggle-btn {
border: 1px solid var(--rip-border); border: 1px solid var(--rip-border);
background: var(--rip-panel); background: var(--rip-panel);
color: var(--rip-brown-700); color: var(--rip-brown-700);
width: 2rem; width: 1.6rem;
min-width: 2rem; min-width: 1.6rem;
height: 2rem; height: 1.6rem;
border-radius: 999px; border-radius: 999px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
flex-shrink: 0;
} }
.hardware-cpu-core-toggle-btn:hover { .hardware-cpu-core-toggle-btn:hover {
@@ -507,12 +515,13 @@ body {
} }
.hardware-core-item.compact { .hardware-core-item.compact {
grid-template-columns: auto auto auto; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: start; gap: 0.2rem 0.3rem;
gap: 0.35rem;
padding: 0.3rem 0.45rem; padding: 0.3rem 0.45rem;
text-align: left; text-align: left;
overflow: hidden;
} }
.hardware-core-title { .hardware-core-title {
@@ -1112,6 +1121,21 @@ body {
white-space: normal; white-space: normal;
} }
.p-fileupload-buttonbar {
padding: 0.4rem 0.6rem !important;
gap: 0.35rem !important;
}
.p-fileupload-buttonbar .p-button.p-button-icon-only {
width: 2rem !important;
height: 2rem !important;
padding: 0 !important;
}
.p-fileupload-buttonbar .p-button.p-button-icon-only .p-button-icon {
font-size: 0.85rem !important;
}
.aax-drop-zone { .aax-drop-zone {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1507,7 +1531,7 @@ body {
.settings-grid { .settings-grid {
display: grid; display: grid;
gap: 1rem; gap: 0.5rem;
} }
.settings-tabview { .settings-tabview {
@@ -1528,7 +1552,7 @@ body {
.settings-sections { .settings-sections {
display: grid; display: grid;
gap: 0.85rem; gap: 0.6rem;
} }
.settings-section.grouped { .settings-section.grouped {
@@ -1556,8 +1580,8 @@ body {
.setting-row { .setting-row {
display: grid; display: grid;
gap: 0.35rem; gap: 0.25rem;
padding: 0.75rem; padding: 0.45rem 0.6rem;
border: 1px solid var(--rip-border); border: 1px solid var(--rip-border);
border-radius: 0.5rem; border-radius: 0.5rem;
background: var(--rip-panel-soft); background: var(--rip-panel-soft);
@@ -1569,6 +1593,9 @@ body {
.setting-description { .setting-description {
white-space: pre-wrap; white-space: pre-wrap;
font-size: 0.78rem;
color: var(--rip-muted);
line-height: 1.35;
} }
.notification-toggle-grid { .notification-toggle-grid {
@@ -2075,6 +2102,8 @@ body {
.history-dataview .p-dataview-content .p-grid.grid > .xl-col-4 { .history-dataview .p-dataview-content .p-grid.grid > .xl-col-4 {
width: 100%; width: 100%;
padding: 0.35rem; padding: 0.35rem;
display: flex;
flex-direction: column;
} }
@media (min-width: 900px) { @media (min-width: 900px) {
@@ -2094,6 +2123,9 @@ body {
border-radius: 0.55rem; border-radius: 0.55rem;
background: var(--rip-panel-soft); background: var(--rip-panel-soft);
box-shadow: 0 2px 7px rgba(58, 29, 18, 0.06); box-shadow: 0 2px 7px rgba(58, 29, 18, 0.06);
display: flex;
flex-direction: column;
height: 100%;
} }
.history-dv-item:focus-visible { .history-dv-item:focus-visible {
@@ -2230,13 +2262,28 @@ body {
gap: 0.5rem; gap: 0.5rem;
padding: 0.65rem; padding: 0.65rem;
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
flex: 1;
} }
.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 {
position: absolute;
top: 0.4rem;
left: 0;
z-index: 1;
pointer-events: none;
}
.history-dv-grid-status-overlay .p-tag {
pointer-events: auto;
box-shadow: 0 1px 4px rgba(0,0,0,0.25);
}
.history-dv-grid-main { .history-dv-grid-main {
display: grid; display: grid;
gap: 0.35rem; gap: 0.35rem;
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.10.2-8", "version": "0.10.2-9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.10.2-8", "version": "0.10.2-9",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ripster", "name": "ripster",
"private": true, "private": true,
"version": "0.10.2-8", "version": "0.10.2-9",
"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",