1.0.0-rc4 rc4
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "1.0.0-rc3",
|
||||
"version": "1.0.0-rc4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-frontend",
|
||||
"version": "1.0.0-rc3",
|
||||
"version": "1.0.0-rc4",
|
||||
"dependencies": {
|
||||
"chart.js": "^4.5.1",
|
||||
"primeicons": "^7.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "1.0.0-rc3",
|
||||
"version": "1.0.0-rc4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+23
-2
@@ -65,6 +65,14 @@ function isTerminalStage(value) {
|
||||
return normalized === 'FINISHED' || normalized === 'ERROR' || normalized === 'CANCELLED';
|
||||
}
|
||||
|
||||
function isInteractiveReviewStage(value) {
|
||||
const normalized = normalizeStage(value);
|
||||
return normalized === 'METADATA_LOOKUP'
|
||||
|| normalized === 'METADATA_SELECTION'
|
||||
|| normalized === 'WAITING_FOR_USER_DECISION'
|
||||
|| normalized === 'READY_TO_ENCODE';
|
||||
}
|
||||
|
||||
function parseTimestamp(value) {
|
||||
if (!value) {
|
||||
return 0;
|
||||
@@ -209,8 +217,9 @@ function App() {
|
||||
prevCdDrivesRef.current = current;
|
||||
}, [pipeline?.cdDrives]);
|
||||
|
||||
// Trigger a jobs refresh when a tracked job transitions into a terminal
|
||||
// state through PIPELINE_PROGRESS, so Ripper/History update immediately.
|
||||
// Trigger a jobs refresh when a tracked job transitions into a terminal or
|
||||
// interactive review state through PIPELINE_PROGRESS, so Ripper/History
|
||||
// update immediately without a manual page reload.
|
||||
useEffect(() => {
|
||||
const current = pipeline?.jobProgress && typeof pipeline.jobProgress === 'object'
|
||||
? pipeline.jobProgress
|
||||
@@ -233,6 +242,13 @@ function App() {
|
||||
) {
|
||||
shouldRefresh = true;
|
||||
}
|
||||
if (
|
||||
currentState
|
||||
&& isInteractiveReviewStage(currentState)
|
||||
&& currentState !== previousState
|
||||
) {
|
||||
shouldRefresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
prevJobProgressStatesRef.current = nextStates;
|
||||
@@ -394,6 +410,11 @@ function App() {
|
||||
|
||||
if (message.type === 'PIPELINE_STATE_CHANGED') {
|
||||
setPipeline((prev) => mergePipelineStatePreservingNewestQueue(prev, message.payload));
|
||||
const nextState = normalizeStage(message?.payload?.state);
|
||||
if (isTerminalStage(nextState) || isInteractiveReviewStage(nextState)) {
|
||||
setRipperJobsRefreshToken((token) => token + 1);
|
||||
setHistoryJobsRefreshToken((token) => token + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === 'PIPELINE_PROGRESS') {
|
||||
|
||||
@@ -388,6 +388,13 @@ export const api = {
|
||||
forceRefresh: options.forceRefresh
|
||||
});
|
||||
},
|
||||
async checkMakeMKVBetaKey() {
|
||||
const result = await request('/settings/makemkv/beta-key/check', {
|
||||
method: 'POST'
|
||||
});
|
||||
afterMutationInvalidate(['/settings/makemkv/beta-key']);
|
||||
return result;
|
||||
},
|
||||
async applyMakeMKVBetaKey(payload = {}) {
|
||||
const result = await request('/settings/makemkv/beta-key/apply', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -665,63 +665,146 @@ function isReadonlySetting(setting) {
|
||||
}
|
||||
}
|
||||
|
||||
function MakeMKVBetaKeyHint({ onApply, disabled = false }) {
|
||||
function MakeMKVBetaKeyHint({ disabled = false, onNotify = null, onSettingApplied = null }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [betaKey, setBetaKey] = useState('');
|
||||
const [validUntil, setValidUntil] = useState('');
|
||||
const [fetchedAt, setFetchedAt] = useState('');
|
||||
const [appliedKey, setAppliedKey] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [warning, setWarning] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const loadCachedState = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setWarning('');
|
||||
setValidUntil('');
|
||||
try {
|
||||
const response = await api.getMakeMKVBetaKey();
|
||||
setBetaKey(String(response?.betaKey || '').trim());
|
||||
setValidUntil(String(response?.validUntil || '').trim());
|
||||
setFetchedAt(String(response?.fetchedAt || '').trim());
|
||||
setAppliedKey(String(response?.appliedKey || '').trim());
|
||||
if (response?.stale) {
|
||||
setWarning('Zwischengespeicherter Betakey ist veraltet. Bitte bei Bedarf neu prüfen.');
|
||||
} else if (!response?.betaKey) {
|
||||
setWarning('Noch kein geprüfter Betakey im Cache. Prüfung startet nur per Button.');
|
||||
} else if (response?.appliedMatchesCache) {
|
||||
setWarning('Geprüfter Betakey ist bereits übernommen.');
|
||||
} else {
|
||||
setWarning('');
|
||||
}
|
||||
} catch (loadError) {
|
||||
setBetaKey('');
|
||||
setValidUntil('');
|
||||
setFetchedAt('');
|
||||
setAppliedKey('');
|
||||
setError(loadError?.message || 'Betakey konnte nicht geladen werden.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
api.getMakeMKVBetaKey()
|
||||
.then((response) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setBetaKey(String(response?.betaKey || '').trim());
|
||||
setValidUntil(String(response?.validUntil || '').trim());
|
||||
setWarning(String(response?.warning || '').trim());
|
||||
})
|
||||
.catch((loadError) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setBetaKey('');
|
||||
setValidUntil('');
|
||||
setError(loadError?.message || 'Betakey konnte nicht geladen werden.');
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
useEffect(() => {
|
||||
void loadCachedState();
|
||||
}, []);
|
||||
|
||||
const handleCheck = async () => {
|
||||
setChecking(true);
|
||||
setError('');
|
||||
setWarning('');
|
||||
try {
|
||||
const response = await api.checkMakeMKVBetaKey();
|
||||
const nextKey = String(response?.betaKey || '').trim();
|
||||
setBetaKey(nextKey);
|
||||
setValidUntil(String(response?.validUntil || '').trim());
|
||||
setFetchedAt(String(response?.fetchedAt || '').trim());
|
||||
setWarning('');
|
||||
if (typeof onNotify === 'function') {
|
||||
onNotify({
|
||||
severity: 'success',
|
||||
summary: 'MakeMKV Betakey',
|
||||
detail: nextKey ? 'Betakey erfolgreich geprüft.' : 'Betakey-Prüfung abgeschlossen.'
|
||||
});
|
||||
}
|
||||
} catch (checkError) {
|
||||
setError(checkError?.message || 'Betakey konnte nicht geprüft werden.');
|
||||
if (typeof onNotify === 'function') {
|
||||
onNotify({
|
||||
severity: 'error',
|
||||
summary: 'MakeMKV Betakey',
|
||||
detail: checkError?.message || 'Betakey konnte nicht geprüft werden.'
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApply = async () => {
|
||||
if (!betaKey) {
|
||||
return;
|
||||
}
|
||||
setApplying(true);
|
||||
setError('');
|
||||
try {
|
||||
const response = await api.applyMakeMKVBetaKey({ betaKey });
|
||||
const nextAppliedKey = String(response?.betaKey || betaKey).trim();
|
||||
setAppliedKey(nextAppliedKey);
|
||||
setWarning('Geprüfter Betakey ist bereits übernommen.');
|
||||
if (typeof onSettingApplied === 'function') {
|
||||
onSettingApplied({
|
||||
key: 'makemkv_registration_key',
|
||||
value: nextAppliedKey
|
||||
});
|
||||
}
|
||||
if (typeof onNotify === 'function') {
|
||||
onNotify({
|
||||
severity: 'success',
|
||||
summary: 'MakeMKV Betakey',
|
||||
detail: 'Betakey wurde übernommen und gespeichert.'
|
||||
});
|
||||
}
|
||||
} catch (applyError) {
|
||||
setError(applyError?.message || 'Betakey konnte nicht übernommen werden.');
|
||||
if (typeof onNotify === 'function') {
|
||||
onNotify({
|
||||
severity: 'error',
|
||||
summary: 'MakeMKV Betakey',
|
||||
detail: applyError?.message || 'Betakey konnte nicht übernommen werden.'
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="makemkv-beta-key-box">
|
||||
<div className="makemkv-beta-key-head">
|
||||
<span className="makemkv-beta-key-title">Betakey</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
text
|
||||
label="übernehmen"
|
||||
disabled={disabled || loading || !betaKey}
|
||||
onClick={() => onApply?.(betaKey)}
|
||||
/>
|
||||
<div className="settings-inline-actions">
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
text
|
||||
label="prüfen"
|
||||
disabled={disabled || loading || checking || applying}
|
||||
onClick={() => void handleCheck()}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
text
|
||||
label="übernehmen"
|
||||
disabled={disabled || loading || checking || applying || !betaKey || betaKey === appliedKey}
|
||||
onClick={() => void handleApply()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<small className="setting-description">Aktueller Betakey wird geladen…</small>
|
||||
<small className="setting-description">Zwischengespeicherter Betakey wird geladen…</small>
|
||||
) : null}
|
||||
{!loading && error ? (
|
||||
<small className="error-text">{error}</small>
|
||||
@@ -729,6 +812,9 @@ function MakeMKVBetaKeyHint({ onApply, disabled = false }) {
|
||||
{!loading && !error && warning ? (
|
||||
<small className="setting-description">{warning}</small>
|
||||
) : null}
|
||||
{!loading && !error && fetchedAt ? (
|
||||
<small className="setting-description">Zuletzt geprüft: {new Date(fetchedAt).toLocaleString('de-DE')}</small>
|
||||
) : null}
|
||||
{!loading && !error && validUntil ? (
|
||||
<small className="setting-description">Gültig bis: {new Date(validUntil).toLocaleString('de-DE')}</small>
|
||||
) : null}
|
||||
@@ -786,7 +872,9 @@ function SettingField({
|
||||
ownerDirty,
|
||||
onChange,
|
||||
variant = 'default',
|
||||
disabled = false
|
||||
disabled = false,
|
||||
onNotify = null,
|
||||
onSettingApplied = null
|
||||
}) {
|
||||
const ownerKey = ownerSetting?.key;
|
||||
const pathHasValue = Boolean(String(value ?? '').trim());
|
||||
@@ -905,7 +993,8 @@ function SettingField({
|
||||
{normalizeSettingKey(setting?.key) === 'makemkv_registration_key' ? (
|
||||
<MakeMKVBetaKeyHint
|
||||
disabled={isDisabled}
|
||||
onApply={(nextKey) => onChange?.(setting.key, nextKey)}
|
||||
onNotify={onNotify}
|
||||
onSettingApplied={onSettingApplied}
|
||||
/>
|
||||
) : null}
|
||||
{error ? (
|
||||
@@ -946,7 +1035,16 @@ function SettingField({
|
||||
}
|
||||
|
||||
|
||||
function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effectivePaths }) {
|
||||
function PathCategoryTab({
|
||||
settings,
|
||||
values,
|
||||
errors,
|
||||
dirtyKeys,
|
||||
onChange,
|
||||
effectivePaths,
|
||||
onNotify,
|
||||
onSettingApplied
|
||||
}) {
|
||||
const list = Array.isArray(settings) ? settings : [];
|
||||
const settingsByKey = new Map(list.map((s) => [s.key, s]));
|
||||
|
||||
@@ -1153,6 +1251,8 @@ function PathCategoryTab({ settings, values, errors, dirtyKeys, onChange, effect
|
||||
ownerError={ownerError}
|
||||
ownerDirty={ownerDirty}
|
||||
onChange={onChange}
|
||||
onNotify={onNotify}
|
||||
onSettingApplied={onSettingApplied}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -1173,7 +1273,8 @@ export default function DynamicSettingsForm({
|
||||
onChange,
|
||||
effectivePaths,
|
||||
activationBytes = [],
|
||||
onNotify
|
||||
onNotify,
|
||||
onSettingApplied
|
||||
}) {
|
||||
const safeCategories = Array.isArray(categories) ? categories : [];
|
||||
const expertModeEnabled = toBoolean(values?.[EXPERT_MODE_SETTING_KEY]);
|
||||
@@ -1258,6 +1359,8 @@ export default function DynamicSettingsForm({
|
||||
dirtyKeys={dirtyKeys}
|
||||
onChange={onChange}
|
||||
effectivePaths={effectivePaths}
|
||||
onNotify={onNotify}
|
||||
onSettingApplied={onSettingApplied}
|
||||
/>
|
||||
) : (() => {
|
||||
const sections = buildSectionsForCategory(category?.category, category?.settings || []);
|
||||
@@ -1339,6 +1442,8 @@ export default function DynamicSettingsForm({
|
||||
onChange={onChangeFn}
|
||||
variant={variant}
|
||||
disabled={disabled}
|
||||
onNotify={onNotify}
|
||||
onSettingApplied={onSettingApplied}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1588,6 +1588,7 @@ export default function MediaInfoReviewPanel({
|
||||
commandOutputPathByTitle = {},
|
||||
selectedEncodeTitleId = null,
|
||||
selectedEncodeTitleIds = [],
|
||||
displayOnlySelectedTitle = false,
|
||||
titleSelectionTouched = false,
|
||||
allowTitleSelection = false,
|
||||
compactTitleSelection = false,
|
||||
@@ -1670,6 +1671,9 @@ export default function MediaInfoReviewPanel({
|
||||
|| selectedTitleIds[0]
|
||||
|| null;
|
||||
const selectedTitleIdSet = new Set(selectedTitleIds.map((id) => String(id)));
|
||||
const visibleDisplayTitles = displayOnlySelectedTitle && currentSelectedId !== null
|
||||
? displayTitles.filter((title) => normalizeTitleId(title?.id) === currentSelectedId)
|
||||
: displayTitles;
|
||||
const normalizedComparisonReviews = (Array.isArray(comparisonReviewItems) ? comparisonReviewItems : [])
|
||||
.map((item) => {
|
||||
const reviewCandidate = item?.review && typeof item.review === 'object'
|
||||
@@ -2707,12 +2711,12 @@ export default function MediaInfoReviewPanel({
|
||||
</div>
|
||||
) : null}
|
||||
<div className="media-title-list">
|
||||
{displayTitles.length === 0 ? (
|
||||
{visibleDisplayTitles.length === 0 ? (
|
||||
<p>Keine Titel analysiert.</p>
|
||||
) : displayTitles.map((title) => {
|
||||
) : visibleDisplayTitles.map((title) => {
|
||||
const normalizedTitleId = normalizeTitleId(title.id);
|
||||
const displayTitleId = normalizeTitleId(title?.handBrakeTitleId) || normalizedTitleId;
|
||||
const titleChecked = allowTitleSelection
|
||||
const titleChecked = (allowTitleSelection || displayOnlySelectedTitle)
|
||||
? (selectedTitleIdSet.size > 0
|
||||
? selectedTitleIdSet.has(String(normalizedTitleId))
|
||||
: Boolean(title.selectedForEncode))
|
||||
@@ -2799,8 +2803,8 @@ export default function MediaInfoReviewPanel({
|
||||
);
|
||||
const allowTrackSelectionForTitle = Boolean(
|
||||
allowTrackSelection
|
||||
&& allowTitleSelection
|
||||
&& titleChecked
|
||||
&& (allowTitleSelection || displayOnlySelectedTitle)
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -2813,6 +2817,15 @@ export default function MediaInfoReviewPanel({
|
||||
if (!allowTitleSelection) {
|
||||
return;
|
||||
}
|
||||
if (displayOnlySelectedTitle) {
|
||||
if (typeof onToggleEncodeTitle === 'function') {
|
||||
onToggleEncodeTitle(normalizedTitleId, true);
|
||||
}
|
||||
if (typeof onSelectEncodeTitle === 'function') {
|
||||
onSelectEncodeTitle(normalizedTitleId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const checked = Boolean(event?.target?.checked);
|
||||
if (typeof onToggleEncodeTitle === 'function') {
|
||||
onToggleEncodeTitle(normalizedTitleId, checked);
|
||||
|
||||
@@ -1635,17 +1635,29 @@ 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 hasReviewRecoverableRaw = Boolean(
|
||||
!['converter', 'audiobook'].includes(String(jobMediaProfile || '').trim().toLowerCase())
|
||||
&& ripCompleted
|
||||
&& (
|
||||
jobRow?.raw_path
|
||||
|| pipeline?.context?.rawPath
|
||||
|| jobRow?.makemkvInfo?.rawPath
|
||||
)
|
||||
);
|
||||
const retryRipRecoveryRequired = Boolean(
|
||||
!running
|
||||
&& retryJobId
|
||||
&& typeof onRetry === 'function'
|
||||
&& !queueLocked
|
||||
&& !isOrphanCancelled
|
||||
&& !['converter', 'audiobook'].includes(String(jobMediaProfile || '').trim().toLowerCase())
|
||||
&& !hasReviewRecoverableRaw
|
||||
&& ['ERROR', 'CANCELLED'].includes(jobStatusUpper)
|
||||
&& (
|
||||
['RIPPING', 'CD_RIPPING'].includes(jobLastStateUpper)
|
||||
|| jobErrorMessageLower.includes('server-neustart')
|
||||
|| (
|
||||
jobErrorMessageLower.includes('server-neustart')
|
||||
&& !hasReviewRecoverableRaw
|
||||
)
|
||||
|| jobErrorMessageLower.includes('rip ist unvollständig')
|
||||
|| jobErrorMessageLower.includes('rip-validierung fehlgeschlagen')
|
||||
)
|
||||
@@ -2374,6 +2386,15 @@ export default function PipelineStatusCard({
|
||||
);
|
||||
const allowTrackSelectionInReview = state === 'READY_TO_ENCODE' && allowReviewSelectionEdit;
|
||||
const allowEncodeItemSelectionInReview = state === 'READY_TO_ENCODE' && allowReviewSelectionEdit;
|
||||
const alternativeTitleSelection = mediaInfoReview?.alternativeTitleSelection
|
||||
&& typeof mediaInfoReview.alternativeTitleSelection === 'object'
|
||||
? mediaInfoReview.alternativeTitleSelection
|
||||
: null;
|
||||
const hasAlternativeTitleSelection = Boolean(
|
||||
alternativeTitleSelection?.enabled
|
||||
&& Array.isArray(mediaInfoReview?.titles)
|
||||
&& mediaInfoReview.titles.length > 1
|
||||
);
|
||||
|
||||
// Filter user presets by job media profile ('all' presets always shown)
|
||||
const filteredUserPresets = (Array.isArray(userPresets) ? userPresets : []).filter((p) => {
|
||||
@@ -2550,6 +2571,31 @@ export default function PipelineStatusCard({
|
||||
}
|
||||
return dedup;
|
||||
}, [playlistAnalysis, pipeline?.context?.playlistCandidates]);
|
||||
const alternativeTitleOptions = useMemo(() => {
|
||||
if (!hasAlternativeTitleSelection) {
|
||||
return [];
|
||||
}
|
||||
const titles = Array.isArray(mediaInfoReview?.titles) ? mediaInfoReview.titles : [];
|
||||
return titles
|
||||
.map((title) => {
|
||||
const titleId = normalizeTitleId(title?.id);
|
||||
if (!titleId) {
|
||||
return null;
|
||||
}
|
||||
const playlistFile = String(title?.playlistFile || '').trim();
|
||||
const durationLabel = formatDurationClock(title?.durationSeconds) || null;
|
||||
const parts = [
|
||||
playlistFile || `Titel #${titleId}`,
|
||||
durationLabel
|
||||
].filter(Boolean);
|
||||
return {
|
||||
value: titleId,
|
||||
label: parts.join(' | ')
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((left, right) => Number(left.value) - Number(right.value));
|
||||
}, [hasAlternativeTitleSelection, mediaInfoReview?.titles]);
|
||||
|
||||
const handBrakeTitleDecisionRequired = state === 'WAITING_FOR_USER_DECISION'
|
||||
&& Boolean(pipeline?.context?.handBrakeTitleDecisionRequired);
|
||||
@@ -4316,6 +4362,38 @@ export default function PipelineStatusCard({
|
||||
commandOutputPathByTitle={commandOutputPathByTitle}
|
||||
topContent={(
|
||||
<>
|
||||
{hasAlternativeTitleSelection ? (
|
||||
<div className="playlist-decision-block">
|
||||
<h3>Alternative Titel gefunden</h3>
|
||||
<small>
|
||||
Es wurden weitere gleichlange oder längere Titel erkannt. Das kann auf alternative Schnittfassungen
|
||||
oder Playlist-Varianten derselben Disc hindeuten. Bitte waehle hier den Titel aus, der fuer Tracks,
|
||||
Untertitel, Infos und den HandBrakeCLI-Command verwendet werden soll.
|
||||
</small>
|
||||
<div style={{ marginTop: '0.75rem' }}>
|
||||
<label style={{ display: 'block', marginBottom: '0.35rem', fontWeight: 600 }}>
|
||||
Zu verwendender Titel
|
||||
</label>
|
||||
<Dropdown
|
||||
value={normalizeTitleId(selectedEncodeTitleId) || normalizeTitleId(mediaInfoReview?.encodeInputTitleId) || null}
|
||||
options={alternativeTitleOptions}
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
onChange={(e) => {
|
||||
const nextTitleId = normalizeTitleId(e?.value);
|
||||
if (!nextTitleId) {
|
||||
return;
|
||||
}
|
||||
setSelectedEncodeTitleId(nextTitleId);
|
||||
setSelectedEncodeTitleIds([nextTitleId]);
|
||||
setTitleSelectionTouched(true);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
disabled={!allowReviewSelectionEdit || alternativeTitleOptions.length <= 1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{multipartSettingsLocked ? (
|
||||
<div className="playlist-decision-block">
|
||||
<h3>Multipart-Lock aktiv</h3>
|
||||
@@ -4354,6 +4432,7 @@ export default function PipelineStatusCard({
|
||||
)}
|
||||
selectedEncodeTitleId={normalizeTitleId(selectedEncodeTitleId)}
|
||||
selectedEncodeTitleIds={normalizeTitleIdList(selectedEncodeTitleIds)}
|
||||
displayOnlySelectedTitle={hasAlternativeTitleSelection}
|
||||
titleSelectionTouched={titleSelectionTouched}
|
||||
allowTitleSelection={allowTitleSelectionInReview}
|
||||
compactTitleSelection={isDiscTitleSelectionRequired}
|
||||
|
||||
@@ -9,6 +9,21 @@ function buildWsUrl() {
|
||||
return `${protocol}//${window.location.host}/ws`;
|
||||
}
|
||||
|
||||
function normalizeStage(value) {
|
||||
return String(value || '').trim().toUpperCase();
|
||||
}
|
||||
|
||||
function isRunningStage(value) {
|
||||
const normalized = normalizeStage(value);
|
||||
return normalized === 'ANALYZING'
|
||||
|| normalized === 'RIPPING'
|
||||
|| normalized === 'MEDIAINFO_CHECK'
|
||||
|| normalized === 'ENCODING'
|
||||
|| normalized === 'CD_ANALYZING'
|
||||
|| normalized === 'CD_RIPPING'
|
||||
|| normalized === 'CD_ENCODING';
|
||||
}
|
||||
|
||||
export function useWebSocket({ onMessage }) {
|
||||
const onMessageRef = useRef(onMessage);
|
||||
onMessageRef.current = onMessage;
|
||||
@@ -69,10 +84,19 @@ export function useWebSocket({ onMessage }) {
|
||||
if (message?.type === 'PIPELINE_PROGRESS') {
|
||||
const progressJobId = message?.payload?.activeJobId;
|
||||
const jobKey = progressJobId == null ? '__global__' : String(progressJobId);
|
||||
latestProgressMessagesByJob.set(jobKey, message);
|
||||
scheduleProgressFlush();
|
||||
const progressStage = normalizeStage(message?.payload?.state);
|
||||
if (isRunningStage(progressStage)) {
|
||||
latestProgressMessagesByJob.set(jobKey, message);
|
||||
scheduleProgressFlush();
|
||||
} else {
|
||||
latestProgressMessagesByJob.delete(jobKey);
|
||||
clearProgressFlush();
|
||||
flushProgressMessage();
|
||||
onMessageRef.current?.(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
flushProgressMessage();
|
||||
onMessageRef.current?.(message);
|
||||
} catch (error) {
|
||||
// ignore invalid json
|
||||
|
||||
@@ -1614,7 +1614,7 @@ export default function RipperPage({
|
||||
// Detects when a background job reaches an interactive review/selection state via
|
||||
// PIPELINE_PROGRESS (e.g. METADATA_LOOKUP, METADATA_SELECTION, READY_TO_ENCODE, WAITING_FOR_USER_DECISION),
|
||||
// triggering a ripper jobs reload.
|
||||
const jobProgressInteractiveKey = useMemo(() => Object.entries(pipeline?.jobProgress || {})
|
||||
const jobProgressInteractiveSignature = useMemo(() => Object.entries(pipeline?.jobProgress || {})
|
||||
.filter(([, v]) => {
|
||||
const s = String(v?.state || '').toUpperCase();
|
||||
return s === 'METADATA_LOOKUP'
|
||||
@@ -1622,7 +1622,7 @@ export default function RipperPage({
|
||||
|| s === 'READY_TO_ENCODE'
|
||||
|| s === 'WAITING_FOR_USER_DECISION';
|
||||
})
|
||||
.map(([id]) => id)
|
||||
.map(([id, value]) => `${id}:${String(value?.state || '').trim().toUpperCase()}`)
|
||||
.sort()
|
||||
.join(','),
|
||||
[pipeline?.jobProgress]);
|
||||
@@ -1918,7 +1918,17 @@ export default function RipperPage({
|
||||
|
||||
useEffect(() => {
|
||||
void loadRipperJobs();
|
||||
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, cdDriveLifecycleKey, jobsRefreshToken, jobProgressInteractiveKey]);
|
||||
}, [pipeline?.state, pipeline?.activeJobId, pipeline?.context?.jobId, cdDriveLifecycleKey, jobsRefreshToken, jobProgressInteractiveSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
const normalizedPipelineState = String(pipeline?.state || '').trim().toUpperCase();
|
||||
const isReviewState = normalizedPipelineState === 'READY_TO_ENCODE'
|
||||
|| normalizedPipelineState === 'WAITING_FOR_USER_DECISION';
|
||||
if (!isReviewState || !currentPipelineJobId) {
|
||||
return;
|
||||
}
|
||||
setExpandedJobId(currentPipelineJobId);
|
||||
}, [pipeline?.state, currentPipelineJobId]);
|
||||
|
||||
useEffect(() => {
|
||||
api.getDetectedDrives()
|
||||
|
||||
@@ -1476,6 +1476,15 @@ export default function SettingsPage() {
|
||||
effectivePaths={effectivePaths}
|
||||
activationBytes={activationBytes}
|
||||
onNotify={(msg) => toastRef.current?.show(msg)}
|
||||
onSettingApplied={(setting) => {
|
||||
const key = String(setting?.key || '').trim();
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
setDraftValues((prev) => ({ ...prev, [key]: setting?.value }));
|
||||
setInitialValues((prev) => ({ ...prev, [key]: setting?.value }));
|
||||
setErrors((prev) => ({ ...prev, [key]: null }));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
@@ -2228,6 +2228,13 @@ body {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.settings-inline-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.makemkv-beta-key-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
|
||||
Reference in New Issue
Block a user