0.9.1 Fix Restart
This commit is contained in:
4
backend/package-lock.json
generated
4
backend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
4
package-lock.json
generated
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user