0.14.0-3 Analyze fix
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const RAW_FOLDER_STATES = Object.freeze({
|
|||||||
RIP_COMPLETE: 'rip_complete',
|
RIP_COMPLETE: 'rip_complete',
|
||||||
COMPLETE: 'complete'
|
COMPLETE: 'complete'
|
||||||
});
|
});
|
||||||
|
const RAW_STATE_PREFIX_CLEANUP_PATTERN = /^(?:(?:incomplete|rip[_-]?complete|rip[_-]?incomplete)[_-])+/i;
|
||||||
const SUBTITLE_CONFIDENCE_SCORES = Object.freeze({
|
const SUBTITLE_CONFIDENCE_SCORES = Object.freeze({
|
||||||
low: 1,
|
low: 1,
|
||||||
medium: 2,
|
medium: 2,
|
||||||
@@ -3960,6 +3961,89 @@ function buildStructuredSeriesAnalysisFromHandBrakeScanJson(scanJson, options =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildStructuredSeriesAnalysisFromPlaylistAnalysis(playlistAnalysis, options = {}) {
|
||||||
|
const sourceTitles = Array.isArray(playlistAnalysis?.titles)
|
||||||
|
? playlistAnalysis.titles
|
||||||
|
: [];
|
||||||
|
if (sourceTitles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syntheticScanJson = {
|
||||||
|
MainFeature: 0,
|
||||||
|
TitleList: sourceTitles.map((title, index) => ({
|
||||||
|
Index: normalizeScanTrackId(title?.titleId, index),
|
||||||
|
Duration: Number(title?.durationSeconds || 0) || String(title?.durationLabel || '').trim() || 0,
|
||||||
|
AudioList: Array.isArray(title?.audioTracks) ? title.audioTracks : [],
|
||||||
|
SubtitleList: Array.isArray(title?.subtitleTracks) ? title.subtitleTracks : [],
|
||||||
|
Size: {
|
||||||
|
Bytes: Number(title?.sizeBytes || 0) || 0
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const structured = buildStructuredSeriesAnalysisFromHandBrakeScanJson(syntheticScanJson, options);
|
||||||
|
if (!structured?.summary || typeof structured.summary !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
...structured.summary,
|
||||||
|
titleCount: Math.max(
|
||||||
|
Number(structured.summary?.titleCount || 0) || 0,
|
||||||
|
sourceTitles.length
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: 'makemkv_analyze_structured',
|
||||||
|
generatedAt: nowIso(),
|
||||||
|
summary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDiscDecryptWarningInScan(scanAnalysisLines = null) {
|
||||||
|
const lines = Array.isArray(scanAnalysisLines)
|
||||||
|
? scanAnalysisLines
|
||||||
|
: (String(scanAnalysisLines || '').trim()
|
||||||
|
? String(scanAnalysisLines || '').split(/\r?\n/)
|
||||||
|
: []);
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.some((line) =>
|
||||||
|
/(aacs|keydbcfg|no valid aacs|cannot decrypt|can't decrypt|encrypted|libdvdcss|\bcss\b|copy protection|bd\+)/i
|
||||||
|
.test(String(line || ''))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRunMakeMkvSeriesSecondChance(seriesAnalysis = null, scanJson = null, scanAnalysisLines = null) {
|
||||||
|
const summary = seriesAnalysis?.summary && typeof seriesAnalysis.summary === 'object'
|
||||||
|
? seriesAnalysis.summary
|
||||||
|
: null;
|
||||||
|
if (Boolean(summary?.seriesLike)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedTitles = parseHandBrakeTitleList(scanJson);
|
||||||
|
const durations = parsedTitles
|
||||||
|
.map((title) => Number(title?.durationSeconds || 0))
|
||||||
|
.filter((value) => Number.isFinite(value) && value > 0);
|
||||||
|
const titleCount = Math.max(
|
||||||
|
Number(summary?.titleCount || 0) || 0,
|
||||||
|
parsedTitles.length
|
||||||
|
);
|
||||||
|
const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
|
||||||
|
const shortOnlyScan = titleCount > 0 && titleCount <= 2 && maxDuration > 0 && maxDuration <= 600;
|
||||||
|
const hasDecryptWarning = hasDiscDecryptWarningInScan(scanAnalysisLines);
|
||||||
|
const noEpisodeSignals = (Number(summary?.episodeCandidateCount || 0) || 0) <= 0
|
||||||
|
&& (Number(summary?.playAllCount || 0) || 0) <= 0;
|
||||||
|
const noFeatureExtras = (Number(summary?.extraCount || 0) || 0) <= 0;
|
||||||
|
const lowConfidence = normalizeSeriesDetectionConfidence(summary?.confidence) === 'low';
|
||||||
|
return lowConfidence && noEpisodeSignals && (shortOnlyScan || (hasDecryptWarning && noFeatureExtras));
|
||||||
|
}
|
||||||
|
|
||||||
function mergeSeriesAnalysisWithStructuredFallback(seriesAnalysis = null, structuredSeriesAnalysis = null) {
|
function mergeSeriesAnalysisWithStructuredFallback(seriesAnalysis = null, structuredSeriesAnalysis = null) {
|
||||||
const baseSummary = seriesAnalysis?.summary && typeof seriesAnalysis.summary === 'object'
|
const baseSummary = seriesAnalysis?.summary && typeof seriesAnalysis.summary === 'object'
|
||||||
? seriesAnalysis.summary
|
? seriesAnalysis.summary
|
||||||
@@ -4183,13 +4267,22 @@ function enrichSeriesAnalyzeContextFromScan(analyzeContext = null, scanLines = n
|
|||||||
|
|
||||||
const scanJson = parseMediainfoJsonOutput(scanText);
|
const scanJson = parseMediainfoJsonOutput(scanText);
|
||||||
const structuredSeriesAnalysis = buildStructuredSeriesAnalysisFromHandBrakeScanJson(scanJson, options?.seriesOptions || {});
|
const structuredSeriesAnalysis = buildStructuredSeriesAnalysisFromHandBrakeScanJson(scanJson, options?.seriesOptions || {});
|
||||||
const mergedSeriesAnalysis = mergeSeriesAnalysisWithStructuredFallback(
|
const playlistStructuredSeriesAnalysis = buildStructuredSeriesAnalysisFromPlaylistAnalysis(
|
||||||
|
baseAnalyzeContext?.playlistAnalysis || null,
|
||||||
|
options?.seriesOptions || {}
|
||||||
|
);
|
||||||
|
const mergedFromScan = mergeSeriesAnalysisWithStructuredFallback(
|
||||||
textSeriesAnalysis,
|
textSeriesAnalysis,
|
||||||
structuredSeriesAnalysis
|
structuredSeriesAnalysis
|
||||||
);
|
);
|
||||||
|
const mergedSeriesAnalysis = mergeSeriesAnalysisWithStructuredFallback(
|
||||||
|
mergedFromScan,
|
||||||
|
playlistStructuredSeriesAnalysis
|
||||||
|
);
|
||||||
const derivedFromText = textSeriesTitles.length > 0;
|
const derivedFromText = textSeriesTitles.length > 0;
|
||||||
const derivedFromStructured = Boolean(structuredSeriesAnalysis?.summary?.seriesLike);
|
const derivedFromStructured = Boolean(structuredSeriesAnalysis?.summary?.seriesLike);
|
||||||
if (!mergedSeriesAnalysis || (!derivedFromText && !derivedFromStructured)) {
|
const derivedFromPlaylistStructured = Boolean(playlistStructuredSeriesAnalysis?.summary?.seriesLike);
|
||||||
|
if (!mergedSeriesAnalysis || (!derivedFromText && !derivedFromStructured && !derivedFromPlaylistStructured)) {
|
||||||
return {
|
return {
|
||||||
analyzeContext: baseAnalyzeContext,
|
analyzeContext: baseAnalyzeContext,
|
||||||
derived: false,
|
derived: false,
|
||||||
@@ -4211,7 +4304,9 @@ function enrichSeriesAnalyzeContextFromScan(analyzeContext = null, scanLines = n
|
|||||||
derived: true,
|
derived: true,
|
||||||
reason: derivedFromText
|
reason: derivedFromText
|
||||||
? 'scan_classification'
|
? 'scan_classification'
|
||||||
: 'scan_json_structural_classification'
|
: (derivedFromStructured
|
||||||
|
? 'scan_json_structural_classification'
|
||||||
|
: 'playlist_structural_classification')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5258,10 +5353,24 @@ function stripRawStatePrefix(folderName) {
|
|||||||
if (!rawName) {
|
if (!rawName) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return rawName
|
const cleaned = rawName
|
||||||
.replace(/^Incomplete_/i, '')
|
.replace(RAW_STATE_PREFIX_CLEANUP_PATTERN, '')
|
||||||
.replace(/^Rip_Complete_/i, '')
|
.replace(/^[_-]+/, '')
|
||||||
.trim();
|
.trim();
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRawFolderJobId(folderName) {
|
||||||
|
const rawName = stripRawStatePrefix(path.basename(String(folderName || '').trim()));
|
||||||
|
if (!rawName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = rawName.match(/-\s*RAW\s*-\s*job-(\d+)\s*$/i);
|
||||||
|
const parsed = Number(match?.[1]);
|
||||||
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.trunc(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractSeriesSeasonDiscFromRawFolderName(folderName) {
|
function extractSeriesSeasonDiscFromRawFolderName(folderName) {
|
||||||
@@ -5302,7 +5411,7 @@ function resolveRawFolderStateFromPath(rawPath) {
|
|||||||
return RAW_FOLDER_STATES.COMPLETE;
|
return RAW_FOLDER_STATES.COMPLETE;
|
||||||
}
|
}
|
||||||
const folderName = path.basename(sourcePath);
|
const folderName = path.basename(sourcePath);
|
||||||
if (/^Incomplete_/i.test(folderName)) {
|
if (/^(?:Incomplete_|Rip[_-]?Incomplete_)/i.test(folderName)) {
|
||||||
return RAW_FOLDER_STATES.INCOMPLETE;
|
return RAW_FOLDER_STATES.INCOMPLETE;
|
||||||
}
|
}
|
||||||
if (/^Rip_Complete_/i.test(folderName)) {
|
if (/^Rip_Complete_/i.test(folderName)) {
|
||||||
@@ -8045,6 +8154,89 @@ class PipelineService extends EventEmitter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async runMakeMkvSeriesSecondChance({
|
||||||
|
jobId,
|
||||||
|
mediaProfile,
|
||||||
|
deviceInfo = null,
|
||||||
|
rawPath = null,
|
||||||
|
silent = true
|
||||||
|
} = {}) {
|
||||||
|
const normalizedJobId = this.normalizeQueueJobId(jobId);
|
||||||
|
const normalizedMediaProfile = normalizeMediaProfile(mediaProfile);
|
||||||
|
if (!normalizedJobId || !isSeriesDiscMediaProfile(normalizedMediaProfile)) {
|
||||||
|
return {
|
||||||
|
runInfo: null,
|
||||||
|
scanLines: [],
|
||||||
|
playlistAnalysis: null,
|
||||||
|
structuredSeriesAnalysis: null,
|
||||||
|
reason: 'invalid_input'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scanLines = [];
|
||||||
|
let runInfo = null;
|
||||||
|
try {
|
||||||
|
await this.ensureMakeMKVRegistration(normalizedJobId, 'ANALYZING').catch((error) => {
|
||||||
|
logger.warn('dvd-series:makemkv-second-chance:registration-failed', {
|
||||||
|
jobId: normalizedJobId,
|
||||||
|
mediaProfile: normalizedMediaProfile,
|
||||||
|
error: errorToMeta(error)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = await settingsService.getEffectiveSettingsMap(normalizedMediaProfile);
|
||||||
|
const analyzeConfig = rawPath
|
||||||
|
? await settingsService.buildMakeMKVAnalyzePathConfig(rawPath, {
|
||||||
|
mediaProfile: normalizedMediaProfile,
|
||||||
|
disableMinLengthFilter: true,
|
||||||
|
settingsMap: settings
|
||||||
|
})
|
||||||
|
: await settingsService.buildMakeMKVAnalyzeConfig(deviceInfo, {
|
||||||
|
mediaProfile: normalizedMediaProfile,
|
||||||
|
disableMinLengthFilter: true,
|
||||||
|
settingsMap: settings
|
||||||
|
});
|
||||||
|
|
||||||
|
runInfo = await this.runCommand({
|
||||||
|
jobId: normalizedJobId,
|
||||||
|
stage: 'ANALYZING',
|
||||||
|
source: 'MAKEMKV_ANALYZE_SERIES_SECOND_CHANCE',
|
||||||
|
cmd: analyzeConfig.cmd,
|
||||||
|
args: analyzeConfig.args,
|
||||||
|
parser: parseMakeMkvProgress,
|
||||||
|
collectLines: scanLines,
|
||||||
|
silent: Boolean(silent)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('dvd-series:makemkv-second-chance:failed', {
|
||||||
|
jobId: normalizedJobId,
|
||||||
|
mediaProfile: normalizedMediaProfile,
|
||||||
|
source: rawPath ? 'raw_path' : 'device',
|
||||||
|
error: errorToMeta(error)
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
runInfo: error?.runInfo || runInfo || null,
|
||||||
|
scanLines,
|
||||||
|
playlistAnalysis: null,
|
||||||
|
structuredSeriesAnalysis: null,
|
||||||
|
reason: 'command_failed'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const playlistAnalysis = analyzePlaylistObfuscation(scanLines, 0, {});
|
||||||
|
const structuredSeriesAnalysis = buildStructuredSeriesAnalysisFromPlaylistAnalysis(
|
||||||
|
playlistAnalysis,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
runInfo,
|
||||||
|
scanLines,
|
||||||
|
playlistAnalysis,
|
||||||
|
structuredSeriesAnalysis,
|
||||||
|
reason: structuredSeriesAnalysis ? 'ok' : 'no_structured_signal'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
isRipSuccessful(job = null) {
|
isRipSuccessful(job = null) {
|
||||||
if (Number(job?.rip_successful || 0) === 1) {
|
if (Number(job?.rip_successful || 0) === 1) {
|
||||||
return true;
|
return true;
|
||||||
@@ -8333,15 +8525,53 @@ class PipelineService extends EventEmitter {
|
|||||||
AND (media_type IS NULL OR media_type <> 'converter')
|
AND (media_type IS NULL OR media_type <> 'converter')
|
||||||
AND (job_kind IS NULL OR job_kind NOT LIKE 'converter_%')
|
AND (job_kind IS NULL OR job_kind NOT LIKE 'converter_%')
|
||||||
`);
|
`);
|
||||||
if (!Array.isArray(rows) || rows.length === 0) {
|
const jobRows = Array.isArray(rows) ? rows : [];
|
||||||
return;
|
const existingJobIdRows = await db.all('SELECT id FROM jobs');
|
||||||
}
|
const existingJobIdSet = new Set(
|
||||||
|
(Array.isArray(existingJobIdRows) ? existingJobIdRows : [])
|
||||||
|
.map((row) => Number(row?.id))
|
||||||
|
.filter((value) => Number.isFinite(value) && value > 0)
|
||||||
|
.map((value) => Math.trunc(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
const linkedRawPathSet = new Set();
|
||||||
|
const addLinkedRawPath = (candidatePath) => {
|
||||||
|
const normalizedCandidate = normalizeComparablePath(candidatePath);
|
||||||
|
if (!normalizedCandidate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
linkedRawPathSet.add(normalizedCandidate);
|
||||||
|
for (const rawRoot of allRawDirs) {
|
||||||
|
if (!isPathInsideDirectory(rawRoot, normalizedCandidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const relative = String(path.relative(rawRoot, normalizedCandidate) || '').trim();
|
||||||
|
if (!relative || relative === '.' || relative.startsWith('..')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const topSegment = relative
|
||||||
|
.split(path.sep)
|
||||||
|
.find((segment) => String(segment || '').trim() && segment !== '.');
|
||||||
|
if (!topSegment) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
linkedRawPathSet.add(normalizeComparablePath(path.join(rawRoot, topSegment)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let renamedCount = 0;
|
let renamedCount = 0;
|
||||||
let pathUpdateCount = 0;
|
let pathUpdateCount = 0;
|
||||||
let ripFlagUpdateCount = 0;
|
let ripFlagUpdateCount = 0;
|
||||||
let conflictCount = 0;
|
let conflictCount = 0;
|
||||||
let missingCount = 0;
|
let missingCount = 0;
|
||||||
|
let orphanScanCount = 0;
|
||||||
|
let orphanCandidateCount = 0;
|
||||||
|
let orphanRenamedCount = 0;
|
||||||
|
let orphanConflictCount = 0;
|
||||||
|
let orphanFailedCount = 0;
|
||||||
|
let orphanAlreadyNormalizedCount = 0;
|
||||||
|
let orphanSkippedLinkedCount = 0;
|
||||||
|
let orphanSkippedKnownJobIdCount = 0;
|
||||||
const discoveredByJobId = new Map();
|
const discoveredByJobId = new Map();
|
||||||
|
|
||||||
for (const scanDir of allRawDirs) {
|
for (const scanDir of allRawDirs) {
|
||||||
@@ -8351,12 +8581,8 @@ class PipelineService extends EventEmitter {
|
|||||||
if (!entry.isDirectory()) {
|
if (!entry.isDirectory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const match = String(entry.name || '').match(/-\s*RAW\s*-\s*job-(\d+)\s*$/i);
|
const mappedJobId = extractRawFolderJobId(entry.name);
|
||||||
if (!match) {
|
if (!mappedJobId) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const mappedJobId = Number(match[1]);
|
|
||||||
if (!Number.isFinite(mappedJobId) || mappedJobId <= 0) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const candidatePath = path.join(scanDir, entry.name);
|
const candidatePath = path.join(scanDir, entry.name);
|
||||||
@@ -8382,7 +8608,7 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of jobRows) {
|
||||||
const jobId = Number(row?.id);
|
const jobId = Number(row?.id);
|
||||||
if (!Number.isFinite(jobId) || jobId <= 0) {
|
if (!Number.isFinite(jobId) || jobId <= 0) {
|
||||||
continue;
|
continue;
|
||||||
@@ -8412,6 +8638,8 @@ class PipelineService extends EventEmitter {
|
|||||||
missingCount += 1;
|
missingCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
addLinkedRawPath(row.raw_path);
|
||||||
|
addLinkedRawPath(currentRawPath);
|
||||||
|
|
||||||
// Keep renamed folder in the same base dir as the current path
|
// Keep renamed folder in the same base dir as the current path
|
||||||
const currentBaseDir = path.dirname(currentRawPath);
|
const currentBaseDir = path.dirname(currentRawPath);
|
||||||
@@ -8459,6 +8687,7 @@ class PipelineService extends EventEmitter {
|
|||||||
fs.renameSync(currentRawPath, desiredRawPath);
|
fs.renameSync(currentRawPath, desiredRawPath);
|
||||||
finalRawPath = desiredRawPath;
|
finalRawPath = desiredRawPath;
|
||||||
renamedCount += 1;
|
renamedCount += 1;
|
||||||
|
addLinkedRawPath(finalRawPath);
|
||||||
} catch (renameError) {
|
} catch (renameError) {
|
||||||
logger.warn('startup:raw-dir-migrate:rename-failed', {
|
logger.warn('startup:raw-dir-migrate:rename-failed', {
|
||||||
jobId,
|
jobId,
|
||||||
@@ -8475,15 +8704,119 @@ class PipelineService extends EventEmitter {
|
|||||||
await historyService.updateRawPathByOldPath(row.raw_path, finalRawPath);
|
await historyService.updateRawPathByOldPath(row.raw_path, finalRawPath);
|
||||||
pathUpdateCount += 1;
|
pathUpdateCount += 1;
|
||||||
}
|
}
|
||||||
|
addLinkedRawPath(finalRawPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renamedCount > 0 || pathUpdateCount > 0 || ripFlagUpdateCount > 0 || conflictCount > 0 || missingCount > 0) {
|
for (const scanDir of allRawDirs) {
|
||||||
|
let dirEntries = [];
|
||||||
|
try {
|
||||||
|
dirEntries = fs.readdirSync(scanDir, { withFileTypes: true });
|
||||||
|
} catch (scanError) {
|
||||||
|
logger.warn('startup:raw-orphan-normalize:scan-failed', {
|
||||||
|
scanDir,
|
||||||
|
error: errorToMeta(scanError)
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of dirEntries) {
|
||||||
|
if (!entry?.isDirectory?.()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rawFolderName = String(entry.name || '').trim();
|
||||||
|
if (!rawFolderName || rawFolderName.startsWith('.')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
orphanScanCount += 1;
|
||||||
|
|
||||||
|
const currentRawPath = normalizeComparablePath(path.join(scanDir, rawFolderName));
|
||||||
|
if (!currentRawPath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (linkedRawPathSet.has(currentRawPath)) {
|
||||||
|
orphanSkippedLinkedCount += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderJobId = extractRawFolderJobId(rawFolderName);
|
||||||
|
if (folderJobId && existingJobIdSet.has(folderJobId)) {
|
||||||
|
orphanSkippedKnownJobIdCount += 1;
|
||||||
|
addLinkedRawPath(currentRawPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
orphanCandidateCount += 1;
|
||||||
|
const cleanedBaseName = stripRawStatePrefix(rawFolderName);
|
||||||
|
const targetFolderName = applyRawFolderStateToName(cleanedBaseName, RAW_FOLDER_STATES.RIP_COMPLETE);
|
||||||
|
if (!targetFolderName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const targetRawPath = normalizeComparablePath(path.join(scanDir, targetFolderName));
|
||||||
|
if (!targetRawPath || targetRawPath === currentRawPath) {
|
||||||
|
orphanAlreadyNormalizedCount += 1;
|
||||||
|
addLinkedRawPath(currentRawPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(targetRawPath)) {
|
||||||
|
orphanConflictCount += 1;
|
||||||
|
logger.warn('startup:raw-orphan-normalize:target-exists', {
|
||||||
|
scanDir,
|
||||||
|
currentRawPath,
|
||||||
|
targetRawPath,
|
||||||
|
folderJobId,
|
||||||
|
folderJobIdExists: existingJobIdSet.has(Number(folderJobId || 0))
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.renameSync(currentRawPath, targetRawPath);
|
||||||
|
orphanRenamedCount += 1;
|
||||||
|
addLinkedRawPath(targetRawPath);
|
||||||
|
logger.info('startup:raw-orphan-normalize:renamed', {
|
||||||
|
from: currentRawPath,
|
||||||
|
to: targetRawPath,
|
||||||
|
folderJobId,
|
||||||
|
folderJobIdExists: existingJobIdSet.has(Number(folderJobId || 0))
|
||||||
|
});
|
||||||
|
} catch (renameError) {
|
||||||
|
orphanFailedCount += 1;
|
||||||
|
logger.warn('startup:raw-orphan-normalize:rename-failed', {
|
||||||
|
scanDir,
|
||||||
|
currentRawPath,
|
||||||
|
targetRawPath,
|
||||||
|
error: errorToMeta(renameError)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
renamedCount > 0
|
||||||
|
|| pathUpdateCount > 0
|
||||||
|
|| ripFlagUpdateCount > 0
|
||||||
|
|| conflictCount > 0
|
||||||
|
|| missingCount > 0
|
||||||
|
|| orphanCandidateCount > 0
|
||||||
|
|| orphanRenamedCount > 0
|
||||||
|
|| orphanConflictCount > 0
|
||||||
|
|| orphanFailedCount > 0
|
||||||
|
) {
|
||||||
logger.info('startup:raw-dir-migrate:done', {
|
logger.info('startup:raw-dir-migrate:done', {
|
||||||
renamedCount,
|
renamedCount,
|
||||||
pathUpdateCount,
|
pathUpdateCount,
|
||||||
ripFlagUpdateCount,
|
ripFlagUpdateCount,
|
||||||
conflictCount,
|
conflictCount,
|
||||||
missingCount,
|
missingCount,
|
||||||
|
orphanScanCount,
|
||||||
|
orphanCandidateCount,
|
||||||
|
orphanRenamedCount,
|
||||||
|
orphanConflictCount,
|
||||||
|
orphanFailedCount,
|
||||||
|
orphanAlreadyNormalizedCount,
|
||||||
|
orphanSkippedLinkedCount,
|
||||||
|
orphanSkippedKnownJobIdCount,
|
||||||
scannedDirs: allRawDirs
|
scannedDirs: allRawDirs
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -12960,6 +13293,31 @@ class PipelineService extends EventEmitter {
|
|||||||
seriesPluginResult?.seriesAnalysis || null,
|
seriesPluginResult?.seriesAnalysis || null,
|
||||||
structuredSeriesAnalysis
|
structuredSeriesAnalysis
|
||||||
);
|
);
|
||||||
|
if (shouldRunMakeMkvSeriesSecondChance(
|
||||||
|
seriesAnalysis,
|
||||||
|
scanJson,
|
||||||
|
reviewResult?.scanAnalysisLines || reviewResult?.scanLines || null
|
||||||
|
)) {
|
||||||
|
const secondChance = await this.runMakeMkvSeriesSecondChance({
|
||||||
|
jobId: normalizedJobId,
|
||||||
|
mediaProfile,
|
||||||
|
rawPath: seriesScanInputPath,
|
||||||
|
silent: true
|
||||||
|
});
|
||||||
|
if (secondChance?.structuredSeriesAnalysis) {
|
||||||
|
seriesAnalysis = mergeSeriesAnalysisWithStructuredFallback(
|
||||||
|
seriesAnalysis,
|
||||||
|
secondChance.structuredSeriesAnalysis
|
||||||
|
);
|
||||||
|
logger.info('dvd-series:analyze:makemkv-second-chance', {
|
||||||
|
jobId: normalizedJobId,
|
||||||
|
source: 'raw_import',
|
||||||
|
titleCount: Number(secondChance?.structuredSeriesAnalysis?.summary?.titleCount || 0) || 0,
|
||||||
|
episodeCandidateCount: Number(secondChance?.structuredSeriesAnalysis?.summary?.episodeCandidateCount || 0) || 0,
|
||||||
|
seriesLike: Boolean(secondChance?.structuredSeriesAnalysis?.summary?.seriesLike)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
seriesLookupHint = seriesPluginResult?.seriesLookupHint || null;
|
seriesLookupHint = seriesPluginResult?.seriesLookupHint || null;
|
||||||
const rawSeriesSummary = seriesAnalysis?.summary && typeof seriesAnalysis.summary === 'object'
|
const rawSeriesSummary = seriesAnalysis?.summary && typeof seriesAnalysis.summary === 'object'
|
||||||
? seriesAnalysis.summary
|
? seriesAnalysis.summary
|
||||||
@@ -13329,6 +13687,31 @@ class PipelineService extends EventEmitter {
|
|||||||
seriesPluginResult?.seriesAnalysis || null,
|
seriesPluginResult?.seriesAnalysis || null,
|
||||||
structuredSeriesAnalysis
|
structuredSeriesAnalysis
|
||||||
);
|
);
|
||||||
|
if (shouldRunMakeMkvSeriesSecondChance(
|
||||||
|
seriesAnalysis,
|
||||||
|
scanJson,
|
||||||
|
reviewResult?.scanAnalysisLines || reviewResult?.scanLines || null
|
||||||
|
)) {
|
||||||
|
const secondChance = await this.runMakeMkvSeriesSecondChance({
|
||||||
|
jobId: job.id,
|
||||||
|
mediaProfile,
|
||||||
|
deviceInfo: deviceWithProfile,
|
||||||
|
silent: true
|
||||||
|
});
|
||||||
|
if (secondChance?.structuredSeriesAnalysis) {
|
||||||
|
seriesAnalysis = mergeSeriesAnalysisWithStructuredFallback(
|
||||||
|
seriesAnalysis,
|
||||||
|
secondChance.structuredSeriesAnalysis
|
||||||
|
);
|
||||||
|
logger.info('dvd-series:analyze:makemkv-second-chance', {
|
||||||
|
jobId: job.id,
|
||||||
|
source: 'drive_analyze',
|
||||||
|
titleCount: Number(secondChance?.structuredSeriesAnalysis?.summary?.titleCount || 0) || 0,
|
||||||
|
episodeCandidateCount: Number(secondChance?.structuredSeriesAnalysis?.summary?.episodeCandidateCount || 0) || 0,
|
||||||
|
seriesLike: Boolean(secondChance?.structuredSeriesAnalysis?.summary?.seriesLike)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
seriesLookupHint = seriesPluginResult?.seriesLookupHint || null;
|
seriesLookupHint = seriesPluginResult?.seriesLookupHint || null;
|
||||||
const rawSeriesSummary = seriesAnalysis?.summary && typeof seriesAnalysis.summary === 'object'
|
const rawSeriesSummary = seriesAnalysis?.summary && typeof seriesAnalysis.summary === 'object'
|
||||||
? seriesAnalysis.summary
|
? seriesAnalysis.summary
|
||||||
@@ -13530,15 +13913,17 @@ class PipelineService extends EventEmitter {
|
|||||||
const seasonFilter = String(seasonNumber || '').trim();
|
const seasonFilter = String(seasonNumber || '').trim();
|
||||||
const applySeasonFilter = (rows) => {
|
const applySeasonFilter = (rows) => {
|
||||||
const sourceRows = Array.isArray(rows) ? rows : [];
|
const sourceRows = Array.isArray(rows) ? rows : [];
|
||||||
|
const failureCode = tmdbService.readFailureCode(sourceRows);
|
||||||
if (!seasonFilter) {
|
if (!seasonFilter) {
|
||||||
return sourceRows;
|
return sourceRows;
|
||||||
}
|
}
|
||||||
const seasonFilterLower = seasonFilter.toLowerCase();
|
const seasonFilterLower = seasonFilter.toLowerCase();
|
||||||
return sourceRows.filter((row) => {
|
const filteredRows = sourceRows.filter((row) => {
|
||||||
const rowSeason = String(row?.seasonNumber ?? '').trim().toLowerCase();
|
const rowSeason = String(row?.seasonNumber ?? '').trim().toLowerCase();
|
||||||
const rowSeasonName = String(row?.seasonName || '').trim().toLowerCase();
|
const rowSeasonName = String(row?.seasonName || '').trim().toLowerCase();
|
||||||
return rowSeason.includes(seasonFilterLower) || rowSeasonName.includes(seasonFilterLower);
|
return rowSeason.includes(seasonFilterLower) || rowSeasonName.includes(seasonFilterLower);
|
||||||
});
|
});
|
||||||
|
return tmdbService.attachFailureCode(filteredRows, failureCode);
|
||||||
};
|
};
|
||||||
logger.info('tmdb:series-search', {
|
logger.info('tmdb:series-search', {
|
||||||
query: normalizedQuery,
|
query: normalizedQuery,
|
||||||
@@ -13576,32 +13961,43 @@ class PipelineService extends EventEmitter {
|
|||||||
language: tmdbLanguage
|
language: tmdbLanguage
|
||||||
});
|
});
|
||||||
normalizedResults = applySeasonFilter(normalizedResults);
|
normalizedResults = applySeasonFilter(normalizedResults);
|
||||||
|
const firstPassFailureCode = tmdbService.readFailureCode(normalizedResults);
|
||||||
|
|
||||||
if (normalizedResults.length === 0) {
|
if (normalizedResults.length === 0) {
|
||||||
logger.warn('tmdb:series-search:empty:first-pass', {
|
if (firstPassFailureCode) {
|
||||||
query: normalizedQuery,
|
logger.warn('tmdb:series-search:empty:first-pass:skip-retry', {
|
||||||
seasonNumber: seasonFilter || null
|
|
||||||
});
|
|
||||||
let retryResults = [];
|
|
||||||
try {
|
|
||||||
retryResults = await tmdbService.searchSeriesWithSeasons(normalizedQuery, {
|
|
||||||
language: tmdbLanguage
|
|
||||||
});
|
|
||||||
} catch (retryError) {
|
|
||||||
logger.warn('tmdb:series-search:retry:failed', {
|
|
||||||
query: normalizedQuery,
|
query: normalizedQuery,
|
||||||
seasonNumber: seasonFilter || null,
|
seasonNumber: seasonFilter || null,
|
||||||
error: errorToMeta(retryError)
|
failureCode: firstPassFailureCode
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn('tmdb:series-search:empty:first-pass', {
|
||||||
|
query: normalizedQuery,
|
||||||
|
seasonNumber: seasonFilter || null
|
||||||
|
});
|
||||||
|
let retryResults = [];
|
||||||
|
try {
|
||||||
|
retryResults = await tmdbService.searchSeriesWithSeasons(normalizedQuery, {
|
||||||
|
language: tmdbLanguage
|
||||||
|
});
|
||||||
|
} catch (retryError) {
|
||||||
|
logger.warn('tmdb:series-search:retry:failed', {
|
||||||
|
query: normalizedQuery,
|
||||||
|
seasonNumber: seasonFilter || null,
|
||||||
|
error: errorToMeta(retryError)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
normalizedResults = applySeasonFilter(retryResults);
|
||||||
}
|
}
|
||||||
normalizedResults = applySeasonFilter(retryResults);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedResults.length === 0) {
|
if (normalizedResults.length === 0) {
|
||||||
|
const failureCode = tmdbService.readFailureCode(normalizedResults);
|
||||||
logger.info('tmdb:series-search:done', {
|
logger.info('tmdb:series-search:done', {
|
||||||
query: normalizedQuery,
|
query: normalizedQuery,
|
||||||
count: 0,
|
count: 0,
|
||||||
empty: true
|
empty: true,
|
||||||
|
failureCode
|
||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,58 @@ const logger = require('./logger').child('TMDB');
|
|||||||
|
|
||||||
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
||||||
const TMDB_IMAGE_BASE_URL = 'https://image.tmdb.org/t/p';
|
const TMDB_IMAGE_BASE_URL = 'https://image.tmdb.org/t/p';
|
||||||
const TMDB_TIMEOUT_MS = 10000;
|
const TMDB_TIMEOUT_MS = 15000;
|
||||||
|
|
||||||
class TmdbService {
|
class TmdbService {
|
||||||
|
isAbortError(error) {
|
||||||
|
const name = String(error?.name || '').trim().toLowerCase();
|
||||||
|
const message = String(error?.message || '').trim().toLowerCase();
|
||||||
|
return name === 'aborterror' || message.includes('aborted');
|
||||||
|
}
|
||||||
|
|
||||||
|
classifyRequestError(error) {
|
||||||
|
if (this.isAbortError(error)) {
|
||||||
|
return 'timeout';
|
||||||
|
}
|
||||||
|
const statusCode = Number(error?.statusCode || 0) || null;
|
||||||
|
if (statusCode === 401 || statusCode === 403) {
|
||||||
|
return 'auth';
|
||||||
|
}
|
||||||
|
if (statusCode >= 500) {
|
||||||
|
return 'upstream';
|
||||||
|
}
|
||||||
|
if (statusCode >= 400) {
|
||||||
|
return 'request_failed';
|
||||||
|
}
|
||||||
|
return 'network';
|
||||||
|
}
|
||||||
|
|
||||||
|
readFailureCode(rows) {
|
||||||
|
const value = rows && typeof rows.tmdbFailureCode === 'string'
|
||||||
|
? rows.tmdbFailureCode
|
||||||
|
: '';
|
||||||
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
|
return normalized || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachFailureCode(rows, failureCode = null) {
|
||||||
|
const output = Array.isArray(rows) ? rows : [];
|
||||||
|
const normalized = String(failureCode || '').trim().toLowerCase();
|
||||||
|
if (!normalized) {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Object.defineProperty(output, 'tmdbFailureCode', {
|
||||||
|
value: normalized,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
} catch (_error) {
|
||||||
|
output.tmdbFailureCode = normalized;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
normalizeNameList(values = [], options = {}) {
|
normalizeNameList(values = [], options = {}) {
|
||||||
const maxItems = Math.max(1, Number(options.maxItems || 10));
|
const maxItems = Math.max(1, Number(options.maxItems || 10));
|
||||||
const source = Array.isArray(values) ? values : [];
|
const source = Array.isArray(values) ? values : [];
|
||||||
@@ -130,24 +179,30 @@ class TmdbService {
|
|||||||
}
|
}
|
||||||
const language = await this.resolveLanguage(options.language);
|
const language = await this.resolveLanguage(options.language);
|
||||||
|
|
||||||
const data = await this.request('/search/tv', {
|
let data = null;
|
||||||
query: {
|
try {
|
||||||
query: normalizedQuery,
|
data = await this.request('/search/tv', {
|
||||||
first_air_date_year: options.year || undefined,
|
query: {
|
||||||
language,
|
query: normalizedQuery,
|
||||||
page: options.page || 1,
|
first_air_date_year: options.year || undefined,
|
||||||
include_adult: false
|
language,
|
||||||
}
|
page: options.page || 1,
|
||||||
}).catch((error) => {
|
include_adult: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const failureCode = this.classifyRequestError(error);
|
||||||
logger.warn('search:failed', {
|
logger.warn('search:failed', {
|
||||||
query: normalizedQuery,
|
query: normalizedQuery,
|
||||||
error: error?.message || String(error)
|
error: error?.message || String(error),
|
||||||
|
failureCode,
|
||||||
|
statusCode: Number(error?.statusCode || 0) || null
|
||||||
});
|
});
|
||||||
return null;
|
return this.attachFailureCode([], failureCode);
|
||||||
});
|
}
|
||||||
|
|
||||||
const rows = Array.isArray(data?.results) ? data.results : [];
|
const rows = Array.isArray(data?.results) ? data.results : [];
|
||||||
return rows
|
const normalizedRows = rows
|
||||||
.map((row) => ({
|
.map((row) => ({
|
||||||
id: Number(row?.id || 0) || null,
|
id: Number(row?.id || 0) || null,
|
||||||
title: String(row?.name || row?.original_name || '').trim() || null,
|
title: String(row?.name || row?.original_name || '').trim() || null,
|
||||||
@@ -162,6 +217,7 @@ class TmdbService {
|
|||||||
popularity: Number(row?.popularity || 0) || 0
|
popularity: Number(row?.popularity || 0) || 0
|
||||||
}))
|
}))
|
||||||
.filter((row) => row.id && row.title);
|
.filter((row) => row.id && row.title);
|
||||||
|
return this.attachFailureCode(normalizedRows, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSeriesDetails(seriesId, options = {}) {
|
async getSeriesDetails(seriesId, options = {}) {
|
||||||
@@ -366,8 +422,9 @@ class TmdbService {
|
|||||||
|
|
||||||
async searchSeriesWithSeasons(query, options = {}) {
|
async searchSeriesWithSeasons(query, options = {}) {
|
||||||
const candidates = await this.searchSeries(query, options);
|
const candidates = await this.searchSeries(query, options);
|
||||||
|
const failureCode = this.readFailureCode(candidates);
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
return [];
|
return this.attachFailureCode([], failureCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = Math.max(1, Math.min(10, Number(options.limit || 5)));
|
const limit = Math.max(1, Math.min(10, Number(options.limit || 5)));
|
||||||
@@ -387,7 +444,8 @@ class TmdbService {
|
|||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return expanded.flat().filter((item) => item?.tmdbId && item?.title);
|
const normalized = expanded.flat().filter((item) => item?.tmdbId && item?.title);
|
||||||
|
return this.attachFailureCode(normalized, failureCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchSeriesWithSeason(query, seasonNumber, options = {}) {
|
async searchSeriesWithSeason(query, seasonNumber, options = {}) {
|
||||||
@@ -397,8 +455,9 @@ class TmdbService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const candidates = await this.searchSeries(query, options);
|
const candidates = await this.searchSeries(query, options);
|
||||||
|
const failureCode = this.readFailureCode(candidates);
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
return [];
|
return this.attachFailureCode([], failureCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = Math.max(1, Math.min(10, Number(options.limit || 5)));
|
const limit = Math.max(1, Math.min(10, Number(options.limit || 5)));
|
||||||
@@ -417,11 +476,12 @@ class TmdbService {
|
|||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return withSeasons
|
const normalized = withSeasons
|
||||||
.filter((candidate) => candidate.season && candidate.season.episodeCount > 0)
|
.filter((candidate) => candidate.season && candidate.season.episodeCount > 0)
|
||||||
.map((candidate) => this.buildSeriesMetadataCandidate(candidate, {
|
.map((candidate) => this.buildSeriesMetadataCandidate(candidate, {
|
||||||
seasonNumber: normalizedSeason
|
seasonNumber: normalizedSeason
|
||||||
}));
|
}));
|
||||||
|
return this.attachFailureCode(normalized, failureCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"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.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.14.0-2",
|
"version": "0.14.0-3",
|
||||||
"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