0.9.1 Fix Restart

This commit is contained in:
2026-03-14 08:57:25 +00:00
parent 5580d3be98
commit e140a9fa8c
11 changed files with 218 additions and 10 deletions

View File

@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.9.0-1", "version": "0.9.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.9.0-1", "version": "0.9.1",
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.9.0-1", "version": "0.9.1",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {

View File

@@ -95,6 +95,25 @@ router.post(
}) })
); );
router.post(
'/:id/cd/assign',
asyncHandler(async (req, res) => {
const id = Number(req.params.id);
const payload = req.body || {};
logger.info('post:job:cd:assign', {
reqId: req.reqId,
id,
mbId: payload?.mbId || null,
hasTitle: Boolean(payload?.title),
hasArtist: Boolean(payload?.artist),
trackCount: Array.isArray(payload?.tracks) ? payload.tracks.length : 0
});
const job = await historyService.assignCdMetadata(id, payload);
res.json({ job });
})
);
router.post( router.post(
'/:id/delete-files', '/:id/delete-files',
asyncHandler(async (req, res) => { asyncHandler(async (req, res) => {

View File

@@ -1718,6 +1718,85 @@ class HistoryService {
return enrichJobRow(updated, settings); return enrichJobRow(updated, settings);
} }
async assignCdMetadata(jobId, payload = {}) {
const job = await this.getJobById(jobId);
if (!job) {
const error = new Error('Job nicht gefunden.');
error.statusCode = 404;
throw error;
}
const title = String(payload.title || '').trim() || null;
const artist = String(payload.artist || '').trim() || null;
const yearRaw = Number(payload.year);
const year = Number.isFinite(yearRaw) && yearRaw > 0 ? Math.trunc(yearRaw) : null;
const mbId = String(payload.mbId || '').trim() || null;
const coverUrl = String(payload.coverUrl || '').trim() || null;
const selectedTracks = Array.isArray(payload.tracks) ? payload.tracks : null;
if (!title && !artist && !mbId) {
const error = new Error('Keine CD-Metadaten zum Aktualisieren angegeben.');
error.statusCode = 400;
throw error;
}
const cdInfo = parseJsonSafe(job.makemkv_info_json, {});
const tocTracks = Array.isArray(cdInfo.tracks) ? cdInfo.tracks : [];
let mergedTracks = tocTracks;
if (selectedTracks && tocTracks.length > 0) {
mergedTracks = tocTracks.map((t) => {
const selected = selectedTracks.find((st) => Number(st.position) === Number(t.position));
const resolvedTitle = String(selected?.title || t.title || `Track ${t.position}`).replace(/\s+/g, ' ').trim();
const resolvedArtist = String(selected?.artist || t.artist || artist || '').replace(/\s+/g, ' ').trim() || null;
return {
...t,
title: resolvedTitle,
artist: resolvedArtist,
selected: selected ? Boolean(selected.selected) : true
};
});
}
const prevSelected = cdInfo.selectedMetadata && typeof cdInfo.selectedMetadata === 'object' ? cdInfo.selectedMetadata : {};
const updatedCdInfo = {
...cdInfo,
tracks: mergedTracks,
selectedMetadata: {
...prevSelected,
title: title || prevSelected.title || null,
artist: artist || prevSelected.artist || null,
year: year !== null ? year : (prevSelected.year || null),
mbId: mbId || prevSelected.mbId || null,
coverUrl: coverUrl || prevSelected.coverUrl || null
}
};
await this.updateJob(jobId, {
title: title || null,
year: year || null,
imdb_id: mbId || null,
poster_url: coverUrl || null,
makemkv_info_json: JSON.stringify(updatedCdInfo)
});
if (coverUrl && !thumbnailService.isLocalUrl(coverUrl)) {
thumbnailService.cacheJobThumbnail(jobId, coverUrl).catch(() => {});
}
await this.appendLog(
jobId,
'USER_ACTION',
`CD-Metadaten aktualisiert: album="${title || '-'}", artist="${artist || '-'}", year="${year || '-'}", mbId="${mbId || '-'}"`
);
const [updated, settings] = await Promise.all([
this.getJobById(jobId),
settingsService.getSettingsMap()
]);
return enrichJobRow(updated, settings);
}
async _resolveRelatedJobsForDeletion(jobId, options = {}) { async _resolveRelatedJobsForDeletion(jobId, options = {}) {
const includeRelated = options?.includeRelated !== false; const includeRelated = options?.includeRelated !== false;
const normalizedJobId = normalizeJobIdValue(jobId); const normalizedJobId = normalizeJobIdValue(jobId);

View File

@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.9.0-1", "version": "0.9.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.9.0-1", "version": "0.9.1",
"dependencies": { "dependencies": {
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.2", "primereact": "^10.9.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.9.0-1", "version": "0.9.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -435,6 +435,14 @@ export const api = {
afterMutationInvalidate(['/history']); afterMutationInvalidate(['/history']);
return result; return result;
}, },
async assignJobCdMetadata(jobId, payload = {}) {
const result = await request(`/history/${jobId}/cd/assign`, {
method: 'POST',
body: JSON.stringify(payload || {})
});
afterMutationInvalidate(['/history']);
return result;
},
async deleteJobFiles(jobId, target = 'both') { async deleteJobFiles(jobId, target = 'both') {
const result = await request(`/history/${jobId}/delete-files`, { const result = await request(`/history/${jobId}/delete-files`, {
method: 'POST', method: 'POST',

View File

@@ -379,6 +379,7 @@ export default function JobDetailDialog({
onLoadLog, onLoadLog,
logLoadingMode = null, logLoadingMode = null,
onAssignOmdb, onAssignOmdb,
onAssignCdMetadata,
onResumeReady, onResumeReady,
onRestartEncode, onRestartEncode,
onRestartReview, onRestartReview,
@@ -389,6 +390,7 @@ export default function JobDetailDialog({
onRemoveFromQueue, onRemoveFromQueue,
isQueued = false, isQueued = false,
omdbAssignBusy = false, omdbAssignBusy = false,
cdMetadataAssignBusy = false,
actionBusy = false, actionBusy = false,
reencodeBusy = false, reencodeBusy = false,
deleteEntryBusy = false deleteEntryBusy = false
@@ -748,7 +750,17 @@ export default function JobDetailDialog({
loading={omdbAssignBusy} loading={omdbAssignBusy}
disabled={running || typeof onAssignOmdb !== 'function'} disabled={running || typeof onAssignOmdb !== 'function'}
/> />
) : null} ) : (
<Button
label="MusicBrainz neu zuordnen"
icon="pi pi-search"
severity="secondary"
size="small"
onClick={() => onAssignCdMetadata?.(job)}
loading={cdMetadataAssignBusy}
disabled={running || typeof onAssignCdMetadata !== 'function'}
/>
)}
{!isCd && canResumeReady ? ( {!isCd && canResumeReady ? (
<Button <Button
label="Im Dashboard öffnen" label="Im Dashboard öffnen"

View File

@@ -10,6 +10,7 @@ import { Toast } from 'primereact/toast';
import { api } from '../api/client'; import { api } from '../api/client';
import JobDetailDialog from '../components/JobDetailDialog'; import JobDetailDialog from '../components/JobDetailDialog';
import MetadataSelectionDialog from '../components/MetadataSelectionDialog'; import MetadataSelectionDialog from '../components/MetadataSelectionDialog';
import CdMetadataDialog from '../components/CdMetadataDialog';
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';
@@ -75,6 +76,9 @@ export default function DatabasePage() {
const [metadataDialogVisible, setMetadataDialogVisible] = useState(false); const [metadataDialogVisible, setMetadataDialogVisible] = useState(false);
const [metadataDialogContext, setMetadataDialogContext] = useState(null); const [metadataDialogContext, setMetadataDialogContext] = useState(null);
const [metadataDialogBusy, setMetadataDialogBusy] = useState(false); const [metadataDialogBusy, setMetadataDialogBusy] = useState(false);
const [cdMetadataDialogVisible, setCdMetadataDialogVisible] = useState(false);
const [cdMetadataDialogContext, setCdMetadataDialogContext] = useState(null);
const [cdMetadataDialogBusy, setCdMetadataDialogBusy] = useState(false);
const [actionBusy, setActionBusy] = useState(false); const [actionBusy, setActionBusy] = useState(false);
const [reencodeBusyJobId, setReencodeBusyJobId] = useState(null); const [reencodeBusyJobId, setReencodeBusyJobId] = useState(null);
const [deleteEntryBusyJobId, setDeleteEntryBusyJobId] = useState(null); const [deleteEntryBusyJobId, setDeleteEntryBusyJobId] = useState(null);
@@ -527,6 +531,77 @@ export default function DatabasePage() {
} }
}; };
const handleMusicBrainzSearch = async (query) => {
try {
const response = await api.searchMusicBrainz(query);
return response.results || [];
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'MusicBrainz Suche fehlgeschlagen', detail: error.message, life: 4500 });
return [];
}
};
const handleMusicBrainzReleaseFetch = async (mbId) => {
try {
const response = await api.getMusicBrainzRelease(mbId);
return response.release || null;
} catch (error) {
toastRef.current?.show({ severity: 'error', summary: 'MusicBrainz Release fehlgeschlagen', detail: error.message, life: 4500 });
return null;
}
};
const openCdMetadataAssignDialog = (row) => {
if (!row?.id) {
return;
}
const makemkvInfo = row.makemkvInfo && typeof row.makemkvInfo === 'object' ? row.makemkvInfo : {};
const tocTracks = Array.isArray(makemkvInfo.tracks) ? makemkvInfo.tracks : [];
const selectedMetadata = makemkvInfo.selectedMetadata && typeof makemkvInfo.selectedMetadata === 'object'
? makemkvInfo.selectedMetadata
: {};
setCdMetadataDialogContext({
jobId: row.id,
detectedTitle: row.title || row.detected_title || selectedMetadata.title || '',
tracks: tocTracks
});
setCdMetadataDialogVisible(true);
};
const handleCdMetadataAssignSubmit = async (payload) => {
const jobId = Number(payload?.jobId || cdMetadataDialogContext?.jobId || 0);
if (!jobId) {
return;
}
setCdMetadataDialogBusy(true);
try {
const response = await api.assignJobCdMetadata(jobId, payload);
toastRef.current?.show({
severity: 'success',
summary: 'CD-Metadaten aktualisiert',
detail: `Job #${jobId} wurde aktualisiert.`,
life: 3500
});
setCdMetadataDialogVisible(false);
await load();
if (detailVisible && selectedJob?.id === jobId && response?.job) {
setSelectedJob(response.job);
} else {
await refreshDetailIfOpen(jobId);
}
} catch (error) {
toastRef.current?.show({
severity: 'error',
summary: 'CD-Metadaten fehlgeschlagen',
detail: error.message,
life: 5000
});
} finally {
setCdMetadataDialogBusy(false);
}
};
const openMetadataAssignDialog = (row) => { const openMetadataAssignDialog = (row) => {
if (!row?.id) { if (!row?.id) {
return; return;
@@ -751,6 +826,7 @@ export default function DatabasePage() {
setLogLoadingMode(null); setLogLoadingMode(null);
}} }}
onAssignOmdb={openMetadataAssignDialog} onAssignOmdb={openMetadataAssignDialog}
onAssignCdMetadata={openCdMetadataAssignDialog}
onResumeReady={handleResumeReady} onResumeReady={handleResumeReady}
onRestartEncode={handleRestartEncode} onRestartEncode={handleRestartEncode}
onRestartReview={handleRestartReview} onRestartReview={handleRestartReview}
@@ -760,6 +836,7 @@ export default function DatabasePage() {
onRemoveFromQueue={handleRemoveFromQueue} onRemoveFromQueue={handleRemoveFromQueue}
isQueued={Boolean(selectedJob?.id && queuedJobIdSet.has(normalizeJobId(selectedJob.id)))} isQueued={Boolean(selectedJob?.id && queuedJobIdSet.has(normalizeJobId(selectedJob.id)))}
omdbAssignBusy={metadataDialogBusy} omdbAssignBusy={metadataDialogBusy}
cdMetadataAssignBusy={cdMetadataDialogBusy}
actionBusy={actionBusy} actionBusy={actionBusy}
reencodeBusy={reencodeBusyJobId === selectedJob?.id} reencodeBusy={reencodeBusyJobId === selectedJob?.id}
deleteEntryBusy={deleteEntryBusyJobId === selectedJob?.id} deleteEntryBusy={deleteEntryBusyJobId === selectedJob?.id}
@@ -773,6 +850,19 @@ export default function DatabasePage() {
onSearch={handleOmdbSearch} onSearch={handleOmdbSearch}
busy={metadataDialogBusy} busy={metadataDialogBusy}
/> />
<CdMetadataDialog
visible={cdMetadataDialogVisible}
context={cdMetadataDialogContext || {}}
onHide={() => {
setCdMetadataDialogVisible(false);
setCdMetadataDialogContext(null);
}}
onSubmit={handleCdMetadataAssignSubmit}
onSearch={handleMusicBrainzSearch}
onFetchRelease={handleMusicBrainzReleaseFetch}
busy={cdMetadataDialogBusy}
/>
</div> </div>
); );
} }

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.9.0-1", "version": "0.9.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.9.0-1", "version": "0.9.1",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "ripster", "name": "ripster",
"private": true, "private": true,
"version": "0.9.0-1", "version": "0.9.1",
"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",