0.13.0-1 Series Flow
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-backend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@@ -905,11 +905,27 @@ function enrichJobRow(job, settings = null, options = {}) {
|
||||
const backupSuccess = ripSuccessful;
|
||||
const finishedWithOutput = normalizedJobStatus === 'FINISHED'
|
||||
&& (includeFsChecks ? Boolean(outputStatus?.exists) : true);
|
||||
const seriesBatchSummary = handbrakeInfo?.seriesBatch && typeof handbrakeInfo.seriesBatch === 'object'
|
||||
? handbrakeInfo.seriesBatch
|
||||
: null;
|
||||
const seriesBatchMode = String(handbrakeInfo?.mode || '').trim().toLowerCase() === 'series_batch';
|
||||
const seriesBatchTotal = Number(seriesBatchSummary?.totalCount || 0);
|
||||
const seriesBatchFinished = Number(seriesBatchSummary?.finishedCount || 0);
|
||||
const seriesBatchErrors = Number(seriesBatchSummary?.errorCount || 0);
|
||||
const seriesBatchCancelled = Number(seriesBatchSummary?.cancelledCount || 0);
|
||||
const seriesBatchEncodeSuccess = seriesBatchMode
|
||||
&& seriesBatchTotal > 0
|
||||
&& seriesBatchFinished >= seriesBatchTotal
|
||||
&& seriesBatchErrors <= 0
|
||||
&& seriesBatchCancelled <= 0;
|
||||
const encodeSuccess = directoryLikeOutput
|
||||
? finishedWithOutput
|
||||
: (mediaType === 'converter'
|
||||
? finishedWithOutput
|
||||
: String(handbrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS');
|
||||
: (
|
||||
seriesBatchEncodeSuccess
|
||||
|| (mediaType === 'converter'
|
||||
? finishedWithOutput
|
||||
: String(handbrakeInfo?.status || '').trim().toUpperCase() === 'SUCCESS')
|
||||
);
|
||||
|
||||
return {
|
||||
...job,
|
||||
@@ -3629,6 +3645,7 @@ class HistoryService {
|
||||
});
|
||||
const seriesCandidateIds = seriesCandidates.map((job) => normalizeJobIdValue(job?.id)).filter(Boolean);
|
||||
let childRows = [];
|
||||
let nestedChildRows = [];
|
||||
let outputFolders = [];
|
||||
if (containerIds.length > 0) {
|
||||
const placeholders = containerIds.map(() => '?').join(', ');
|
||||
@@ -3639,15 +3656,46 @@ class HistoryService {
|
||||
const childIds = childRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean);
|
||||
if (childIds.length > 0) {
|
||||
const childPlaceholders = childIds.map(() => '?').join(', ');
|
||||
nestedChildRows = await db.all(
|
||||
`SELECT id, parent_job_id, output_path, raw_path, handbrake_info_json, status, encode_plan_json, makemkv_info_json, rip_successful FROM jobs WHERE parent_job_id IN (${childPlaceholders})`,
|
||||
childIds
|
||||
);
|
||||
}
|
||||
const summaryJobIds = Array.from(new Set([
|
||||
...childRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean),
|
||||
...nestedChildRows.map((row) => normalizeJobIdValue(row?.id)).filter(Boolean)
|
||||
]));
|
||||
if (summaryJobIds.length > 0) {
|
||||
const childPlaceholders = summaryJobIds.map(() => '?').join(', ');
|
||||
outputFolders = await db.all(
|
||||
`SELECT job_id, output_path FROM job_output_folders WHERE job_id IN (${childPlaceholders})`,
|
||||
childIds
|
||||
summaryJobIds
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const allContainerChildRows = [...childRows, ...nestedChildRows];
|
||||
const directChildRowById = new Map();
|
||||
for (const row of childRows) {
|
||||
const childId = normalizeJobIdValue(row?.id);
|
||||
if (childId) {
|
||||
directChildRowById.set(childId, row);
|
||||
}
|
||||
}
|
||||
const nestedRowsByParentId = new Map();
|
||||
for (const row of nestedChildRows) {
|
||||
const parentId = normalizeJobIdValue(row?.parent_job_id);
|
||||
if (!parentId) {
|
||||
continue;
|
||||
}
|
||||
if (!nestedRowsByParentId.has(parentId)) {
|
||||
nestedRowsByParentId.set(parentId, []);
|
||||
}
|
||||
nestedRowsByParentId.get(parentId).push(row);
|
||||
}
|
||||
|
||||
const childOutputMap = new Map();
|
||||
for (const child of childRows) {
|
||||
for (const child of allContainerChildRows) {
|
||||
const childId = normalizeJobIdValue(child?.id);
|
||||
if (!childId) {
|
||||
continue;
|
||||
@@ -3701,9 +3749,12 @@ class HistoryService {
|
||||
let expectedFromPlans = 0;
|
||||
const seenOutputs = new Set();
|
||||
let existingCount = 0;
|
||||
let existingCountFromSeriesBatch = 0;
|
||||
const totalChildren = childIds.length;
|
||||
for (const childId of childIds) {
|
||||
const childRow = childRows.find((row) => normalizeJobIdValue(row?.id) === childId) || null;
|
||||
const childRow = directChildRowById.get(childId) || null;
|
||||
const descendantRows = nestedRowsByParentId.get(childId) || [];
|
||||
const relatedRows = [childRow, ...descendantRows].filter(Boolean);
|
||||
if (childRow) {
|
||||
const rawPath = String(childRow?.raw_path || '').trim();
|
||||
if (rawPath) {
|
||||
@@ -3722,32 +3773,87 @@ class HistoryService {
|
||||
if (ripSuccessful) {
|
||||
backupSuccessCount += 1;
|
||||
}
|
||||
let selectedCount = 0;
|
||||
const childPlan = parseJsonSafe(childRow?.encode_plan_json, null);
|
||||
if (childPlan && typeof childPlan === 'object') {
|
||||
const titles = Array.isArray(childPlan?.titles) ? childPlan.titles : [];
|
||||
const selectedCount = titles.filter((title) => title?.selectedForEncode || title?.encodeInput).length;
|
||||
if (selectedCount > 0) {
|
||||
expectedFromPlans += selectedCount;
|
||||
}
|
||||
selectedCount = titles.filter((title) => title?.selectedForEncode || title?.encodeInput).length;
|
||||
}
|
||||
const handbrakeInfo = parseJsonSafe(childRow?.handbrake_info_json, null);
|
||||
const hbStatus = String(handbrakeInfo?.status || '').trim().toUpperCase();
|
||||
if (hbStatus === 'SUCCESS') {
|
||||
encodeSuccessAny = true;
|
||||
encodeSuccessCount += 1;
|
||||
const seriesBatchSummary = handbrakeInfo?.seriesBatch && typeof handbrakeInfo.seriesBatch === 'object'
|
||||
? handbrakeInfo.seriesBatch
|
||||
: null;
|
||||
const seriesBatchTotal = Number(seriesBatchSummary?.totalCount || 0);
|
||||
const seriesBatchFinished = Number(seriesBatchSummary?.finishedCount || 0);
|
||||
const seriesBatchErrors = Number(seriesBatchSummary?.errorCount || 0);
|
||||
const seriesBatchCancelled = Number(seriesBatchSummary?.cancelledCount || 0);
|
||||
const expectedFromNested = descendantRows.length > 0 ? descendantRows.length : 0;
|
||||
const expectedFromChild = seriesBatchTotal > 0
|
||||
? seriesBatchTotal
|
||||
: (selectedCount > 0 ? selectedCount : expectedFromNested);
|
||||
if (expectedFromChild > 0) {
|
||||
expectedFromPlans += expectedFromChild;
|
||||
}
|
||||
if (seriesBatchFinished > 0) {
|
||||
existingCountFromSeriesBatch += seriesBatchFinished;
|
||||
}
|
||||
|
||||
let childEncodeSuccess = hbStatus === 'SUCCESS';
|
||||
if (
|
||||
!childEncodeSuccess
|
||||
&& seriesBatchTotal > 0
|
||||
&& seriesBatchFinished >= seriesBatchTotal
|
||||
&& seriesBatchErrors <= 0
|
||||
&& seriesBatchCancelled <= 0
|
||||
) {
|
||||
childEncodeSuccess = true;
|
||||
}
|
||||
if (!childEncodeSuccess && descendantRows.length > 0) {
|
||||
let finishedDescendants = 0;
|
||||
let errorDescendants = 0;
|
||||
let cancelledDescendants = 0;
|
||||
for (const row of descendantRows) {
|
||||
const rowState = String(row?.status || row?.last_state || '').trim().toUpperCase();
|
||||
if (rowState === 'FINISHED') {
|
||||
finishedDescendants += 1;
|
||||
} else if (rowState === 'ERROR') {
|
||||
errorDescendants += 1;
|
||||
} else if (rowState === 'CANCELLED') {
|
||||
cancelledDescendants += 1;
|
||||
}
|
||||
}
|
||||
childEncodeSuccess = finishedDescendants >= descendantRows.length
|
||||
&& errorDescendants === 0
|
||||
&& cancelledDescendants === 0;
|
||||
}
|
||||
if (childEncodeSuccess) {
|
||||
encodeSuccessAny = true;
|
||||
encodeSuccessCount += 1;
|
||||
}
|
||||
} else if (descendantRows.length > 0) {
|
||||
expectedFromPlans += descendantRows.length;
|
||||
}
|
||||
const outputs = Array.from(childOutputMap.get(childId) || []);
|
||||
for (const outputPath of outputs) {
|
||||
if (!outputPath || seenOutputs.has(outputPath)) {
|
||||
for (const relatedRow of relatedRows) {
|
||||
const relatedRowId = normalizeJobIdValue(relatedRow?.id);
|
||||
if (!relatedRowId) {
|
||||
continue;
|
||||
}
|
||||
seenOutputs.add(outputPath);
|
||||
if (!includeFsChecks || fs.existsSync(outputPath)) {
|
||||
existingCount += 1;
|
||||
const outputs = Array.from(childOutputMap.get(relatedRowId) || []);
|
||||
for (const outputPath of outputs) {
|
||||
if (!outputPath || seenOutputs.has(outputPath)) {
|
||||
continue;
|
||||
}
|
||||
seenOutputs.add(outputPath);
|
||||
if (!includeFsChecks || fs.existsSync(outputPath)) {
|
||||
existingCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (existingCountFromSeriesBatch > existingCount) {
|
||||
existingCount = existingCountFromSeriesBatch;
|
||||
}
|
||||
if (!encodeSuccessAny && existingCount > 0) {
|
||||
encodeSuccessAny = true;
|
||||
if (encodeSuccessCount === 0 && totalChildren > 0) {
|
||||
@@ -5102,6 +5208,18 @@ class HistoryService {
|
||||
?? analyzeContext?.seriesLookupHint?.discNumber
|
||||
?? null
|
||||
);
|
||||
const resolvedJobMediaType = inferMediaType(
|
||||
job,
|
||||
makemkvInfo,
|
||||
job?.mediainfo_info_json,
|
||||
job?.encode_plan_json,
|
||||
job?.handbrake_info_json
|
||||
);
|
||||
if (resolvedJobMediaType === 'dvd' && discNumber === null) {
|
||||
const error = new Error('Serien-DVD erkannt: Disk-Nummer ist Pflicht (Start bei 1).');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
let metadataKind = manualMetadataKind
|
||||
|| String(
|
||||
existingSelectedMetadata?.metadataKind
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"dependencies": {
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -739,6 +739,21 @@ function resolveSeriesDiscNumber(job) {
|
||||
);
|
||||
}
|
||||
|
||||
function compareSeriesChildJobsByDisc(leftJob, rightJob) {
|
||||
const leftDisc = resolveSeriesDiscNumber(leftJob);
|
||||
const rightDisc = resolveSeriesDiscNumber(rightJob);
|
||||
if (leftDisc !== null && rightDisc !== null && leftDisc !== rightDisc) {
|
||||
return leftDisc - rightDisc;
|
||||
}
|
||||
if (leftDisc !== null && rightDisc === null) {
|
||||
return -1;
|
||||
}
|
||||
if (leftDisc === null && rightDisc !== null) {
|
||||
return 1;
|
||||
}
|
||||
return Number(leftJob?.id || 0) - Number(rightJob?.id || 0);
|
||||
}
|
||||
|
||||
function BoolState({ value }) {
|
||||
const isTrue = Boolean(value);
|
||||
return isTrue ? (
|
||||
@@ -1113,7 +1128,9 @@ export default function JobDetailDialog({
|
||||
const metadataDetails = resolveMetadataDetailsForDisplay(job, omdbInfo);
|
||||
const seriesDiscNumber = isDvdSeries ? resolveSeriesDiscNumber(job) : null;
|
||||
const seriesDiscLabel = seriesDiscNumber ? `Disk ${seriesDiscNumber}` : 'Disk unbekannt';
|
||||
const childJobs = Array.isArray(job?.children) ? job.children : [];
|
||||
const childJobs = Array.isArray(job?.children)
|
||||
? [...job.children].sort(compareSeriesChildJobsByDisc)
|
||||
: [];
|
||||
const isSeriesContainer = isDvdSeries && String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container';
|
||||
const seriesChildSummary = job?.seriesChildSummary || null;
|
||||
const seriesBackupSummary = isSeriesContainer ? seriesChildSummary?.backup : null;
|
||||
|
||||
@@ -13,6 +13,17 @@ export default function MetadataSelectionDialog({
|
||||
onSearch,
|
||||
busy
|
||||
}) {
|
||||
const parseDiscNumber = (rawValue) => {
|
||||
const text = String(rawValue ?? '').trim();
|
||||
if (!/^\d+$/.test(text)) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(text);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.trunc(parsed);
|
||||
};
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [query, setQuery] = useState('');
|
||||
const [manualTitle, setManualTitle] = useState('');
|
||||
@@ -23,9 +34,18 @@ export default function MetadataSelectionDialog({
|
||||
const [searchBusy, setSearchBusy] = useState(false);
|
||||
const [searchError, setSearchError] = useState('');
|
||||
const [seasonFilter, setSeasonFilter] = useState('');
|
||||
const [discValidationError, setDiscValidationError] = useState('');
|
||||
const metadataProvider = String(context?.metadataProvider || 'omdb').trim().toLowerCase() || 'omdb';
|
||||
const mediaProfile = String(
|
||||
context?.mediaProfile
|
||||
|| context?.selectedMetadata?.mediaProfile
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
const providerLabel = metadataProvider === 'tmdb' ? 'TMDb' : 'OMDb';
|
||||
const supportsExternalIdInput = metadataProvider === 'omdb';
|
||||
const requiresDiscNumber = metadataProvider === 'tmdb' && (mediaProfile === 'dvd' || !mediaProfile);
|
||||
const effectiveDiscNumber = parseDiscNumber(manualDiscNumber);
|
||||
const hasValidDiscNumber = effectiveDiscNumber !== null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
@@ -51,8 +71,18 @@ export default function MetadataSelectionDialog({
|
||||
setSearchBusy(false);
|
||||
setSearchError('');
|
||||
setSeasonFilter('');
|
||||
setDiscValidationError('');
|
||||
}, [visible, context]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!discValidationError) {
|
||||
return;
|
||||
}
|
||||
if (!requiresDiscNumber || hasValidDiscNumber) {
|
||||
setDiscValidationError('');
|
||||
}
|
||||
}, [discValidationError, requiresDiscNumber, hasValidDiscNumber]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const base = Array.isArray(context?.metadataCandidates)
|
||||
? context.metadataCandidates
|
||||
@@ -136,10 +166,10 @@ export default function MetadataSelectionDialog({
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const parsedDiscNumber = Number(manualDiscNumber);
|
||||
const effectiveDiscNumber = Number.isFinite(parsedDiscNumber) && parsedDiscNumber > 0
|
||||
? Math.trunc(parsedDiscNumber)
|
||||
: null;
|
||||
if (requiresDiscNumber && !hasValidDiscNumber) {
|
||||
setDiscValidationError('Disk-Nummer ist Pflicht und muss eine ganze Zahl >= 1 sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = selected
|
||||
? {
|
||||
@@ -174,11 +204,19 @@ export default function MetadataSelectionDialog({
|
||||
await onSubmit(payload);
|
||||
};
|
||||
|
||||
const handleHideRequest = () => {
|
||||
if (requiresDiscNumber && !hasValidDiscNumber) {
|
||||
setDiscValidationError('Disk-Nummer ist Pflicht und muss eine ganze Zahl >= 1 sein.');
|
||||
return;
|
||||
}
|
||||
onHide?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header="Metadaten auswählen"
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
onHide={handleHideRequest}
|
||||
style={{ width: '52rem', maxWidth: '95vw' }}
|
||||
className="metadata-selection-dialog"
|
||||
breakpoints={{ '1200px': '92vw', '768px': '96vw', '560px': '98vw' }}
|
||||
@@ -211,16 +249,19 @@ export default function MetadataSelectionDialog({
|
||||
<InputText
|
||||
value={manualDiscNumber}
|
||||
onChange={(event) => setManualDiscNumber(event.target.value)}
|
||||
placeholder="Disc-Nr (optional)"
|
||||
placeholder={requiresDiscNumber ? 'Disc-Nr (Pflicht)' : 'Disc-Nr (optional)'}
|
||||
inputMode="numeric"
|
||||
disabled={searchBusy || busy}
|
||||
/>
|
||||
<small className="metadata-filter-subtitle">
|
||||
Optionale Disc-Nummer fuer internes Mapping und Output-Templates. Nur Werte groesser 0 werden gespeichert.
|
||||
{requiresDiscNumber
|
||||
? 'Pflichtfeld fuer Serien-DVDs. Ohne Disk-Nummer koennen die Metadaten nicht verlassen werden.'
|
||||
: 'Optionale Disc-Nummer fuer internes Mapping und Output-Templates. Nur Werte groesser 0 werden gespeichert.'}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{searchError ? <small className="error-text">{searchError}</small> : null}
|
||||
{discValidationError ? <small className="error-text">{discValidationError}</small> : null}
|
||||
|
||||
<div className="table-scroll-wrap table-scroll-medium">
|
||||
<DataTable
|
||||
@@ -271,13 +312,22 @@ export default function MetadataSelectionDialog({
|
||||
</div>
|
||||
|
||||
<div className="dialog-actions">
|
||||
<Button label="Abbrechen" severity="secondary" text onClick={onHide} />
|
||||
<Button
|
||||
label="Abbrechen"
|
||||
severity="secondary"
|
||||
text
|
||||
onClick={handleHideRequest}
|
||||
disabled={requiresDiscNumber && !hasValidDiscNumber}
|
||||
/>
|
||||
<Button
|
||||
label="Auswahl übernehmen"
|
||||
icon="pi pi-play"
|
||||
onClick={handleSubmit}
|
||||
loading={busy}
|
||||
disabled={!selected && !manualTitle.trim() && !manualImdb.trim()}
|
||||
disabled={
|
||||
(!selected && !manualTitle.trim() && !manualImdb.trim())
|
||||
|| (requiresDiscNumber && !hasValidDiscNumber)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"private": true,
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.0-1",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||
"dev:backend": "npm run dev --prefix backend",
|
||||
|
||||
Reference in New Issue
Block a user