0.14.0 BluRay Series Flow

This commit is contained in:
2026-04-20 08:41:48 +00:00
parent 010c96461c
commit 9890971ff3
23 changed files with 2282 additions and 310 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "ripster-frontend",
"version": "0.13.1-12",
"version": "0.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ripster-frontend",
"version": "0.13.1-12",
"version": "0.14.0",
"dependencies": {
"primeicons": "^7.0.0",
"primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ripster-frontend",
"version": "0.13.1-12",
"version": "0.14.0",
"private": true,
"type": "module",
"scripts": {
+12 -2
View File
@@ -806,10 +806,20 @@ export const api = {
afterMutationInvalidate(['/history']);
return result;
},
async deleteJobFiles(jobId, target = 'both') {
async deleteJobFiles(jobId, target = 'both', options = {}) {
const includeRelated = Boolean(options?.includeRelated);
const selectedMoviePaths = Array.isArray(options?.selectedMoviePaths)
? options.selectedMoviePaths
.map((item) => String(item || '').trim())
.filter(Boolean)
: null;
const result = await request(`/history/${jobId}/delete-files`, {
method: 'POST',
body: JSON.stringify({ target })
body: JSON.stringify({
target,
includeRelated,
...(selectedMoviePaths ? { selectedMoviePaths } : {})
})
});
afterMutationInvalidate(['/history']);
return result;
@@ -501,7 +501,15 @@ function buildToolSections(settings) {
}
// Path keys per medium — _owner keys are rendered inline
const BLURAY_PATH_KEYS = ['raw_dir_bluray', 'movie_dir_bluray', 'output_template_bluray'];
const BLURAY_PATH_KEYS = [
'raw_dir_bluray',
'raw_dir_bluray_series',
'movie_dir_bluray',
'series_dir_bluray',
'output_template_bluray',
'output_template_bluray_series_episode',
'output_template_bluray_series_multi_episode'
];
const DVD_PATH_KEYS = [
'raw_dir_dvd',
'raw_dir_dvd_series',
@@ -816,7 +824,9 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
const ep = effectivePaths || {};
const blurayRaw = ep.bluray?.raw || defaultRaw;
const bluraySeriesRaw = ep.bluray?.seriesRaw || blurayRaw;
const blurayMovies = ep.bluray?.movies || defaultMovies;
const bluraySeries = ep.bluray?.series || defaultSeries;
const dvdRaw = ep.dvd?.raw || defaultRaw;
const dvdSeriesRaw = ep.dvd?.seriesRaw || dvdRaw;
const dvdMovies = ep.dvd?.movies || defaultMovies;
@@ -860,6 +870,17 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
{isDefault(blurayMovies, defaultMovies) && <span className="path-default-badge">Standard</span>}
</td>
</tr>
<tr>
<td><strong>Blu-ray Serie</strong></td>
<td>
<code>{bluraySeriesRaw}</code>
{isDefault(bluraySeriesRaw, blurayRaw) && <span className="path-default-badge">wie Blu-ray RAW</span>}
</td>
<td>
<code>{bluraySeries}</code>
{isDefault(bluraySeries, defaultSeries) && <span className="path-default-badge">Standard</span>}
</td>
</tr>
<tr>
<td><strong>DVD</strong></td>
<td>
+7 -6
View File
@@ -6,7 +6,7 @@ import blurayIndicatorIcon from '../assets/media-bluray.svg';
import discIndicatorIcon from '../assets/media-disc.svg';
import otherIndicatorIcon from '../assets/media-other.svg';
import { getProcessStatusLabel, getStatusLabel } from '../utils/statusPresentation';
import { isSeriesDvdJob } from '../utils/jobTaxonomy';
import { isSeriesVideoJob } from '../utils/jobTaxonomy';
const CD_FORMAT_LABELS = {
flac: 'FLAC',
@@ -136,6 +136,7 @@ function shellQuote(value) {
function trackLang(value) {
const raw = String(value || '').trim().toLowerCase();
if (!raw) return 'und';
if (raw === 'und' || raw === 'unknown' || raw === 'un') return 'und';
const map = { en: 'en', eng: 'en', de: 'de', deu: 'de', ger: 'de', tr: 'tr', tur: 'tr', fr: 'fr', fra: 'fr', fre: 'fr', es: 'es', spa: 'es', it: 'it', ita: 'it' };
if (map[raw]) return map[raw];
return raw.length >= 2 ? raw.slice(0, 2) : raw;
@@ -964,7 +965,7 @@ export default function JobDetailDialog({
const showCancelAction = (running || softCancelable) && typeof onCancel === 'function';
const showFinalLog = !running;
const mediaType = resolveMediaType(job);
const isDvdSeries = mediaType === 'dvd' && isSeriesDvdJob(job);
const isDvdSeries = (mediaType === 'dvd' || mediaType === 'bluray') && isSeriesVideoJob(job);
const seriesBatchEpisodes = isDvdSeries ? mergeSeriesBatchEpisodes(job) : [];
const seriesEpisodeAssignments = isDvdSeries && job?.encodePlan?.episodeAssignments && typeof job.encodePlan.episodeAssignments === 'object'
? job.encodePlan.episodeAssignments
@@ -1151,7 +1152,7 @@ export default function JobDetailDialog({
const audiobookDetails = isAudiobook ? resolveAudiobookDetails(job) : null;
const canRetry = isCd && !running && typeof onRetry === 'function';
const mediaTypeLabel = mediaType === 'bluray'
? 'Blu-ray'
? (isDvdSeries ? 'Blu-ray Serie' : 'Blu-ray')
: mediaType === 'dvd'
? (isDvdSeries ? 'DVD Serie' : 'DVD')
: isCd
@@ -2323,7 +2324,7 @@ export default function JobDetailDialog({
<div className="actions-group-label"><i className="pi pi-trash" /> Dateien löschen</div>
<div className="action-item">
<Button
label="RAW löschen"
label="RAW löschen (Eintrag bleibt)"
icon="pi pi-trash"
severity="warning"
outlined
@@ -2336,7 +2337,7 @@ export default function JobDetailDialog({
</div>
<div className="action-item">
<Button
label={isCd ? 'Audio löschen' : (isAudiobook ? 'Ausgabe löschen' : (isConverter ? 'Output löschen' : 'Movie löschen'))}
label={isCd ? 'Audio löschen (Eintrag bleibt)' : (isAudiobook ? 'Ausgabe löschen (Eintrag bleibt)' : (isConverter ? 'Output löschen (Eintrag bleibt)' : 'Movie löschen (Eintrag bleibt)'))}
icon="pi pi-trash"
severity="warning"
outlined
@@ -2349,7 +2350,7 @@ export default function JobDetailDialog({
</div>
<div className="action-item">
<Button
label="Beides löschen"
label="Beides löschen (Eintrag bleibt)"
icon="pi pi-times"
severity="danger"
size="small"
@@ -685,6 +685,9 @@ function toLang2(value) {
if (!raw) {
return 'und';
}
if (raw === 'und' || raw === 'unknown' || raw === 'un') {
return 'und';
}
const map = {
en: 'en',
eng: 'en',
@@ -2185,10 +2188,9 @@ export default function MediaInfoReviewPanel({
disabled={!allowTitleSelection}
style={{ width: '100%' }}
filter
showClear
/>
<small>
Startet bei der gewählten Episode und befüllt alle ausgewählten DVD-Titel der Reihe nach.
Startet bei der gewählten Episode und befüllt alle ausgewählten Episoden-Titel der Reihe nach.
Multi-Episoden werden als Bereich (z.B. E16-17 | Teil 1+2) angezeigt.
</small>
</div>
+221 -42
View File
@@ -486,6 +486,34 @@ function normalizePositiveInt(value) {
return Math.trunc(parsed);
}
function hasSeriesMetadataShape(source = null) {
const metadata = source && typeof source === 'object' ? source : {};
const seasonNumber = normalizePositiveInt(metadata?.seasonNumber ?? metadata?.season ?? null);
const episodes = Array.isArray(metadata?.episodes) ? metadata.episodes : [];
const metadataProvider = String(metadata?.metadataProvider || '').trim().toLowerCase();
const workflowKind = String(metadata?.workflowKind || metadata?.kind || '').trim().toLowerCase();
return Boolean(
seasonNumber
|| episodes.length > 0
|| metadataProvider === 'tmdb'
|| metadataProvider === 'themoviedb'
|| workflowKind === 'series'
|| workflowKind === 'season'
);
}
function hasSeriesEpisodeAssignmentHints(titles = []) {
const reviewTitles = Array.isArray(titles) ? titles : [];
return reviewTitles.some((title) => {
const seasonNumber = normalizePositiveInt(title?.seasonNumber ?? title?.season ?? null);
const episodeNumber = normalizePositiveInt(title?.episodeNumber ?? title?.number ?? null);
const episodeId = normalizePositiveInt(title?.episodeId ?? null);
const episodeTitle = String(title?.episodeTitle || '').trim();
const episodeTitleStart = String(title?.episodeTitleStart || '').trim();
return Boolean(seasonNumber || episodeNumber || episodeId || episodeTitle || episodeTitleStart);
});
}
function normalizeEpisodeReference(entry = {}) {
const source = entry && typeof entry === 'object' ? entry : {};
const episodeId = normalizePositiveInt(source?.episodeId ?? source?.id ?? null);
@@ -605,6 +633,20 @@ function normalizeSeriesLanguage(value) {
return raw;
}
function normalizeSeriesTitleForOutputPath(value, fallback = 'series') {
const raw = String(value || '').trim();
if (!raw) {
return String(fallback || 'series').trim() || 'series';
}
const stripped = raw
.replace(/\s*-\s*S\d{1,2}E\d{1,3}(?:-\d{1,3})?\b.*$/i, '')
.trim();
if (stripped) {
return stripped;
}
return raw;
}
function normalizeSeriesBatchStatus(value) {
return String(value || '').trim().toUpperCase() || 'UNKNOWN';
}
@@ -1061,6 +1103,7 @@ function buildFallbackEpisodeTitleForOutput(episodeRange = null, template = '')
function buildDvdSeriesEpisodeOutputPathPreview(
settings,
mediaProfile,
metadata,
title,
assignment = null,
@@ -1069,9 +1112,13 @@ function buildDvdSeriesEpisodeOutputPathPreview(
assignmentByTitle = {},
selectedTitleIds = []
) {
const normalizedSeriesMediaProfile = String(mediaProfile || '').trim().toLowerCase() === 'bluray'
? 'bluray'
: 'dvd';
const seriesRootDir = String(
settings?.series_dir_dvd
|| settings?.movie_dir_dvd
(normalizedSeriesMediaProfile === 'bluray'
? (settings?.series_dir_bluray || settings?.series_dir_dvd || settings?.movie_dir_bluray || settings?.movie_dir_dvd)
: (settings?.series_dir_dvd || settings?.series_dir_bluray || settings?.movie_dir_dvd || settings?.movie_dir_bluray))
|| settings?.movie_dir
|| ''
).trim();
@@ -1105,21 +1152,29 @@ function buildDvdSeriesEpisodeOutputPathPreview(
const episodeRangeToken = buildEpisodeRangeToken(effectiveEpisodeRange.start, effectiveEpisodeRange.end);
const partsToken = buildEpisodePartsToken(effectiveEpisodeRange);
const discNumber = normalizePositiveInt(metadata?.discNumber ?? assignment?.discNumber ?? null);
const seriesTitleRaw = String(
metadata?.title
|| metadata?.seriesTitle
|| (fallbackJobId ? `job-${fallbackJobId}` : 'series')
).trim();
const seriesTitle = seriesTitleRaw || 'series';
const seriesTitle = normalizeSeriesTitleForOutputPath(
metadata?.seriesTitle
|| metadata?.title
|| (fallbackJobId ? `job-${fallbackJobId}` : 'series'),
fallbackJobId ? `job-${fallbackJobId}` : 'series'
);
const year = String(metadata?.year || new Date().getFullYear()).trim();
const language = normalizeSeriesLanguage(settings?.dvd_series_language || metadata?.language || null);
const singleTemplateRaw = String(
settings?.output_template_dvd_series_episode
(normalizedSeriesMediaProfile === 'bluray'
? settings?.output_template_bluray_series_episode
: settings?.output_template_dvd_series_episode)
|| settings?.output_template_dvd_series_episode
|| settings?.output_template_bluray_series_episode
|| '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}'
).trim();
const multiTemplateRaw = String(
settings?.output_template_dvd_series_multi_episode
(normalizedSeriesMediaProfile === 'bluray'
? settings?.output_template_bluray_series_multi_episode
: settings?.output_template_dvd_series_multi_episode)
|| settings?.output_template_dvd_series_multi_episode
|| settings?.output_template_bluray_series_multi_episode
|| '{seriesTitle}/Staffel {seasonNr}/S{0:seasonNr}E{episodeRange} - {episodeTitle} (Teil {parts})'
).trim();
const singleTemplate = singleTemplateRaw || '{seriesTitle}/Staffel {seasonNr}/S{seasonNr}E{episodeNr} - {episodeTitle}';
@@ -1160,7 +1215,15 @@ function buildDvdSeriesEpisodeOutputPathPreview(
.filter(Boolean);
const baseName = segments.length > 0 ? segments[segments.length - 1] : 'untitled';
const folderParts = segments.slice(0, -1);
const ext = String(settings?.output_extension_dvd || settings?.output_extension || 'mkv').trim() || 'mkv';
const ext = String(
(normalizedSeriesMediaProfile === 'bluray'
? settings?.output_extension_bluray
: settings?.output_extension_dvd)
|| settings?.output_extension_dvd
|| settings?.output_extension_bluray
|| settings?.output_extension
|| 'mkv'
).trim() || 'mkv';
const root = seriesRootDir.replace(/\/+$/g, '');
if (folderParts.length > 0) {
return `${root}/${folderParts.join('/')}/${baseName}.${ext}`;
@@ -1304,8 +1367,9 @@ export default function PipelineStatusCard({
const reviewMode = String(mediaInfoReview?.mode || '').trim().toLowerCase();
const isPreRipReview = reviewMode === 'pre_rip' || Boolean(mediaInfoReview?.preRip);
const jobMediaProfile = resolvePipelineMediaProfile(pipeline, mediaInfoReview);
const isDvdTitleSelectionRequired = Boolean(
jobMediaProfile === 'dvd'
const discTypeLabel = jobMediaProfile === 'bluray' ? 'Blu-ray' : 'DVD';
const isDiscTitleSelectionRequired = Boolean(
(jobMediaProfile === 'dvd' || jobMediaProfile === 'bluray')
&& (mediaInfoReview?.titleSelectionRequired || mediaInfoReview?.handBrakeTitleDecisionRequired)
);
const [liveJobLog, setLiveJobLog] = useState('');
@@ -1467,8 +1531,21 @@ export default function PipelineStatusCard({
...reviewTitles.filter((title) => Boolean(title?.selectedForEncode)).map((title) => title?.id),
...(fromReview ? [fromReview] : [])
]);
const isSeriesVideoByMetadata = Boolean(
(jobMediaProfile === 'dvd' || jobMediaProfile === 'bluray')
&& (
hasSeriesMetadataShape(selectedMetadata)
|| hasSeriesMetadataShape(mediaInfoReview?.selectedMetadata)
|| hasSeriesEpisodeAssignmentHints(reviewTitles)
|| Boolean(mediaInfoReview?.seriesBatchParent)
)
);
const defaultAllTitles = Boolean(
(mediaInfoReview?.titleSelectionRequired || mediaInfoReview?.handBrakeTitleDecisionRequired)
(
mediaInfoReview?.titleSelectionRequired
|| mediaInfoReview?.handBrakeTitleDecisionRequired
|| (isSeriesVideoByMetadata && reviewTitles.length > 1)
)
&& reviewTitles.length > 0
);
const effectiveSelected = defaultAllTitles
@@ -1537,6 +1614,10 @@ export default function PipelineStatusCard({
mediaInfoReview?.outputFormat,
mediaInfoReview?.userPreset?.id,
mediaInfoReview?.userPreset?.handbrakePreset,
selectedMetadata?.seasonNumber,
selectedMetadata?.episodes,
selectedMetadata?.metadataProvider,
jobMediaProfile,
retryJobId
]);
@@ -1877,16 +1958,6 @@ export default function PipelineStatusCard({
if (!Array.isArray(selectedMetadataEpisodes) || selectedMetadataEpisodes.length === 0) {
return;
}
const hasAnyEpisodeAssignment = Object.values(episodeAssignmentsByTitle || {}).some((assignment) => {
const episodeId = Number(assignment?.episodeId || 0);
const episodeNumber = Number(assignment?.episodeNumber || 0);
const seasonNumber = Number(assignment?.seasonNumber || 0);
const episodeTitle = String(assignment?.episodeTitle || '').trim();
return Boolean(episodeTitle || episodeId > 0 || episodeNumber > 0 || seasonNumber > 0);
});
if (hasAnyEpisodeAssignment) {
return;
}
if (!Array.isArray(episodeFillOptions) || episodeFillOptions.length === 0) {
return;
}
@@ -1903,11 +1974,64 @@ export default function PipelineStatusCard({
}
if (!hasValidSelection) {
setSelectedEpisodeFillStart(String(defaultStartRef));
// Keep dropdown + episodentitel in sync when options change (e.g. Disc 2
// after already used episodes were loaded from the container).
applyEpisodeFillFrom(defaultStartRef);
return;
}
const reviewTitles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
const fallbackSelectedIds = normalizeTitleIdList([
...selectedEncodeTitleIds,
selectedEncodeTitleId,
mediaInfoReview?.encodeInputTitleId,
...reviewTitles.filter((title) => Boolean(title?.selectedForEncode)).map((title) => title?.id)
]);
const selectedTitleIdsInReviewOrder = reviewTitles
.map((title) => normalizeTitleId(title?.id))
.filter((id) => id !== null && fallbackSelectedIds.includes(id));
const effectiveSelectedTitleIds = selectedTitleIdsInReviewOrder.length > 0
? selectedTitleIdsInReviewOrder
: fallbackSelectedIds;
const hasEpisodeAssignmentPayload = (assignment) => {
if (!assignment || typeof assignment !== 'object') {
return false;
}
const episodeId = Number(assignment?.episodeId || assignment?.episodeIdStart || 0);
const episodeNumber = Number(assignment?.episodeNumber || assignment?.episodeNumberStart || 0);
const seasonNumber = Number(assignment?.seasonNumber || 0);
const episodeTitle = String(assignment?.episodeTitle || '').trim();
return Boolean(episodeTitle || episodeId > 0 || episodeNumber > 0 || seasonNumber > 0);
};
const hasAnyEpisodeAssignment = effectiveSelectedTitleIds.some((titleId) => {
const assignment = episodeAssignmentsByTitle?.[titleId]
|| episodeAssignmentsByTitle?.[String(titleId)]
|| null;
return hasEpisodeAssignmentPayload(assignment);
});
const hasCompleteEpisodeAssignments = effectiveSelectedTitleIds.length > 0
&& effectiveSelectedTitleIds.every((titleId) => {
const assignment = episodeAssignmentsByTitle?.[titleId]
|| episodeAssignmentsByTitle?.[String(titleId)]
|| null;
return hasEpisodeAssignmentPayload(assignment);
});
if (hasCompleteEpisodeAssignments) {
return;
}
if (hasAnyEpisodeAssignment && normalizedSelection) {
return;
}
applyEpisodeFillFrom(defaultStartRef);
}, [
selectedMetadataEpisodes,
selectedEncodeTitleIds,
selectedEncodeTitleId,
mediaInfoReview?.titles,
mediaInfoReview?.encodeInputTitleId,
mediaInfoReview?.generatedAt,
episodeAssignmentsByTitle,
episodeFillOptions,
@@ -2247,6 +2371,16 @@ export default function PipelineStatusCard({
pipeline?.context?.selectedPlaylist
]);
const seriesBatchRunningChildJobIdHint = useMemo(() => {
const seriesBatch = pipeline?.context?.seriesBatch;
if (!seriesBatch || typeof seriesBatch !== 'object') {
return null;
}
const children = Array.isArray(seriesBatch?.children) ? seriesBatch.children : [];
const runningChild = children.find((child) => normalizeSeriesBatchStatus(child?.status) === 'RUNNING') || null;
return normalizeJobId(runningChild?.jobId);
}, [pipeline?.context?.seriesBatch]);
useEffect(() => {
if (!running || !retryJobId) {
setLiveJobLog('');
@@ -2257,7 +2391,38 @@ export default function PipelineStatusCard({
try {
const response = await api.getJob(retryJobId, { includeLiveLog: true, lite: true });
if (!cancelled) {
setLiveJobLog(response?.job?.log || '');
const liveJob = response?.job && typeof response.job === 'object'
? response.job
: null;
const childRows = Array.isArray(liveJob?.children) ? liveJob.children : [];
const liveHintChildId = normalizeJobId(seriesBatchRunningChildJobIdHint);
const isSeriesBatchParentRun = Boolean(
liveHintChildId
|| liveJob?.encodePlan?.seriesBatchParent
|| String(liveJob?.handbrakeInfo?.mode || '').trim().toLowerCase() === 'series_batch'
);
if (isSeriesBatchParentRun) {
const activeChildStates = new Set(['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'RUNNING']);
let activeChild = liveHintChildId
? (childRows.find((child) => normalizeJobId(child?.id) === liveHintChildId) || null)
: null;
if (activeChild) {
const hintChildStatus = String(activeChild?.status || activeChild?.last_state || '').trim().toUpperCase();
if (!activeChildStates.has(hintChildStatus)) {
activeChild = null;
}
}
if (!activeChild) {
activeChild = childRows.find((child) => (
activeChildStates.has(String(child?.status || child?.last_state || '').trim().toUpperCase())
)) || null;
}
setLiveJobLog(String(activeChild?.log || '').trim());
return;
}
setLiveJobLog(String(liveJob?.log || '').trim());
}
} catch (_err) {
// ignore transient polling errors
@@ -2269,22 +2434,34 @@ export default function PipelineStatusCard({
cancelled = true;
clearInterval(interval);
};
}, [running, retryJobId]);
}, [running, retryJobId, seriesBatchRunningChildJobIdHint]);
const manualDecisionState = pipeline?.context?.manualDecisionState || null;
const rawDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && manualDecisionState === 'waiting_for_raw_decision';
const playlistDecisionRequiredBeforeStart = state === 'WAITING_FOR_USER_DECISION' && !handBrakeTitleDecisionRequired && !rawDecisionRequiredBeforeStart;
const isSeriesDvdReview = useMemo(() => {
if (jobMediaProfile !== 'dvd') {
if (jobMediaProfile !== 'dvd' && jobMediaProfile !== 'bluray') {
return false;
}
const seasonNumberRaw = selectedMetadata?.seasonNumber ?? null;
const seasonNumber = Number(String(seasonNumberRaw ?? '').replace(',', '.'));
const hasSeasonNumber = Number.isFinite(seasonNumber) && seasonNumber > 0;
const hasEpisodeList = Array.isArray(selectedMetadata?.episodes) && selectedMetadata.episodes.length > 0;
const metadataProvider = String(selectedMetadata?.metadataProvider || '').trim().toLowerCase();
return hasSeasonNumber || hasEpisodeList || metadataProvider === 'tmdb' || metadataProvider === 'themoviedb';
}, [jobMediaProfile, selectedMetadata?.seasonNumber, selectedMetadata?.episodes, selectedMetadata?.metadataProvider]);
return Boolean(
hasSeriesMetadataShape(selectedMetadata)
|| hasSeriesMetadataShape(mediaInfoReview?.selectedMetadata)
|| hasSeriesEpisodeAssignmentHints(mediaInfoReview?.titles)
|| Boolean(mediaInfoReview?.seriesBatchParent)
);
}, [
jobMediaProfile,
selectedMetadata?.seasonNumber,
selectedMetadata?.episodes,
selectedMetadata?.metadataProvider,
selectedMetadata?.workflowKind,
mediaInfoReview?.selectedMetadata?.seasonNumber,
mediaInfoReview?.selectedMetadata?.episodes,
mediaInfoReview?.selectedMetadata?.metadataProvider,
mediaInfoReview?.selectedMetadata?.workflowKind,
mediaInfoReview?.titles,
mediaInfoReview?.seriesBatchParent
]);
useEffect(() => {
let cancelled = false;
@@ -2515,6 +2692,7 @@ export default function PipelineStatusCard({
|| null;
const path = buildDvdSeriesEpisodeOutputPathPreview(
settingsMap,
jobMediaProfile,
selectedMetadata || {},
title,
assignment,
@@ -2533,6 +2711,7 @@ export default function PipelineStatusCard({
mediaInfoReview?.titles,
episodeAssignmentsByTitle,
settingsMap,
jobMediaProfile,
selectedMetadata,
retryJobId,
selectedEncodeTitleIds
@@ -3091,7 +3270,7 @@ export default function PipelineStatusCard({
{handBrakeTitleDecisionRequired && retryJobId && (
<Button
label={isSeriesPlayAllDecisionMode ? 'PlayAll/Doppelfolge bestätigen' : 'DVD Titel bestätigen'}
label={isSeriesPlayAllDecisionMode ? 'PlayAll/Doppelfolge bestätigen' : `${discTypeLabel} Titel bestätigen`}
icon="pi pi-check"
severity="warning"
outlined
@@ -3353,11 +3532,11 @@ export default function PipelineStatusCard({
{handBrakeTitleDecisionRequired && !queueLocked ? (
<div className="playlist-decision-block">
<h3>{isSeriesPlayAllDecisionMode ? 'Serien-Grenzfall: PlayAll oder Doppelfolge?' : 'DVD Titel-Auswahl erforderlich'}</h3>
<h3>{isSeriesPlayAllDecisionMode ? 'Serien-Grenzfall: PlayAll oder Doppelfolge?' : `${discTypeLabel} Titelauswahl erforderlich`}</h3>
<small>
{isSeriesPlayAllDecisionMode
? 'Min. ein Titel passt zeitlich zur Summe der kürzeren Episoden. Bitte durch Auswahl entscheiden, ob PlayAll Multi-Episode (nicht anhaken) oder Doppelfolge (anhaken).'
: 'Mehrere DVD-Titel gefunden, die die Mindestlänge erfüllen. Bitte einen oder mehrere Episoden-Titel auswählen.'}
: `Mehrere ${discTypeLabel}-Titel gefunden, die die Mindestlänge erfüllen. Bitte einen oder mehrere Episodentitel auswählen.`}
</small>
{visibleWaitingHandBrakeTitleRows.length > 0 ? (
<div className="playlist-decision-list">
@@ -3415,7 +3594,7 @@ export default function PipelineStatusCard({
</div>
) : null}
{selectedMetadata && !isDvdTitleSelectionRequired ? (
{selectedMetadata && !isDiscTitleSelectionRequired ? (
<div className="pipeline-meta-inline">
{selectedMetadata.poster ? (
<img
@@ -3445,8 +3624,8 @@ export default function PipelineStatusCard({
{(state === 'READY_TO_ENCODE' || state === 'MEDIAINFO_CHECK' || mediaInfoReview) && !(handBrakeTitleDecisionRequired && isSeriesPlayAllDecisionMode) ? (
<div className="mediainfo-review-block">
{!isDvdTitleSelectionRequired ? <h3>Titel-/Spurprüfung</h3> : null}
{state === 'READY_TO_ENCODE' && !queueLocked && !isDvdTitleSelectionRequired ? (
{!isDiscTitleSelectionRequired ? <h3>Titel-/Spurprüfung</h3> : null}
{state === 'READY_TO_ENCODE' && !queueLocked && !isDiscTitleSelectionRequired ? (
<small>
{isPreRipReview
? 'Spurauswahl kann direkt übernommen werden. Beim Klick auf "Backup + Encoding starten" wird automatisch bestätigt und gestartet.'
@@ -3543,7 +3722,7 @@ export default function PipelineStatusCard({
<div className="playlist-decision-block">
<h3>Hinweis Episoden-Zuordnung</h3>
<small>
Im Job wurden Hinweise zur Episoden-Zuordnung erkannt. Bitte Episodentitel und Episodenbereich pruefen, bevor das Encoding startet.
Im Job wurden Hinweise zur Episoden-Zuordnung erkannt. Bitte Episodentitel und Episodenbereich prüfen, bevor das Encoding startet.
</small>
{episodeAssignmentWarningLines.map((line, index) => (
<small key={`episode-assignment-warning-${index}`}>{line}</small>
@@ -3558,7 +3737,7 @@ export default function PipelineStatusCard({
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
selectedEncodeTitleIds={normalizeTitleIdList(selectedEncodeTitleIds)}
allowTitleSelection={(state === 'READY_TO_ENCODE' || state === 'WAITING_FOR_USER_DECISION') && !queueLocked}
compactTitleSelection={isDvdTitleSelectionRequired}
compactTitleSelection={isDiscTitleSelectionRequired}
onSelectEncodeTitle={(titleId) => {
const normalizedTitleId = normalizeTitleId(titleId);
setSelectedEncodeTitleId(normalizedTitleId);
+45 -5
View File
@@ -32,16 +32,30 @@ export default function DatabasePage() {
const [orphanLoading, setOrphanLoading] = useState(false);
const [orphanImportBusyPath, setOrphanImportBusyPath] = useState(null);
const toastRef = useRef(null);
const orphanLoadRequestRef = useRef(0);
const loadOrphans = async () => {
setOrphanLoading(true);
const loadOrphans = async (options = {}) => {
const requestId = orphanLoadRequestRef.current + 1;
orphanLoadRequestRef.current = requestId;
const silent = Boolean(options?.silent);
if (!silent) {
setOrphanLoading(true);
}
try {
const response = await api.getOrphanRawFolders();
setOrphanRows(response.rows || []);
if (orphanLoadRequestRef.current !== requestId) {
return;
}
setOrphanRows(Array.isArray(response?.rows) ? response.rows : []);
} catch (error) {
if (orphanLoadRequestRef.current !== requestId) {
return;
}
toastRef.current?.show({ severity: 'error', summary: 'RAW-Prüfung fehlgeschlagen', detail: error.message });
} finally {
setOrphanLoading(false);
if (orphanLoadRequestRef.current === requestId && !silent) {
setOrphanLoading(false);
}
}
};
@@ -66,6 +80,14 @@ export default function DatabasePage() {
const response = await api.importOrphanRawFolder(row.rawPath);
const newJobId = response?.job?.id;
const activationStarted = Boolean(response?.activation?.started);
const activationError = String(response?.activationError || '').trim();
const importedRawPath = String(row?.rawPath || '').trim();
if (importedRawPath) {
// Immediate UX feedback: imported orphan should disappear without a full page reload.
setOrphanRows((previousRows) => (
(Array.isArray(previousRows) ? previousRows : []).filter((entry) => String(entry?.rawPath || '').trim() !== importedRawPath)
));
}
toastRef.current?.show({
severity: 'success',
summary: activationStarted ? 'Analyse gestartet' : 'Job angelegt',
@@ -76,9 +98,27 @@ export default function DatabasePage() {
: (newJobId ? `Historieneintrag #${newJobId} erstellt.` : 'Historieneintrag wurde erstellt.'),
life: 3500
});
if (activationError) {
toastRef.current?.show({
severity: 'warn',
summary: 'Analyse-Start fehlgeschlagen',
detail: activationError,
life: 5200
});
}
// Some workflows update job linkage milliseconds later (e.g. follow-up analyze start).
// Recheck a few times in background to converge UI without manual refresh.
const refreshDelaysMs = [0, 600, 1800];
for (const delayMs of refreshDelaysMs) {
if (delayMs > 0) {
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
}
await loadOrphans({ silent: true });
}
// Jump to the Ripper view immediately after creating a job from /database.
navigate('/ripper');
await loadOrphans();
} catch (error) {
toastRef.current?.show({
severity: 'error',
+28 -13
View File
@@ -23,11 +23,12 @@ import {
STATUS_FILTER_OPTIONS
} from '../utils/statusPresentation';
import { confirmModal } from '../utils/confirmModal';
import { isSeriesDvdJob } from '../utils/jobTaxonomy';
import { isSeriesVideoJob } from '../utils/jobTaxonomy';
const MEDIA_FILTER_OPTIONS = [
{ label: 'Alle Medien', value: '' },
{ label: 'Blu-ray', value: 'bluray' },
{ label: 'Blu-ray Serie', value: 'bluray_series' },
{ label: 'DVD', value: 'dvd' },
{ label: 'DVD Serie', value: 'dvd_series' },
{ label: 'Audio CD', value: 'cd' },
@@ -125,13 +126,13 @@ function resolveMediaType(row) {
function resolveMediaTypeMeta(row) {
const mediaType = resolveMediaType(row);
const isSeriesDvd = mediaType === 'dvd' && isSeriesDvdJob(row);
const isSeriesDvd = (mediaType === 'dvd' || mediaType === 'bluray') && isSeriesVideoJob(row);
if (mediaType === 'bluray') {
return {
mediaType,
icon: blurayIndicatorIcon,
label: 'Blu-ray',
alt: 'Blu-ray'
label: isSeriesDvd ? 'Blu-ray Serie' : 'Blu-ray',
alt: isSeriesDvd ? 'Blu-ray Serie' : 'Blu-ray'
};
}
if (mediaType === 'dvd') {
@@ -495,8 +496,11 @@ export default function HistoryPage({ refreshToken = 0 }) {
const visibleJobs = useMemo(
() => (mediumFilter
? preparedJobs.filter((job) => {
if (mediumFilter === 'bluray_series') {
return job.sortMediaType === 'bluray' && isSeriesVideoJob(job);
}
if (mediumFilter === 'dvd_series') {
return job.sortMediaType === 'dvd' && isSeriesDvdJob(job);
return job.sortMediaType === 'dvd' && isSeriesVideoJob(job);
}
return job.sortMediaType === mediumFilter;
})
@@ -895,6 +899,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
// File deletion
const handleDeleteFiles = async (row, target) => {
const includeRelated = isSeriesVideoJob(row);
if (target === 'movie') {
const outputFolders = await loadOutputFoldersForJob(row);
if (outputFolders.length > 1) {
@@ -907,9 +913,12 @@ export default function HistoryPage({ refreshToken = 0 }) {
const outputShortLabel = getOutputShortLabelForRow(row);
const label = target === 'raw' ? 'RAW-Dateien' : target === 'movie' ? outputLabel : `RAW + ${outputShortLabel}`;
const title = row.title || row.detected_title || `Job #${row.id}`;
const scopeSuffix = includeRelated
? '\nScope: kompletter Serien-Container (alle zugehörigen RAWs/Outputs).'
: '';
const confirmed = await confirmModal({
header: 'Dateien löschen',
message: `${label} für "${title}" wirklich löschen?`,
message: `${label} für "${title}" wirklich löschen?${scopeSuffix}`,
acceptLabel: 'Löschen',
rejectLabel: 'Abbrechen',
danger: true
@@ -920,7 +929,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
setActionBusy(true);
try {
const response = await api.deleteJobFiles(row.id, target);
const response = await api.deleteJobFiles(row.id, target, { includeRelated });
const summary = response.summary || {};
const rawSummary = summary.raw || {};
const movieSummary = summary.movie || {};
@@ -1120,7 +1129,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
const workflowKind = ['film', 'movie', 'feature'].includes(workflowKindRaw)
? 'film'
: (['series', 'tv', 'season', 'episode'].includes(workflowKindRaw) ? 'series' : null);
const isSeriesDvd = resolveMediaType(row) === 'dvd' && isSeriesDvdJob(row);
const rowMediaType = resolveMediaType(row);
const isSeriesDvd = (rowMediaType === 'dvd' || rowMediaType === 'bluray') && isSeriesVideoJob(row);
const metadataProvider = workflowKind === 'series'
? 'tmdb'
: (workflowKind === 'film'
@@ -1670,7 +1680,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
const selectedMetadata = row?.makemkvInfo?.selectedMetadata && typeof row.makemkvInfo.selectedMetadata === 'object'
? row.makemkvInfo.selectedMetadata
: {};
const isSeriesDvd = resolveMediaType(row) === 'dvd' && isSeriesDvdJob(row);
const rowMediaType = resolveMediaType(row);
const isSeriesDvd = (rowMediaType === 'dvd' || rowMediaType === 'bluray') && isSeriesVideoJob(row);
const seasonLabel = isSeriesDvd && selectedMetadata?.seasonNumber
? `Staffel ${selectedMetadata.seasonNumber}`
: null;
@@ -1751,7 +1762,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
const selectedMetadata = row?.makemkvInfo?.selectedMetadata && typeof row.makemkvInfo.selectedMetadata === 'object'
? row.makemkvInfo.selectedMetadata
: {};
const isSeriesDvd = resolveMediaType(row) === 'dvd' && isSeriesDvdJob(row);
const rowMediaType = resolveMediaType(row);
const isSeriesDvd = (rowMediaType === 'dvd' || rowMediaType === 'bluray') && isSeriesVideoJob(row);
const seasonLabel = isSeriesDvd && selectedMetadata?.seasonNumber
? `Staffel ${selectedMetadata.seasonNumber}`
: null;
@@ -1962,6 +1974,9 @@ export default function HistoryPage({ refreshToken = 0 }) {
<p>
{`Es werden ${previewRelatedJobs.length || 1} Historien-Eintrag/Einträge entfernt.`}
</p>
<small className="history-dv-subtle">
Hinweis: In diesem Dialog löschen alle ersten drei Buttons immer den Historien-Eintrag plus ausgewählte Dateien.
</small>
{deleteEntryDialogRow ? (
<small className="muted-inline">
@@ -2048,7 +2063,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
<div className="dialog-actions">
<Button
label="Nur RAW löschen"
label="Eintrag + nur RAW-Dateien"
icon="pi pi-trash"
severity="warning"
outlined
@@ -2057,7 +2072,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
disabled={deleteTargetActionsDisabled}
/>
<Button
label={`Nur ${deleteEntryOutputShortLabel} löschen`}
label={`Eintrag + nur ${deleteEntryOutputShortLabel}`}
icon="pi pi-trash"
severity="warning"
outlined
@@ -2066,7 +2081,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
disabled={deleteTargetActionsDisabled || movieDeleteSelectionRequired}
/>
<Button
label="Beides löschen"
label={`Eintrag + RAW & ${deleteEntryOutputShortLabel}`}
icon="pi pi-times"
severity="danger"
onClick={() => confirmDeleteEntry('both')}
+84 -17
View File
@@ -21,7 +21,7 @@ import otherIndicatorIcon from '../assets/media-other.svg';
import audiobookIndicatorIcon from '../assets/media-audiobook.svg';
import { getStatusLabel, getStatusSeverity, normalizeStatus } from '../utils/statusPresentation';
import { confirmModal } from '../utils/confirmModal';
import { isConverterJob, isSeriesDvdJob, resolveMediaType as resolveCentralMediaType } from '../utils/jobTaxonomy';
import { isConverterJob, isSeriesVideoJob, resolveMediaType as resolveCentralMediaType } from '../utils/jobTaxonomy';
const processingStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING'];
const driveActiveStates = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'CD_ANALYZING', 'CD_RIPPING'];
@@ -78,6 +78,32 @@ function isIncompleteRawPath(rawPath) {
return /^incomplete_/i.test(baseName);
}
function normalizePathForMatch(value) {
return String(value || '').trim().replace(/[\\]+/g, '/');
}
function isIncompleteOutputPath(outputPath) {
const normalized = normalizePathForMatch(outputPath);
if (!normalized) {
return false;
}
return /(^|\/)incomplete_job-\d+(\/|$)/i.test(normalized);
}
function getIncompleteOutputSelectionPaths(outputPath) {
const normalized = normalizePathForMatch(outputPath);
if (!normalized) {
return [];
}
const segments = normalized.split('/').filter(Boolean);
const incompleteIndex = segments.findIndex((segment) => /^incomplete_job-\d+$/i.test(segment));
if (incompleteIndex < 0) {
return [];
}
const folderPath = `/${segments.slice(0, incompleteIndex + 1).join('/')}`;
return Array.from(new Set([normalized, folderPath]));
}
function formatPercent(value, digits = 1) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
@@ -557,17 +583,25 @@ function resolveMediaType(job) {
function mediaIndicatorMeta(job) {
const mediaType = resolveMediaType(job);
const isSeriesDvd = mediaType === 'dvd' && isSeriesDvdJob(job);
const isSeriesVideo = (mediaType === 'dvd' || mediaType === 'bluray') && isSeriesVideoJob(job);
if (mediaType === 'bluray') {
return { mediaType, src: blurayIndicatorIcon, alt: 'Blu-ray', title: 'Blu-ray' };
return {
mediaType,
src: blurayIndicatorIcon,
alt: isSeriesVideo ? 'Blu-ray Serie' : 'Blu-ray',
title: isSeriesVideo ? 'Blu-ray Serie' : 'Blu-ray',
isSeriesDvd: isSeriesVideo,
isSeriesVideo
};
}
if (mediaType === 'dvd') {
return {
mediaType,
src: discIndicatorIcon,
alt: isSeriesDvd ? 'DVD Serie' : 'DVD',
title: isSeriesDvd ? 'DVD Serie' : 'DVD',
isSeriesDvd
alt: isSeriesVideo ? 'DVD Serie' : 'DVD',
title: isSeriesVideo ? 'DVD Serie' : 'DVD',
isSeriesDvd: isSeriesVideo,
isSeriesVideo
};
}
if (mediaType === 'cd') {
@@ -1669,6 +1703,9 @@ export default function RipperPage({
const jobId = normalizeJobId(cancelCleanupDialog?.jobId);
const target = String(cancelCleanupDialog?.target || '').trim().toLowerCase();
const effectiveTarget = target === 'raw' ? 'raw' : 'movie';
const jobRow = (Array.isArray(ripperJobs) ? ripperJobs : [])
.find((row) => normalizeJobId(row?.id) === jobId) || null;
const includeRelated = isSeriesVideoJob(jobRow);
if (!jobId) {
setCancelCleanupDialog({ visible: false, jobId: null, target: null, path: null });
return;
@@ -1676,7 +1713,7 @@ export default function RipperPage({
setCancelCleanupBusy(true);
try {
const response = await api.deleteJobFiles(jobId, effectiveTarget);
const response = await api.deleteJobFiles(jobId, effectiveTarget, { includeRelated });
const summary = response?.summary || {};
const targetSummary = effectiveTarget === 'raw'
? (summary.raw || {})
@@ -1956,20 +1993,48 @@ export default function RipperPage({
const isOrphanRawImportJob = orphanImportSource === 'orphan_raw_import';
const ripSuccessful = Number(job?.rip_successful || 0) === 1;
const deleteIncompleteRaw = !isOrphanRawImportJob && !ripSuccessful && isIncompleteRawPath(job?.raw_path);
const deleteTarget = deleteIncompleteRaw ? 'raw' : 'none';
let incompleteMovieSelectionPaths = [];
try {
const deletePreview = await api.getJobDeletePreview(normalizedJobId, { includeRelated: true });
const movieCandidates = Array.isArray(deletePreview?.preview?.pathCandidates?.movie)
? deletePreview.preview.pathCandidates.movie
: [];
incompleteMovieSelectionPaths = Array.from(new Set(
movieCandidates
.filter((candidate) => Boolean(candidate?.exists) && isIncompleteOutputPath(candidate?.path))
.map((candidate) => String(candidate?.path || '').trim())
.filter(Boolean)
));
} catch (_error) {
incompleteMovieSelectionPaths = [];
}
if (incompleteMovieSelectionPaths.length === 0) {
incompleteMovieSelectionPaths = getIncompleteOutputSelectionPaths(job?.output_path);
}
const deleteIncompleteMovie = incompleteMovieSelectionPaths.length > 0;
const deleteTarget = deleteIncompleteRaw && deleteIncompleteMovie
? 'both'
: (deleteIncompleteRaw
? 'raw'
: (deleteIncompleteMovie ? 'movie' : 'none'));
const resetDriveStateOnDelete = !isOrphanRawImportJob;
const keepDetectedDeviceOnDelete = isOrphanRawImportJob;
const title = String(job?.title || job?.detected_title || `Job #${normalizedJobId}`).trim();
const deleteHint = deleteIncompleteRaw && deleteIncompleteMovie
? 'Hinweis: Unvollständiges RAW und temporärer Incomplete-Output werden gelöscht.'
: (deleteIncompleteRaw
? 'Hinweis: Unvollständiges RAW wird gelöscht. Für einen neuen Versuch bitte das Laufwerk erneut analysieren.'
: (deleteIncompleteMovie
? 'Hinweis: Temporärer Incomplete-Output wird beim Löschen mit entfernt.'
: (isOrphanRawImportJob
? 'Hinweis: Dieser /database-Job löscht beim Entfernen aus dem Ripper keine RAW-Dateien.'
: 'Hinweis: Dateien bleiben erhalten. Vollständiges RAW ist anschließend über /database wiederherstellbar.')));
const confirmed = await confirmModal({
header: 'Job löschen',
message:
`Job #${normalizedJobId} wirklich löschen?\n` +
`${title}\n\n` +
(deleteIncompleteRaw
? 'Hinweis: Unvollständiges RAW wird gelöscht. Für einen neuen Versuch bitte das Laufwerk erneut analysieren.'
: (isOrphanRawImportJob
? 'Hinweis: Dieser /database-Job löscht beim Entfernen aus dem Ripper keine RAW-Dateien.'
: 'Hinweis: Dateien bleiben erhalten. Vollständiges RAW ist anschließend über /database wiederherstellbar.')) +
deleteHint +
'\nZugehörige Einträge (z.B. Serien-Container oder Folgejobs) werden ebenfalls entfernt.' +
(isOrphanRawImportJob
? '\nOrphan-Import-Job: Das zugeordnete Laufwerk bleibt unverändert.'
@@ -1999,6 +2064,7 @@ export default function RipperPage({
}
await api.deleteJobEntry(normalizedJobId, deleteTarget, {
includeRelated: true,
...(deleteIncompleteMovie ? { selectedMoviePaths: incompleteMovieSelectionPaths } : {}),
resetDriveState: resetDriveStateOnDelete,
keepDetectedDevice: keepDetectedDeviceOnDelete,
preserveRawForImportJobs: true
@@ -2291,14 +2357,14 @@ export default function RipperPage({
const handleMetadataSubmit = async (payload) => {
const payloadProvider = String(payload?.metadataProvider || '').trim().toLowerCase();
const currentJobMediaProfile = String(effectiveMetadataDialogContext?.mediaProfile || '').trim().toLowerCase();
const isSeriesDvdPayload = payloadProvider === 'tmdb' && currentJobMediaProfile === 'dvd';
const isSeriesDvdPayload = payloadProvider === 'tmdb' && (currentJobMediaProfile === 'dvd' || currentJobMediaProfile === 'bluray');
if (metadataDialogReassignMode) {
await doSelectMetadata(payload);
return;
}
// Serien-DVDs laufen ohne Film-Duplikatdialog weiter; die Disk-Pruefung
// Serien-Discs laufen ohne Film-Duplikatdialog weiter; die Disk-Pruefung
// erfolgt serverseitig gegen den vorhandenen Staffel-Container.
if (isSeriesDvdPayload) {
await doSelectMetadata(payload, { suppressErrorToast: true });
@@ -2921,6 +2987,7 @@ export default function RipperPage({
const pipelineForJob = pipelineByJobId.get(jobId) || pipeline;
const jobTitle = job?.title || job?.detected_title || `Job #${jobId}`;
const mediaIndicator = mediaIndicatorMeta(job);
const seriesTagLabel = mediaIndicator.mediaType === 'bluray' ? 'Blu-ray Serie' : 'DVD Serie';
const isResumable = (
normalizedStatus === 'READY_TO_ENCODE'
|| (mediaIndicator.mediaType === 'audiobook' && normalizedStatus === 'READY_TO_START')
@@ -3041,7 +3108,7 @@ export default function RipperPage({
) : null}
<div className="ripper-job-badges">
<Tag value={statusBadgeValue} severity={statusBadgeSeverity} />
{mediaIndicator.isSeriesDvd ? <Tag value="DVD Serie" severity="secondary" /> : null}
{mediaIndicator.isSeriesDvd ? <Tag value={seriesTagLabel} severity="secondary" /> : null}
{isCurrentSession ? <Tag value="Aktive Session" severity="info" /> : null}
{isResumable ? <Tag value="Fortsetzbar" severity="success" /> : null}
{normalizedStatus === 'READY_TO_ENCODE'
@@ -3185,7 +3252,7 @@ export default function RipperPage({
</div>
<div className="ripper-job-badges">
<Tag value={statusBadgeValue} severity={statusBadgeSeverity} />
{mediaIndicator.isSeriesDvd ? <Tag value="DVD Serie" severity="secondary" /> : null}
{mediaIndicator.isSeriesDvd ? <Tag value={seriesTagLabel} severity="secondary" /> : null}
{isCurrentSession ? <Tag value="Aktive Session" severity="info" /> : null}
{isResumable ? <Tag value="Fortsetzbar" severity="success" /> : null}
{normalizedStatus === 'READY_TO_ENCODE'
+7 -2
View File
@@ -204,8 +204,9 @@ export function resolveMediaType(job) {
return 'other';
}
export function isSeriesDvdJob(job) {
if (resolveMediaType(job) !== 'dvd') {
export function isSeriesVideoJob(job) {
const mediaType = resolveMediaType(job);
if (mediaType !== 'dvd' && mediaType !== 'bluray') {
return false;
}
@@ -265,6 +266,10 @@ export function isSeriesDvdJob(job) {
return (metadataProvider === 'tmdb' || metadataProvider === 'themoviedb') && hasTmdbId;
}
export function isSeriesDvdJob(job) {
return isSeriesVideoJob(job);
}
export function isConverterJob(job) {
return resolveMediaType(job) === 'converter';
}