0.14.0-4 Merger
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-backend",
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-backend",
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@@ -153,6 +153,11 @@ router.post(
|
||||
const id = Number(req.params.id);
|
||||
const target = String(req.body?.target || 'both');
|
||||
const includeRelated = ['1', 'true', 'yes'].includes(String(req.body?.includeRelated || 'false').toLowerCase());
|
||||
const selectedRawPaths = Array.isArray(req.body?.selectedRawPaths)
|
||||
? req.body.selectedRawPaths
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const selectedMoviePaths = Array.isArray(req.body?.selectedMoviePaths)
|
||||
? req.body.selectedMoviePaths
|
||||
.map((item) => String(item || '').trim())
|
||||
@@ -164,11 +169,13 @@ router.post(
|
||||
id,
|
||||
target,
|
||||
includeRelated,
|
||||
selectedRawPathCount: selectedRawPaths ? selectedRawPaths.length : 0,
|
||||
selectedMoviePathCount: selectedMoviePaths ? selectedMoviePaths.length : 0
|
||||
});
|
||||
|
||||
const result = await historyService.deleteJobFiles(id, target, {
|
||||
includeRelated,
|
||||
selectedRawPaths,
|
||||
selectedMoviePaths
|
||||
});
|
||||
res.json(result);
|
||||
@@ -203,6 +210,11 @@ router.post(
|
||||
const requestedKeepDetectedDevice = req.body?.keepDetectedDevice !== undefined
|
||||
? ['1', 'true', 'yes'].includes(String(req.body?.keepDetectedDevice || 'false').toLowerCase())
|
||||
: !requestedResetDriveState;
|
||||
const selectedRawPaths = Array.isArray(req.body?.selectedRawPaths)
|
||||
? req.body.selectedRawPaths
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const selectedMoviePaths = Array.isArray(req.body?.selectedMoviePaths)
|
||||
? req.body.selectedMoviePaths
|
||||
.map((item) => String(item || '').trim())
|
||||
@@ -217,6 +229,7 @@ router.post(
|
||||
requestedResetDriveState,
|
||||
preserveRawForImportJobs,
|
||||
requestedKeepDetectedDevice,
|
||||
selectedRawPathCount: selectedRawPaths ? selectedRawPaths.length : 0,
|
||||
selectedMoviePathCount: selectedMoviePaths ? selectedMoviePaths.length : 0
|
||||
});
|
||||
|
||||
@@ -246,6 +259,7 @@ router.post(
|
||||
: [];
|
||||
const result = await historyService.deleteJob(id, target, {
|
||||
includeRelated,
|
||||
selectedRawPaths,
|
||||
selectedMoviePaths,
|
||||
preserveRawForImportJobs
|
||||
});
|
||||
|
||||
@@ -371,7 +371,10 @@ router.post(
|
||||
seasonName,
|
||||
episodeCount,
|
||||
episodes,
|
||||
discNumber
|
||||
discNumber,
|
||||
duplicateAction,
|
||||
existingJobId,
|
||||
existingDiscNumber
|
||||
} = req.body;
|
||||
|
||||
if (!jobId) {
|
||||
@@ -396,7 +399,10 @@ router.post(
|
||||
tmdbId,
|
||||
workflowKind,
|
||||
seasonNumber,
|
||||
discNumber
|
||||
discNumber,
|
||||
duplicateAction,
|
||||
existingJobId,
|
||||
existingDiscNumber
|
||||
});
|
||||
|
||||
const job = await pipelineService.selectMetadata({
|
||||
@@ -418,7 +424,10 @@ router.post(
|
||||
seasonName,
|
||||
episodeCount,
|
||||
episodes,
|
||||
discNumber
|
||||
discNumber,
|
||||
duplicateAction,
|
||||
existingJobId,
|
||||
existingDiscNumber
|
||||
});
|
||||
|
||||
res.json({ job });
|
||||
|
||||
@@ -385,7 +385,16 @@ function normalizeJobKindValue(value) {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
if (raw === 'audiobook' || raw === 'cd' || raw === 'dvd' || raw === 'bluray') {
|
||||
if (
|
||||
raw === 'audiobook'
|
||||
|| raw === 'cd'
|
||||
|| raw === 'dvd'
|
||||
|| raw === 'bluray'
|
||||
|| raw === 'dvd_series_container'
|
||||
|| raw === 'dvd_series_child'
|
||||
|| raw === 'multipart_movie_container'
|
||||
|| raw === 'multipart_movie_child'
|
||||
) {
|
||||
return raw;
|
||||
}
|
||||
if (raw === 'converter_audio' || raw === 'converter_video' || raw === 'converter_iso') {
|
||||
@@ -4636,7 +4645,14 @@ class HistoryService {
|
||||
if (child?.rawStatus?.exists) {
|
||||
rawCount += 1;
|
||||
}
|
||||
if (child?.backupSuccess) {
|
||||
// Backup should be considered present when rip-success markers are set
|
||||
// or when RAW is physically available for that child.
|
||||
const backupPresent = Boolean(
|
||||
child?.backupSuccess
|
||||
|| child?.rawStatus?.exists
|
||||
|| String(child?.makemkvInfo?.status || '').trim().toUpperCase() === 'SUCCESS'
|
||||
);
|
||||
if (backupPresent) {
|
||||
backupCount += 1;
|
||||
}
|
||||
if (child?.encodeSuccess) {
|
||||
@@ -6275,6 +6291,12 @@ class HistoryService {
|
||||
const normalizedTarget = String(target || 'both').trim().toLowerCase();
|
||||
const includesRaw = normalizedTarget === 'raw' || normalizedTarget === 'both';
|
||||
const includesMovie = normalizedTarget === 'movie' || normalizedTarget === 'both';
|
||||
const selectedRawPathFilter = new Set(
|
||||
(Array.isArray(options?.selectedRawPaths) ? options.selectedRawPaths : [])
|
||||
.map((rawPath) => String(rawPath || '').trim())
|
||||
.filter(Boolean)
|
||||
.map((rawPath) => normalizeComparablePath(rawPath))
|
||||
);
|
||||
const selectedMoviePathFilter = new Set(
|
||||
(Array.isArray(options?.selectedMoviePaths) ? options.selectedMoviePaths : [])
|
||||
.map((moviePath) => String(moviePath || '').trim())
|
||||
@@ -6286,6 +6308,7 @@ class HistoryService {
|
||||
target: normalizedTarget,
|
||||
raw: { attempted: includesRaw, deleted: false, filesDeleted: 0, dirsRemoved: 0, pathsDeleted: 0, reason: null },
|
||||
movie: { attempted: includesMovie, deleted: false, filesDeleted: 0, dirsRemoved: 0, pathsDeleted: 0, reason: null },
|
||||
selectedRawPaths: Array.from(selectedRawPathFilter),
|
||||
selectedMoviePaths: Array.from(selectedMoviePathFilter),
|
||||
deletedPaths: []
|
||||
};
|
||||
@@ -6293,6 +6316,15 @@ class HistoryService {
|
||||
const applyTarget = (targetKey) => {
|
||||
let candidates = (Array.isArray(preview?.pathCandidates?.[targetKey]) ? preview.pathCandidates[targetKey] : [])
|
||||
.filter((item) => Boolean(item?.exists) && (Boolean(item?.isDirectory) || Boolean(item?.isFile)));
|
||||
if (targetKey === 'raw' && selectedRawPathFilter.size > 0) {
|
||||
candidates = candidates.filter((item) => {
|
||||
const candidatePath = String(item?.path || '').trim();
|
||||
if (!candidatePath) {
|
||||
return false;
|
||||
}
|
||||
return selectedRawPathFilter.has(normalizeComparablePath(candidatePath));
|
||||
});
|
||||
}
|
||||
if (targetKey === 'movie' && selectedMoviePathFilter.size > 0) {
|
||||
candidates = candidates.filter((item) => {
|
||||
const candidatePath = String(item?.path || '').trim();
|
||||
@@ -6303,9 +6335,13 @@ class HistoryService {
|
||||
});
|
||||
}
|
||||
if (candidates.length === 0) {
|
||||
summary[targetKey].reason = targetKey === 'movie' && selectedMoviePathFilter.size > 0
|
||||
? 'Keine ausgewählten Audio/Video-Ordner gefunden.'
|
||||
: 'Keine passenden Dateien/Ordner gefunden.';
|
||||
if (targetKey === 'raw' && selectedRawPathFilter.size > 0) {
|
||||
summary[targetKey].reason = 'Keine ausgewählten RAW-Dateien/Ordner gefunden.';
|
||||
} else if (targetKey === 'movie' && selectedMoviePathFilter.size > 0) {
|
||||
summary[targetKey].reason = 'Keine ausgewählten Audio/Video-Ordner gefunden.';
|
||||
} else {
|
||||
summary[targetKey].reason = 'Keine passenden Dateien/Ordner gefunden.';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6413,16 +6449,21 @@ class HistoryService {
|
||||
}
|
||||
|
||||
const includeRelated = Boolean(options?.includeRelated);
|
||||
const selectedRawPaths = Array.isArray(options?.selectedRawPaths)
|
||||
? options.selectedRawPaths
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const selectedMoviePaths = Array.isArray(options?.selectedMoviePaths)
|
||||
? options.selectedMoviePaths
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
|
||||
if (includeRelated || (selectedMoviePaths && selectedMoviePaths.length > 0)) {
|
||||
if (includeRelated || (selectedRawPaths && selectedRawPaths.length > 0) || (selectedMoviePaths && selectedMoviePaths.length > 0)) {
|
||||
const normalizedJobId = normalizeJobIdValue(jobId);
|
||||
const preview = await this.getJobDeletePreview(normalizedJobId, { includeRelated });
|
||||
const summary = this._deletePathsFromPreview(preview, target, { selectedMoviePaths });
|
||||
const summary = this._deletePathsFromPreview(preview, target, { selectedRawPaths, selectedMoviePaths });
|
||||
const relatedJobIds = Array.from(new Set(
|
||||
(Array.isArray(preview?.relatedJobs) ? preview.relatedJobs : [])
|
||||
.map((row) => normalizeJobIdValue(row?.id))
|
||||
@@ -6471,6 +6512,7 @@ class HistoryService {
|
||||
logger.info('job:delete-files', {
|
||||
jobId: normalizedJobId,
|
||||
includeRelated,
|
||||
selectedRawPathCount: selectedRawPaths ? selectedRawPaths.length : 0,
|
||||
selectedMoviePathCount: selectedMoviePaths ? selectedMoviePaths.length : 0,
|
||||
summary
|
||||
});
|
||||
@@ -7158,6 +7200,11 @@ class HistoryService {
|
||||
paths: deletedPaths
|
||||
};
|
||||
};
|
||||
const normalizedSelectedRawPaths = Array.isArray(options?.selectedRawPaths)
|
||||
? options.selectedRawPaths
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const normalizedSelectedMoviePaths = Array.isArray(options?.selectedMoviePaths)
|
||||
? options.selectedMoviePaths
|
||||
.map((item) => String(item || '').trim())
|
||||
@@ -7191,6 +7238,7 @@ class HistoryService {
|
||||
}
|
||||
|
||||
let effectiveFileTarget = resolveEffectiveFileTarget(fileTarget, existing);
|
||||
let selectedRawPathsForDelete = normalizedSelectedRawPaths;
|
||||
let selectedMoviePathsForDelete = normalizedSelectedMoviePaths;
|
||||
let preview = null;
|
||||
let fileSummary = null;
|
||||
@@ -7209,6 +7257,7 @@ class HistoryService {
|
||||
preview = await this.getJobDeletePreview(normalizedJobId, { includeRelated: false });
|
||||
}
|
||||
fileSummary = this._deletePathsFromPreview(preview, effectiveFileTarget, {
|
||||
selectedRawPaths: selectedRawPathsForDelete,
|
||||
selectedMoviePaths: selectedMoviePathsForDelete
|
||||
});
|
||||
}
|
||||
@@ -7371,6 +7420,7 @@ class HistoryService {
|
||||
}
|
||||
|
||||
let effectiveFileTarget = resolveEffectiveFileTarget(fileTarget, existing);
|
||||
let selectedRawPathsForDelete = normalizedSelectedRawPaths;
|
||||
let selectedMoviePathsForDelete = normalizedSelectedMoviePaths;
|
||||
if (effectiveFileTarget === 'none') {
|
||||
const autoIncompleteMoviePaths = collectIncompleteMoviePathsFromPreview(preview);
|
||||
@@ -7384,6 +7434,7 @@ class HistoryService {
|
||||
let fileSummary = null;
|
||||
if (effectiveFileTarget !== 'none') {
|
||||
fileSummary = this._deletePathsFromPreview(preview, effectiveFileTarget, {
|
||||
selectedRawPaths: selectedRawPathsForDelete,
|
||||
selectedMoviePaths: selectedMoviePathsForDelete
|
||||
});
|
||||
}
|
||||
|
||||
@@ -166,6 +166,81 @@ function resolveSeriesDiscLabel(mediaProfile) {
|
||||
return 'Disc';
|
||||
}
|
||||
|
||||
function normalizeMovieFingerprintLabel(value) {
|
||||
return String(value || '')
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
function buildMovieMetadataFingerprint(mediaProfile, metadata = {}) {
|
||||
const normalizedMediaProfile = normalizeMediaProfile(mediaProfile);
|
||||
if (normalizedMediaProfile !== 'dvd' && normalizedMediaProfile !== 'bluray') {
|
||||
return null;
|
||||
}
|
||||
const imdbId = String(metadata?.imdbId || '').trim().toLowerCase();
|
||||
if (imdbId) {
|
||||
return `${normalizedMediaProfile}|imdb|${imdbId}`;
|
||||
}
|
||||
const title = normalizeMovieFingerprintLabel(metadata?.title || '');
|
||||
const year = normalizePositiveInteger(metadata?.year);
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
if (year) {
|
||||
return `${normalizedMediaProfile}|title_year|${title}|${year}`;
|
||||
}
|
||||
return `${normalizedMediaProfile}|title|${title}`;
|
||||
}
|
||||
|
||||
function extractSelectedMetadataFromMakemkvInfo(makemkvInfo = null) {
|
||||
const mkInfo = makemkvInfo && typeof makemkvInfo === 'object'
|
||||
? makemkvInfo
|
||||
: {};
|
||||
const analyzeContext = mkInfo?.analyzeContext && typeof mkInfo.analyzeContext === 'object'
|
||||
? mkInfo.analyzeContext
|
||||
: {};
|
||||
const analyzeSelected = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
|
||||
? analyzeContext.selectedMetadata
|
||||
: {};
|
||||
const topLevelSelected = mkInfo?.selectedMetadata && typeof mkInfo.selectedMetadata === 'object'
|
||||
? mkInfo.selectedMetadata
|
||||
: {};
|
||||
return {
|
||||
...topLevelSelected,
|
||||
...analyzeSelected
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDiscNumberFromJobLike(job = null) {
|
||||
const direct = normalizePositiveInteger(job?.disc_number);
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
let parsedMakemkvInfo = {};
|
||||
if (typeof job?.makemkv_info_json === 'string') {
|
||||
try {
|
||||
parsedMakemkvInfo = JSON.parse(job.makemkv_info_json) || {};
|
||||
} catch (_error) {
|
||||
parsedMakemkvInfo = {};
|
||||
}
|
||||
} else if (job?.makemkv_info_json && typeof job.makemkv_info_json === 'object') {
|
||||
parsedMakemkvInfo = job.makemkv_info_json;
|
||||
} else if (job?.makemkvInfo && typeof job.makemkvInfo === 'object') {
|
||||
parsedMakemkvInfo = job.makemkvInfo;
|
||||
}
|
||||
const mkInfo = parsedMakemkvInfo;
|
||||
const selectedMetadata = extractSelectedMetadataFromMakemkvInfo(mkInfo);
|
||||
return normalizePositiveInteger(
|
||||
selectedMetadata?.discNumber
|
||||
?? mkInfo?.analyzeContext?.seriesLookupHint?.discNumber
|
||||
?? null
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSelectedMetadataForJob(job = null, analyzeContext = null, activeContext = null) {
|
||||
const analyzeSelectedMetadata = analyzeContext?.selectedMetadata && typeof analyzeContext.selectedMetadata === 'object'
|
||||
? analyzeContext.selectedMetadata
|
||||
@@ -605,7 +680,16 @@ function normalizeJobKind(value) {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
if (raw === 'audiobook' || raw === 'cd' || raw === 'dvd' || raw === 'bluray') {
|
||||
if (
|
||||
raw === 'audiobook'
|
||||
|| raw === 'cd'
|
||||
|| raw === 'dvd'
|
||||
|| raw === 'bluray'
|
||||
|| raw === 'dvd_series_container'
|
||||
|| raw === 'dvd_series_child'
|
||||
|| raw === 'multipart_movie_container'
|
||||
|| raw === 'multipart_movie_child'
|
||||
) {
|
||||
return raw;
|
||||
}
|
||||
if (raw === 'converter_audio' || raw === 'converter_video' || raw === 'converter_iso') {
|
||||
@@ -8868,6 +8952,13 @@ class PipelineService extends EventEmitter {
|
||||
error: errorToMeta(driveLockRecoveryError)
|
||||
});
|
||||
}
|
||||
try {
|
||||
await this._forceUnlockStaleDriveLocks();
|
||||
} catch (driveLockCleanupError) {
|
||||
logger.warn('init:drive-lock-cleanup-failed', {
|
||||
error: errorToMeta(driveLockCleanupError)
|
||||
});
|
||||
}
|
||||
|
||||
// Always start with a clean ripper/session snapshot after server restart.
|
||||
const hasContextKeys = this.snapshot.context
|
||||
@@ -8887,7 +8978,7 @@ class PipelineService extends EventEmitter {
|
||||
const staleRows = await db.all(`
|
||||
SELECT id, status, last_state
|
||||
FROM jobs
|
||||
WHERE COALESCE(job_kind, '') != 'dvd_series_container'
|
||||
WHERE COALESCE(job_kind, '') NOT IN ('dvd_series_container', 'multipart_movie_container')
|
||||
AND status IN ('ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING',
|
||||
'CD_ANALYZING', 'CD_RIPPING', 'CD_ENCODING')
|
||||
ORDER BY updated_at ASC, id ASC
|
||||
@@ -9466,7 +9557,7 @@ class PipelineService extends EventEmitter {
|
||||
FROM jobs
|
||||
WHERE disc_device IS NOT NULL
|
||||
AND TRIM(disc_device) <> ''
|
||||
AND COALESCE(job_kind, '') != 'dvd_series_container'
|
||||
AND COALESCE(job_kind, '') NOT IN ('dvd_series_container', 'multipart_movie_container')
|
||||
AND status IN ('RIPPING', 'CD_RIPPING', 'ERROR', 'CANCELLED')
|
||||
`
|
||||
);
|
||||
@@ -9812,19 +9903,28 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
|
||||
async _forceUnlockStaleDriveLocks(devicePaths = []) {
|
||||
const activeLocks = Array.isArray(diskDetectionService.getActiveLocks?.())
|
||||
? diskDetectionService.getActiveLocks()
|
||||
: [];
|
||||
const normalizedPaths = Array.from(new Set(
|
||||
(Array.isArray(devicePaths) ? devicePaths : [])
|
||||
.map((value) => this.normalizeDrivePath(value))
|
||||
.filter((value) => Boolean(value) && !value.startsWith('__virtual__'))
|
||||
));
|
||||
if (normalizedPaths.length === 0 || typeof diskDetectionService.forceUnlockDevice !== 'function') {
|
||||
const pathsToCheck = normalizedPaths.length > 0
|
||||
? normalizedPaths
|
||||
: Array.from(new Set(
|
||||
activeLocks
|
||||
.map((entry) => this.normalizeDrivePath(entry?.path))
|
||||
.filter((value) => Boolean(value) && !value.startsWith('__virtual__'))
|
||||
));
|
||||
if (pathsToCheck.length === 0 || typeof diskDetectionService.forceUnlockDevice !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const activeLocks = diskDetectionService.getActiveLocks();
|
||||
const locksToCheck = normalizedPaths
|
||||
const locksToCheck = pathsToCheck
|
||||
.map((devicePath) => {
|
||||
const lock = activeLocks.find((entry) => this.normalizeDrivePath(entry?.path) === devicePath) || null;
|
||||
const lock = activeLocks.find((entry) => this._isSameDrivePath(entry?.path, devicePath)) || null;
|
||||
return lock ? { devicePath, lock } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
@@ -9832,36 +9932,6 @@ class PipelineService extends EventEmitter {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ownerJobIds = new Set();
|
||||
for (const entry of locksToCheck) {
|
||||
const owners = Array.isArray(entry.lock?.owners) ? entry.lock.owners : [];
|
||||
for (const owner of owners) {
|
||||
const jobId = Number(owner?.jobId);
|
||||
if (Number.isFinite(jobId) && jobId > 0) {
|
||||
ownerJobIds.add(Math.trunc(jobId));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ownerJobIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let existingJobIds = new Set();
|
||||
try {
|
||||
const db = await getDb();
|
||||
const placeholders = Array.from(ownerJobIds).map(() => '?').join(', ');
|
||||
const rows = await db.all(`SELECT id FROM jobs WHERE id IN (${placeholders})`, Array.from(ownerJobIds));
|
||||
existingJobIds = new Set(
|
||||
(Array.isArray(rows) ? rows : [])
|
||||
.map((row) => Number(row?.id))
|
||||
.filter((value) => Number.isFinite(value) && value > 0)
|
||||
.map((value) => Math.trunc(value))
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn('drive-lock:stale-check:failed', { error: errorToMeta(error) });
|
||||
return [];
|
||||
}
|
||||
|
||||
const cleared = [];
|
||||
for (const entry of locksToCheck) {
|
||||
const owners = Array.isArray(entry.lock?.owners) ? entry.lock.owners : [];
|
||||
@@ -9870,22 +9940,33 @@ class PipelineService extends EventEmitter {
|
||||
if (!Number.isFinite(jobId) || jobId <= 0) {
|
||||
return false;
|
||||
}
|
||||
return existingJobIds.has(Math.trunc(jobId));
|
||||
const normalizedJobId = Math.trunc(jobId);
|
||||
return this._isActiveProcessForJob(normalizedJobId, { cleanupStale: true });
|
||||
});
|
||||
if (hasActiveOwner) {
|
||||
continue;
|
||||
}
|
||||
const clearedJobIds = [];
|
||||
for (const [jobId, lock] of this.driveLocksByJob.entries()) {
|
||||
if (!this._isSameDrivePath(lock?.devicePath, entry.devicePath)) {
|
||||
continue;
|
||||
}
|
||||
this.driveLocksByJob.delete(jobId);
|
||||
clearedJobIds.push(Number(jobId));
|
||||
}
|
||||
|
||||
const clearedCount = Number(diskDetectionService.forceUnlockDevice(entry.devicePath, {
|
||||
reason: 'stale_lock',
|
||||
deletedJobIds: []
|
||||
deletedJobIds: clearedJobIds
|
||||
}) || 0);
|
||||
if (clearedCount > 0) {
|
||||
if (clearedCount > 0 || clearedJobIds.length > 0) {
|
||||
cleared.push(entry.devicePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleared.length > 0) {
|
||||
logger.warn('drive-lock:stale-unlocked', { devicePaths: cleared });
|
||||
this._broadcastPipelineStateChanged();
|
||||
}
|
||||
return cleared;
|
||||
}
|
||||
@@ -10082,7 +10163,7 @@ class PipelineService extends EventEmitter {
|
||||
let filmRunning = 0;
|
||||
let audioRunning = 0;
|
||||
for (const job of rows) {
|
||||
if (this.isSeriesContainerHistoryJob(job) || this.isSeriesBatchParentQueueAnchor(job)) {
|
||||
if (this.isContainerHistoryJob(job) || this.isSeriesBatchParentQueueAnchor(job)) {
|
||||
continue;
|
||||
}
|
||||
const status = String(job?.status || job?.last_state || '').trim().toUpperCase();
|
||||
@@ -10113,6 +10194,15 @@ class PipelineService extends EventEmitter {
|
||||
return jobKind === 'dvd_series_container';
|
||||
}
|
||||
|
||||
isMultipartMovieContainerHistoryJob(jobLike = null) {
|
||||
const jobKind = String(jobLike?.job_kind || jobLike?.jobKind || '').trim().toLowerCase();
|
||||
return jobKind === 'multipart_movie_container';
|
||||
}
|
||||
|
||||
isContainerHistoryJob(jobLike = null) {
|
||||
return this.isSeriesContainerHistoryJob(jobLike) || this.isMultipartMovieContainerHistoryJob(jobLike);
|
||||
}
|
||||
|
||||
_deriveSeriesContainerStateFromChildren(childJobs = []) {
|
||||
const rows = Array.isArray(childJobs) ? childJobs : [];
|
||||
if (rows.length === 0) {
|
||||
@@ -10575,6 +10665,37 @@ class PipelineService extends EventEmitter {
|
||||
const finalState = errorCount > 0
|
||||
? 'ERROR'
|
||||
: (cancelledCount > 0 ? 'CANCELLED' : 'FINISHED');
|
||||
let finalizedParentRawPath = String(parentJob?.raw_path || '').trim() || null;
|
||||
if (finalState === 'FINISHED' && finalizedParentRawPath) {
|
||||
const completedRawPath = buildCompletedRawPath(finalizedParentRawPath);
|
||||
if (completedRawPath && completedRawPath !== finalizedParentRawPath) {
|
||||
if (fs.existsSync(completedRawPath)) {
|
||||
logger.warn('series-batch:raw-dir-finalize:target-exists', {
|
||||
parentJobId: normalizedParentJobId,
|
||||
sourceRawPath: finalizedParentRawPath,
|
||||
targetRawPath: completedRawPath
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
fs.renameSync(finalizedParentRawPath, completedRawPath);
|
||||
await historyService.updateRawPathByOldPath(finalizedParentRawPath, completedRawPath);
|
||||
await historyService.appendLog(
|
||||
normalizedParentJobId,
|
||||
'SYSTEM',
|
||||
`RAW-Ordner nach Abschluss aller Serien-Encodes finalisiert (Prefix entfernt): ${finalizedParentRawPath} -> ${completedRawPath}`
|
||||
);
|
||||
finalizedParentRawPath = completedRawPath;
|
||||
} catch (rawFinalizeError) {
|
||||
logger.warn('series-batch:raw-dir-finalize:rename-failed', {
|
||||
parentJobId: normalizedParentJobId,
|
||||
sourceRawPath: finalizedParentRawPath,
|
||||
targetRawPath: completedRawPath,
|
||||
error: errorToMeta(rawFinalizeError)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const finalErrorMessage = finalState === 'FINISHED'
|
||||
? null
|
||||
: (
|
||||
@@ -10588,7 +10709,8 @@ class PipelineService extends EventEmitter {
|
||||
status: finalState,
|
||||
last_state: finalState,
|
||||
end_time: nowIso(),
|
||||
error_message: finalErrorMessage
|
||||
error_message: finalErrorMessage,
|
||||
...(finalizedParentRawPath ? { raw_path: finalizedParentRawPath } : {})
|
||||
});
|
||||
// Remove stale queue entries that still target this parent batch.
|
||||
this.queueEntries = this.queueEntries.filter((entry) => {
|
||||
@@ -10645,12 +10767,13 @@ class PipelineService extends EventEmitter {
|
||||
const hasPendingChildren = terminalCount < totalCount;
|
||||
const hasErrors = errorCount > 0;
|
||||
const hasCancellations = cancelledCount > 0;
|
||||
const nextState = hasErrors
|
||||
? 'ERROR'
|
||||
: (hasCancellations ? 'CANCELLED' : (hasPendingChildren ? 'ENCODING' : 'READY_TO_ENCODE'));
|
||||
const nextLastState = hasPendingChildren && nextState === 'ENCODING'
|
||||
// Keep parent job in ENCODING while any child is still pending/running.
|
||||
// Partial errors/cancellations are reflected via summary/error counters,
|
||||
// but must not flip the parent state to terminal before all children end.
|
||||
const nextState = hasPendingChildren
|
||||
? 'ENCODING'
|
||||
: nextState;
|
||||
: (hasErrors ? 'ERROR' : (hasCancellations ? 'CANCELLED' : 'READY_TO_ENCODE'));
|
||||
const nextLastState = hasPendingChildren ? 'ENCODING' : nextState;
|
||||
const nextErrorMessage = hasErrors
|
||||
? `Serien-Batch enthält ${errorCount} Fehler-Episode(n).`
|
||||
: (
|
||||
@@ -11506,7 +11629,7 @@ class PipelineService extends EventEmitter {
|
||||
historyService.getQueueIdleJobs()
|
||||
]);
|
||||
const visibleRunningJobs = (Array.isArray(runningJobs) ? runningJobs : []).filter((job) => (
|
||||
!this.isSeriesContainerHistoryJob(job)
|
||||
!this.isContainerHistoryJob(job)
|
||||
&& !this.isSeriesBatchParentQueueAnchor(job)
|
||||
));
|
||||
const runningPoolUsage = this.buildRunningPoolUsage(runningJobs);
|
||||
@@ -11523,7 +11646,7 @@ class PipelineService extends EventEmitter {
|
||||
.filter((id) => Number.isFinite(id) && id > 0)
|
||||
);
|
||||
const idleJobs = (Array.isArray(idleJobsRaw) ? idleJobsRaw : []).filter((job) => {
|
||||
if (this.isSeriesContainerHistoryJob(job) || this.isSeriesBatchParentQueueAnchor(job)) {
|
||||
if (this.isContainerHistoryJob(job) || this.isSeriesBatchParentQueueAnchor(job)) {
|
||||
return false;
|
||||
}
|
||||
const jobId = Number(job?.id);
|
||||
@@ -13074,6 +13197,21 @@ class PipelineService extends EventEmitter {
|
||||
jobId: normalizedJobId,
|
||||
rawPath: options?.rawPath || null
|
||||
});
|
||||
const activeLockPaths = Array.isArray(diskDetectionService.getActiveLocks?.())
|
||||
? diskDetectionService.getActiveLocks()
|
||||
.map((entry) => this.normalizeDrivePath(entry?.path))
|
||||
.filter((value) => Boolean(value) && !value.startsWith('__virtual__'))
|
||||
: [];
|
||||
if (activeLockPaths.length > 0) {
|
||||
try {
|
||||
await this._forceUnlockStaleDriveLocks(activeLockPaths);
|
||||
} catch (error) {
|
||||
logger.warn('analyze:raw-import:stale-lock-cleanup-failed', {
|
||||
jobId: normalizedJobId,
|
||||
error: errorToMeta(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sourceJob = await historyService.getJobById(normalizedJobId);
|
||||
if (!sourceJob) {
|
||||
@@ -13594,6 +13732,15 @@ class PipelineService extends EventEmitter {
|
||||
detectedTitle,
|
||||
jobKind: resolveJobKindForMediaProfile(mediaProfile)
|
||||
});
|
||||
let analyzeDriveLockHeld = false;
|
||||
if (effectiveAnalyzeDevicePath && !String(effectiveAnalyzeDevicePath).startsWith('__virtual__')) {
|
||||
analyzeDriveLockHeld = this._acquireDriveLockForJob(effectiveAnalyzeDevicePath, job.id, {
|
||||
stage: 'ANALYZING',
|
||||
source: 'DRIVE_ANALYZE',
|
||||
mediaProfile,
|
||||
reason: 'drive_analyze_running'
|
||||
});
|
||||
}
|
||||
|
||||
// Surface the newly created analyze job immediately in the Ripper UI,
|
||||
// even if metadata provider lookups (TMDb/OMDb) take longer.
|
||||
@@ -13885,6 +14032,10 @@ class PipelineService extends EventEmitter {
|
||||
logger.error('metadata:prepare:failed', { jobId: job.id, error: errorToMeta(error) });
|
||||
await this.failJob(job.id, 'METADATA_SELECTION', error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (analyzeDriveLockHeld) {
|
||||
this._releaseDriveLockForJob(job.id, { reason: 'drive_analyze_finished' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15736,7 +15887,10 @@ class PipelineService extends EventEmitter {
|
||||
seasonName = null,
|
||||
episodeCount = null,
|
||||
episodes = null,
|
||||
discNumber = null
|
||||
discNumber = null,
|
||||
duplicateAction = null,
|
||||
existingJobId = null,
|
||||
existingDiscNumber = null
|
||||
}) {
|
||||
this.ensureNotBusy('selectMetadata', jobId);
|
||||
logger.info('metadata:selected', {
|
||||
@@ -15754,7 +15908,10 @@ class PipelineService extends EventEmitter {
|
||||
tmdbId,
|
||||
workflowKind,
|
||||
seasonNumber,
|
||||
discNumber
|
||||
discNumber,
|
||||
duplicateAction,
|
||||
existingJobId,
|
||||
existingDiscNumber
|
||||
});
|
||||
|
||||
const job = await historyService.getJobById(jobId);
|
||||
@@ -16084,6 +16241,9 @@ class PipelineService extends EventEmitter {
|
||||
? 'series'
|
||||
: (effectiveMetadataProvider === 'omdb' ? 'film' : null);
|
||||
const effectiveWorkflowKind = requestedWorkflowKind || providerWorkflowKind || existingWorkflowKind;
|
||||
const normalizedDuplicateAction = String(duplicateAction || '').trim().toLowerCase();
|
||||
const requestedExistingJobId = normalizePositiveInteger(existingJobId);
|
||||
const requestedExistingDiscNumber = normalizePositiveInteger(existingDiscNumber);
|
||||
if (isSeriesDiscMediaProfile(mediaProfile)) {
|
||||
if (effectiveWorkflowKind === 'series') {
|
||||
effectiveMetadataProvider = 'tmdb';
|
||||
@@ -16144,7 +16304,6 @@ class PipelineService extends EventEmitter {
|
||||
effectiveProviderId = null;
|
||||
effectiveTmdbId = null;
|
||||
effectiveMetadataKind = 'movie';
|
||||
effectiveDiscNumber = null;
|
||||
effectiveSeasonNumber = null;
|
||||
effectiveSeasonName = null;
|
||||
effectiveEpisodeCount = 0;
|
||||
@@ -16271,6 +16430,364 @@ class PipelineService extends EventEmitter {
|
||||
...(tmdbDetails && typeof tmdbDetails === 'object' ? { tmdbDetails } : {})
|
||||
};
|
||||
|
||||
const isDiscFilmMetadataSelection = isSeriesDiscMediaProfile(mediaProfile) && effectiveWorkflowKind === 'film';
|
||||
const filmMetadataFingerprint = isDiscFilmMetadataSelection
|
||||
? buildMovieMetadataFingerprint(mediaProfile, {
|
||||
imdbId: effectiveImdbId,
|
||||
title: effectiveTitle,
|
||||
year: effectiveYear
|
||||
})
|
||||
: null;
|
||||
let multipartContainerJobId = null;
|
||||
let multipartExistingJobId = null;
|
||||
let multipartCurrentDiscNumber = null;
|
||||
let multipartExistingDiscNumber = null;
|
||||
let shouldDetachMultipartContainer = false;
|
||||
|
||||
if (isDiscFilmMetadataSelection) {
|
||||
const currentParentJobId = normalizePositiveInteger(job.parent_job_id || null);
|
||||
if (currentParentJobId) {
|
||||
const currentParentJob = await historyService.getJobById(currentParentJobId).catch(() => null);
|
||||
if (String(currentParentJob?.job_kind || '').trim().toLowerCase() === 'multipart_movie_container') {
|
||||
shouldDetachMultipartContainer = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (filmMetadataFingerprint) {
|
||||
const db = await getDb();
|
||||
let duplicateRows = await db.all(
|
||||
`
|
||||
SELECT *
|
||||
FROM jobs
|
||||
WHERE id != ?
|
||||
AND media_type = ?
|
||||
AND metadata_fingerprint = ?
|
||||
AND COALESCE(job_kind, '') NOT IN ('dvd_series_container', 'multipart_movie_container')
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 25
|
||||
`,
|
||||
[Number(jobId), mediaProfile, filmMetadataFingerprint]
|
||||
);
|
||||
if ((!Array.isArray(duplicateRows) || duplicateRows.length === 0) && effectiveImdbId) {
|
||||
duplicateRows = await db.all(
|
||||
`
|
||||
SELECT *
|
||||
FROM jobs
|
||||
WHERE id != ?
|
||||
AND media_type = ?
|
||||
AND LOWER(TRIM(COALESCE(imdb_id, ''))) = LOWER(TRIM(?))
|
||||
AND COALESCE(job_kind, '') NOT IN ('dvd_series_container', 'multipart_movie_container')
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 25
|
||||
`,
|
||||
[Number(jobId), mediaProfile, String(effectiveImdbId || '').trim()]
|
||||
);
|
||||
}
|
||||
if ((!Array.isArray(duplicateRows) || duplicateRows.length === 0) && effectiveTitle) {
|
||||
if (effectiveYear) {
|
||||
duplicateRows = await db.all(
|
||||
`
|
||||
SELECT *
|
||||
FROM jobs
|
||||
WHERE id != ?
|
||||
AND media_type = ?
|
||||
AND LOWER(TRIM(COALESCE(title, ''))) = LOWER(TRIM(?))
|
||||
AND CAST(COALESCE(year, 0) AS INTEGER) = ?
|
||||
AND COALESCE(job_kind, '') NOT IN ('dvd_series_container', 'multipart_movie_container')
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 25
|
||||
`,
|
||||
[Number(jobId), mediaProfile, String(effectiveTitle || '').trim(), Number(effectiveYear)]
|
||||
);
|
||||
} else {
|
||||
duplicateRows = await db.all(
|
||||
`
|
||||
SELECT *
|
||||
FROM jobs
|
||||
WHERE id != ?
|
||||
AND media_type = ?
|
||||
AND LOWER(TRIM(COALESCE(title, ''))) = LOWER(TRIM(?))
|
||||
AND COALESCE(job_kind, '') NOT IN ('dvd_series_container', 'multipart_movie_container')
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 25
|
||||
`,
|
||||
[Number(jobId), mediaProfile, String(effectiveTitle || '').trim()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateCandidates = Array.isArray(duplicateRows) ? duplicateRows : [];
|
||||
const firstDuplicate = duplicateCandidates[0] || null;
|
||||
const isMultipartAction = normalizedDuplicateAction === 'multipart_movie';
|
||||
const allowDuplicate = normalizedDuplicateAction === 'allow_new';
|
||||
|
||||
let resolvedExistingJob = null;
|
||||
if (requestedExistingJobId && requestedExistingJobId !== Number(jobId)) {
|
||||
resolvedExistingJob = await historyService.getJobById(requestedExistingJobId).catch(() => null);
|
||||
} else if (firstDuplicate) {
|
||||
resolvedExistingJob = await historyService.getJobById(firstDuplicate.id).catch(() => null);
|
||||
}
|
||||
|
||||
const throwDuplicateConflict = (existingJob) => {
|
||||
const existingDisc = resolveDiscNumberFromJobLike(existingJob);
|
||||
const error = new Error('Metadaten bereits in der Historie gefunden. Bitte Auswahl übernehmen oder Multipart movie wählen.');
|
||||
error.statusCode = 409;
|
||||
error.details = [
|
||||
{
|
||||
code: 'METADATA_DUPLICATE_FOUND',
|
||||
fingerprint: filmMetadataFingerprint,
|
||||
mediaProfile,
|
||||
existingJob: existingJob
|
||||
? {
|
||||
id: Number(existingJob.id || 0) || null,
|
||||
title: String(existingJob.title || existingJob.detected_title || '').trim() || null,
|
||||
year: Number(existingJob.year || 0) || null,
|
||||
status: String(existingJob.status || '').trim() || null,
|
||||
lastState: String(existingJob.last_state || '').trim() || null,
|
||||
mediaType: String(existingJob.media_type || existingJob.mediaType || '').trim().toLowerCase() || null,
|
||||
jobKind: String(existingJob.job_kind || existingJob.jobKind || '').trim().toLowerCase() || null,
|
||||
discNumber: existingDisc,
|
||||
isMultipartMovie: Number(existingJob.is_multipart_movie || 0) === 1
|
||||
}
|
||||
: null
|
||||
}
|
||||
];
|
||||
throw error;
|
||||
};
|
||||
|
||||
if (resolvedExistingJob && !allowDuplicate && !isMultipartAction) {
|
||||
throwDuplicateConflict(resolvedExistingJob);
|
||||
}
|
||||
|
||||
if (isMultipartAction) {
|
||||
if (!resolvedExistingJob || Number(resolvedExistingJob?.id || 0) === Number(jobId)) {
|
||||
throwDuplicateConflict(resolvedExistingJob || firstDuplicate);
|
||||
}
|
||||
|
||||
const existingMediaProfile = normalizeMediaProfile(resolvedExistingJob?.media_type || resolvedExistingJob?.mediaType || null);
|
||||
if (existingMediaProfile !== normalizeMediaProfile(mediaProfile)) {
|
||||
const error = new Error('Multipart movie benötigt denselben Medientyp (DVD oder Blu-ray).');
|
||||
error.statusCode = 409;
|
||||
error.details = [{ code: 'MULTIPART_MEDIA_MISMATCH' }];
|
||||
throw error;
|
||||
}
|
||||
|
||||
const existingMkInfo = this.safeParseJson(resolvedExistingJob?.makemkv_info_json)
|
||||
|| (resolvedExistingJob?.makemkvInfo && typeof resolvedExistingJob.makemkvInfo === 'object'
|
||||
? resolvedExistingJob.makemkvInfo
|
||||
: {});
|
||||
const existingAnalyzeContext = existingMkInfo?.analyzeContext && typeof existingMkInfo.analyzeContext === 'object'
|
||||
? existingMkInfo.analyzeContext
|
||||
: {};
|
||||
const existingSelectedMetadata = resolveSelectedMetadataForJob(
|
||||
resolvedExistingJob,
|
||||
existingAnalyzeContext,
|
||||
null
|
||||
);
|
||||
const existingIsSeries = isSeriesDvdMetadataSelection(existingMediaProfile, existingSelectedMetadata, existingAnalyzeContext);
|
||||
if (existingIsSeries) {
|
||||
const error = new Error('Multipart movie ist nur für Film-Jobs erlaubt.');
|
||||
error.statusCode = 409;
|
||||
error.details = [{ code: 'MULTIPART_SERIES_NOT_ALLOWED' }];
|
||||
throw error;
|
||||
}
|
||||
|
||||
const existingFingerprint = buildMovieMetadataFingerprint(existingMediaProfile, {
|
||||
imdbId: existingSelectedMetadata?.imdbId || resolvedExistingJob?.imdb_id || null,
|
||||
title: existingSelectedMetadata?.title || resolvedExistingJob?.title || resolvedExistingJob?.detected_title || null,
|
||||
year: existingSelectedMetadata?.year || resolvedExistingJob?.year || null
|
||||
});
|
||||
if (existingFingerprint && existingFingerprint !== filmMetadataFingerprint) {
|
||||
const error = new Error('Ausgewählter bestehender Job passt nicht zu den aktuellen Film-Metadaten.');
|
||||
error.statusCode = 409;
|
||||
error.details = [{ code: 'MULTIPART_METADATA_MISMATCH' }];
|
||||
throw error;
|
||||
}
|
||||
|
||||
multipartCurrentDiscNumber = normalizePositiveInteger(effectiveDiscNumber);
|
||||
multipartExistingDiscNumber = requestedExistingDiscNumber || resolveDiscNumberFromJobLike(resolvedExistingJob);
|
||||
if (!multipartCurrentDiscNumber || !multipartExistingDiscNumber) {
|
||||
const error = new Error('Für Multipart movie sind Disc-Nummern für beide Jobs erforderlich (>= 1).');
|
||||
error.statusCode = 400;
|
||||
error.details = [{ code: 'MULTIPART_DISC_REQUIRED' }];
|
||||
throw error;
|
||||
}
|
||||
if (multipartCurrentDiscNumber === multipartExistingDiscNumber) {
|
||||
const error = new Error('Disc-Nummern im Multipart-Container müssen eindeutig sein.');
|
||||
error.statusCode = 409;
|
||||
error.details = [{ code: 'MULTIPART_DISC_ALREADY_EXISTS', discNumber: multipartCurrentDiscNumber }];
|
||||
throw error;
|
||||
}
|
||||
|
||||
const loadMultipartContainerById = async (candidateId) => {
|
||||
const normalizedCandidateId = normalizePositiveInteger(candidateId);
|
||||
if (!normalizedCandidateId) {
|
||||
return null;
|
||||
}
|
||||
const row = await historyService.getJobById(normalizedCandidateId).catch(() => null);
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return String(row?.job_kind || '').trim().toLowerCase() === 'multipart_movie_container'
|
||||
? row
|
||||
: null;
|
||||
};
|
||||
|
||||
let multipartContainerJob = null;
|
||||
multipartContainerJob = await loadMultipartContainerById(job.parent_job_id);
|
||||
if (!multipartContainerJob) {
|
||||
multipartContainerJob = await loadMultipartContainerById(resolvedExistingJob?.parent_job_id);
|
||||
}
|
||||
if (!multipartContainerJob) {
|
||||
const existingContainerByFingerprint = await db.get(
|
||||
`
|
||||
SELECT *
|
||||
FROM jobs
|
||||
WHERE job_kind = 'multipart_movie_container'
|
||||
AND media_type = ?
|
||||
AND metadata_fingerprint = ?
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
[mediaProfile, filmMetadataFingerprint]
|
||||
);
|
||||
if (existingContainerByFingerprint?.id) {
|
||||
multipartContainerJob = await historyService.getJobById(existingContainerByFingerprint.id).catch(() => null);
|
||||
}
|
||||
}
|
||||
if (!multipartContainerJob) {
|
||||
multipartContainerJob = await historyService.createJob({
|
||||
discDevice: job.disc_device || resolvedExistingJob?.disc_device || null,
|
||||
status: job.status || 'METADATA_SELECTION',
|
||||
detectedTitle: effectiveTitle || job.detected_title || null,
|
||||
mediaType: mediaProfile,
|
||||
jobKind: 'multipart_movie_container'
|
||||
});
|
||||
}
|
||||
|
||||
multipartContainerJobId = normalizePositiveInteger(multipartContainerJob?.id);
|
||||
if (!multipartContainerJobId) {
|
||||
const error = new Error('Multipart-Container konnte nicht angelegt werden.');
|
||||
error.statusCode = 500;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const containerChildren = await historyService.listChildJobs(multipartContainerJobId);
|
||||
const conflictingDiscChild = (Array.isArray(containerChildren) ? containerChildren : []).find((child) => {
|
||||
const childId = normalizePositiveInteger(child?.id);
|
||||
if (!childId || childId === Number(jobId) || childId === Number(resolvedExistingJob?.id || 0)) {
|
||||
return false;
|
||||
}
|
||||
const childDisc = resolveDiscNumberFromJobLike(child);
|
||||
return childDisc === multipartCurrentDiscNumber || childDisc === multipartExistingDiscNumber;
|
||||
});
|
||||
if (conflictingDiscChild) {
|
||||
const conflictDisc = resolveDiscNumberFromJobLike(conflictingDiscChild);
|
||||
const error = new Error(`Disc ${conflictDisc || '?'} ist im Multipart-Container bereits vergeben.`);
|
||||
error.statusCode = 409;
|
||||
error.details = [
|
||||
{
|
||||
code: 'MULTIPART_DISC_ALREADY_EXISTS',
|
||||
containerJobId: multipartContainerJobId,
|
||||
existingJobId: Number(conflictingDiscChild?.id || 0) || null,
|
||||
discNumber: conflictDisc || null
|
||||
}
|
||||
];
|
||||
throw error;
|
||||
}
|
||||
|
||||
multipartExistingJobId = normalizePositiveInteger(resolvedExistingJob?.id);
|
||||
const patchedExistingSelectedMetadata = {
|
||||
...existingSelectedMetadata,
|
||||
workflowKind: 'film',
|
||||
metadataProvider: 'omdb',
|
||||
metadataKind: 'movie',
|
||||
providerId: null,
|
||||
tmdbId: null,
|
||||
seasonNumber: null,
|
||||
seasonName: null,
|
||||
episodeCount: 0,
|
||||
episodes: [],
|
||||
discNumber: multipartExistingDiscNumber
|
||||
};
|
||||
const existingAnalyzeSelectedMetadata = existingAnalyzeContext?.selectedMetadata && typeof existingAnalyzeContext.selectedMetadata === 'object'
|
||||
? existingAnalyzeContext.selectedMetadata
|
||||
: {};
|
||||
const existingTopLevelSelectedMetadata = existingMkInfo?.selectedMetadata && typeof existingMkInfo.selectedMetadata === 'object'
|
||||
? existingMkInfo.selectedMetadata
|
||||
: {};
|
||||
const patchedExistingMkInfo = this.withAnalyzeContextMediaProfile({
|
||||
...(existingMkInfo && typeof existingMkInfo === 'object' ? existingMkInfo : {}),
|
||||
selectedMetadata: {
|
||||
...existingTopLevelSelectedMetadata,
|
||||
...patchedExistingSelectedMetadata
|
||||
},
|
||||
analyzeContext: {
|
||||
...existingAnalyzeContext,
|
||||
metadataProvider: 'omdb',
|
||||
workflowKind: 'film',
|
||||
selectedMetadata: {
|
||||
...existingAnalyzeSelectedMetadata,
|
||||
...patchedExistingSelectedMetadata
|
||||
}
|
||||
}
|
||||
}, mediaProfile);
|
||||
|
||||
if (multipartExistingJobId) {
|
||||
await historyService.updateJob(multipartExistingJobId, {
|
||||
parent_job_id: multipartContainerJobId,
|
||||
media_type: mediaProfile,
|
||||
job_kind: 'multipart_movie_child',
|
||||
is_multipart_movie: 1,
|
||||
disc_number: multipartExistingDiscNumber,
|
||||
metadata_fingerprint: filmMetadataFingerprint,
|
||||
makemkv_info_json: JSON.stringify(patchedExistingMkInfo)
|
||||
});
|
||||
}
|
||||
|
||||
const containerSelectedMetadataPatch = {
|
||||
...selectedMetadata,
|
||||
workflowKind: 'film',
|
||||
metadataProvider: 'omdb',
|
||||
metadataKind: 'movie',
|
||||
providerId: null,
|
||||
tmdbId: null,
|
||||
seasonNumber: null,
|
||||
seasonName: null,
|
||||
episodeCount: 0,
|
||||
episodes: [],
|
||||
discNumber: null
|
||||
};
|
||||
const containerMkInfo = this.withAnalyzeContextMediaProfile({
|
||||
analyzeContext: {
|
||||
metadataProvider: 'omdb',
|
||||
workflowKind: 'film',
|
||||
selectedMetadata: containerSelectedMetadataPatch
|
||||
},
|
||||
selectedMetadata: containerSelectedMetadataPatch
|
||||
}, mediaProfile);
|
||||
await historyService.updateJob(multipartContainerJobId, {
|
||||
title: effectiveTitle,
|
||||
year: effectiveYear,
|
||||
imdb_id: effectiveImdbId,
|
||||
poster_url: posterValue,
|
||||
selected_from_omdb: selectedFromOmdb,
|
||||
omdb_json: omdbJsonValue,
|
||||
status: job.status || 'METADATA_SELECTION',
|
||||
last_state: job.status || 'METADATA_SELECTION',
|
||||
media_type: mediaProfile,
|
||||
job_kind: 'multipart_movie_container',
|
||||
is_multipart_movie: 1,
|
||||
disc_number: null,
|
||||
metadata_fingerprint: filmMetadataFingerprint,
|
||||
makemkv_info_json: JSON.stringify(containerMkInfo)
|
||||
});
|
||||
|
||||
shouldDetachMultipartContainer = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldDetachSeriesContainer = isSeriesDiscMediaProfile(mediaProfile) && effectiveWorkflowKind === 'film';
|
||||
let parentContainerJobId = shouldDetachSeriesContainer
|
||||
? null
|
||||
@@ -16574,13 +17091,42 @@ class PipelineService extends EventEmitter {
|
||||
}
|
||||
}, mediaProfile);
|
||||
|
||||
const seriesDetachPatch = shouldDetachSeriesContainer
|
||||
const seriesDetachPatch = (shouldDetachSeriesContainer && !multipartContainerJobId)
|
||||
? {
|
||||
parent_job_id: null,
|
||||
job_kind: resolveJobKindForMediaProfile(mediaProfile),
|
||||
media_type: mediaProfile
|
||||
}
|
||||
: {};
|
||||
const multipartAttachPatch = multipartContainerJobId
|
||||
? {
|
||||
parent_job_id: multipartContainerJobId,
|
||||
job_kind: 'multipart_movie_child',
|
||||
media_type: mediaProfile,
|
||||
is_multipart_movie: 1,
|
||||
disc_number: multipartCurrentDiscNumber || normalizePositiveInteger(effectiveDiscNumber),
|
||||
metadata_fingerprint: filmMetadataFingerprint || null
|
||||
}
|
||||
: {};
|
||||
const multipartDetachPatch = (shouldDetachMultipartContainer && !multipartContainerJobId)
|
||||
? {
|
||||
parent_job_id: null,
|
||||
job_kind: resolveJobKindForMediaProfile(mediaProfile),
|
||||
media_type: mediaProfile,
|
||||
is_multipart_movie: 0
|
||||
}
|
||||
: {};
|
||||
const filmMetadataPatch = isDiscFilmMetadataSelection
|
||||
? {
|
||||
metadata_fingerprint: filmMetadataFingerprint || null,
|
||||
disc_number: multipartContainerJobId
|
||||
? (multipartCurrentDiscNumber || normalizePositiveInteger(effectiveDiscNumber))
|
||||
: (normalizePositiveInteger(effectiveDiscNumber) || null),
|
||||
...(multipartContainerJobId
|
||||
? { is_multipart_movie: 1 }
|
||||
: (shouldDetachMultipartContainer ? { is_multipart_movie: 0 } : {}))
|
||||
}
|
||||
: {};
|
||||
await historyService.updateJob(jobId, {
|
||||
title: effectiveTitle,
|
||||
year: effectiveYear,
|
||||
@@ -16593,9 +17139,27 @@ class PipelineService extends EventEmitter {
|
||||
raw_path: updatedRawPath,
|
||||
makemkv_info_json: JSON.stringify(updatedMakemkvInfo),
|
||||
...(parentContainerJobId ? { parent_job_id: parentContainerJobId, job_kind: 'dvd_series_child', media_type: mediaProfile } : {}),
|
||||
...multipartAttachPatch,
|
||||
...multipartDetachPatch,
|
||||
...filmMetadataPatch,
|
||||
...seriesDetachPatch
|
||||
});
|
||||
|
||||
if (multipartContainerJobId) {
|
||||
await historyService.appendLog(
|
||||
jobId,
|
||||
'SYSTEM',
|
||||
`Multipart movie aktiviert: Container #${multipartContainerJobId}, Disc ${multipartCurrentDiscNumber || '-'}`
|
||||
);
|
||||
if (multipartExistingJobId && Number(multipartExistingJobId) !== Number(jobId)) {
|
||||
await historyService.appendLog(
|
||||
multipartExistingJobId,
|
||||
'SYSTEM',
|
||||
`Multipart movie aktiviert: Container #${multipartContainerJobId}, Disc ${multipartExistingDiscNumber || '-'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Bild in Cache laden (async, blockiert nicht)
|
||||
if (posterValue && !thumbnailService.isLocalUrl(posterValue)) {
|
||||
historyService.queuePosterCache(jobId, posterValue, {
|
||||
@@ -20746,7 +21310,8 @@ class PipelineService extends EventEmitter {
|
||||
};
|
||||
const postEncodeScriptsSummary = this.buildPostEncodeScriptsSummaryPlaceholder(encodePlan);
|
||||
let finalizedRawPath = activeRawPath || null;
|
||||
if (activeRawPath) {
|
||||
const deferRawFinalizeUntilSeriesBatchDone = isSeriesBatchEpisodeRun || Boolean(seriesBatchParentJobId);
|
||||
if (activeRawPath && !deferRawFinalizeUntilSeriesBatchDone) {
|
||||
const currentRawPath = String(activeRawPath || '').trim();
|
||||
const completedRawPath = buildCompletedRawPath(currentRawPath);
|
||||
if (completedRawPath && completedRawPath !== currentRawPath) {
|
||||
|
||||
@@ -51,6 +51,9 @@ CREATE TABLE jobs (
|
||||
aax_checksum TEXT,
|
||||
media_type TEXT,
|
||||
job_kind TEXT,
|
||||
is_multipart_movie INTEGER NOT NULL DEFAULT 0,
|
||||
disc_number INTEGER,
|
||||
metadata_fingerprint TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_job_id) REFERENCES jobs(id) ON DELETE SET NULL
|
||||
@@ -60,6 +63,8 @@ CREATE INDEX idx_jobs_status ON jobs(status);
|
||||
CREATE INDEX idx_jobs_created_at ON jobs(created_at DESC);
|
||||
CREATE INDEX idx_jobs_parent_job_id ON jobs(parent_job_id);
|
||||
CREATE INDEX idx_jobs_job_kind ON jobs(job_kind);
|
||||
CREATE INDEX idx_jobs_disc_number ON jobs(disc_number);
|
||||
CREATE INDEX idx_jobs_metadata_fingerprint ON jobs(metadata_fingerprint);
|
||||
|
||||
CREATE TABLE job_lineage_artifacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
+75
-67
@@ -1,27 +1,31 @@
|
||||
# History API
|
||||
|
||||
Endpunkte für Job-Historie, Orphan-Import und Löschoperationen.
|
||||
Endpunkte für Job-Historie, Metadaten-Nachpflege, Orphan-Import und Löschoperationen.
|
||||
|
||||
---
|
||||
|
||||
## GET /api/history
|
||||
|
||||
Liefert Jobs (optionale Filter).
|
||||
Liefert Jobs mit optionalen Filtern.
|
||||
|
||||
**Query-Parameter:**
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
|----------|-----|-------------|
|
||||
| `status` | string | Filter nach Job-Status |
|
||||
| `search` | string | Suche in Titel-Feldern |
|
||||
| `status` | string | Einzelstatus-Filter (legacy-kompatibel). |
|
||||
| `statuses` | string | Mehrfachstatus als CSV, z. B. `FINISHED,ERROR`. |
|
||||
| `search` | string | Suche in Titel-/IMDb-Feldern. |
|
||||
| `limit` | number | Maximale Anzahl Ergebnisse. |
|
||||
| `lite` | bool | Ohne Dateisystem-Checks (schneller). |
|
||||
| `includeChildren` | bool | Child-Jobs (z. B. Serien-/Multipart-Kinder) explizit einbeziehen. |
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
```text
|
||||
GET /api/history?status=FINISHED&search=Inception
|
||||
GET /api/history?statuses=FINISHED,ERROR&search=Inception&limit=50&lite=1
|
||||
```
|
||||
|
||||
**Response:**
|
||||
**Response (Beispiel):**
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -30,6 +34,7 @@ GET /api/history?status=FINISHED&search=Inception
|
||||
"id": 42,
|
||||
"status": "FINISHED",
|
||||
"title": "Inception",
|
||||
"job_kind": "bluray",
|
||||
"raw_path": "/mnt/raw/Inception - RAW - job-42",
|
||||
"output_path": "/mnt/movies/Inception (2010)/Inception (2010).mkv",
|
||||
"mediaType": "bluray",
|
||||
@@ -52,12 +57,13 @@ Liefert Job-Detail.
|
||||
|
||||
| Parameter | Typ | Standard | Beschreibung |
|
||||
|----------|-----|---------|-------------|
|
||||
| `includeLogs` | bool | `false` | Prozesslog laden |
|
||||
| `includeLiveLog` | bool | `false` | alias-artig ebenfalls Prozesslog laden |
|
||||
| `includeAllLogs` | bool | `false` | vollständiges Log statt Tail |
|
||||
| `logTailLines` | number | `800` | Tail-Länge falls nicht `includeAllLogs` |
|
||||
| `includeLogs` | bool | `false` | Prozesslog laden. |
|
||||
| `includeLiveLog` | bool | `false` | Live-/Tail-Log laden. |
|
||||
| `includeAllLogs` | bool | `false` | vollständiges Log statt Tail. |
|
||||
| `logTailLines` | number | `800` | Tail-Länge falls nicht `includeAllLogs`. |
|
||||
| `lite` | bool | `false` | Detailantwort ohne FS-Checks. |
|
||||
|
||||
**Response:**
|
||||
**Response (Beispiel):**
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -86,33 +92,9 @@ Liefert Job-Detail.
|
||||
|
||||
Sucht RAW-Ordner ohne zugehörigen Job.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"rawDir": "/mnt/raw",
|
||||
"rawDirs": ["/mnt/raw", "/mnt/raw-bluray"],
|
||||
"rows": [
|
||||
{
|
||||
"rawPath": "/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99",
|
||||
"folderName": "Inception (2010) [tt1375666] - RAW - job-99",
|
||||
"title": "Inception",
|
||||
"year": 2010,
|
||||
"imdbId": "tt1375666",
|
||||
"folderJobId": 99,
|
||||
"entryCount": 4,
|
||||
"hasBlurayStructure": true,
|
||||
"lastModifiedAt": "2026-03-10T09:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/history/orphan-raw/import
|
||||
|
||||
Importiert RAW-Ordner als FINISHED-Job.
|
||||
Importiert einen RAW-Ordner als Job und triggert optional die Analyse des Imports.
|
||||
|
||||
**Request:**
|
||||
|
||||
@@ -120,12 +102,13 @@ Importiert RAW-Ordner als FINISHED-Job.
|
||||
{ "rawPath": "/mnt/raw/Inception (2010) [tt1375666] - RAW - job-99" }
|
||||
```
|
||||
|
||||
**Response:**
|
||||
**Response (Beispiel):**
|
||||
|
||||
```json
|
||||
{
|
||||
"job": { "id": 77, "status": "FINISHED" },
|
||||
"uiReset": { "reset": true, "state": "IDLE" }
|
||||
"activation": { "started": true },
|
||||
"activationError": null
|
||||
}
|
||||
```
|
||||
|
||||
@@ -133,26 +116,43 @@ Importiert RAW-Ordner als FINISHED-Job.
|
||||
|
||||
## POST /api/history/:id/omdb/assign
|
||||
|
||||
Weist OMDb-/Metadaten nachträglich zu.
|
||||
Weist OMDb-/Filmdaten nachträglich zu.
|
||||
|
||||
**Request:**
|
||||
## POST /api/history/:id/cd/assign
|
||||
|
||||
Weist CD-Metadaten (z. B. MusicBrainz) nachträglich zu.
|
||||
|
||||
## POST /api/history/:id/error/ack
|
||||
|
||||
Quittiert einen Fehlerzustand im Historieneintrag.
|
||||
|
||||
---
|
||||
|
||||
## GET /api/history/:id/delete-preview
|
||||
|
||||
Liefert eine Vorschau der löschbaren Pfade (inkl. Scope auf verknüpfte Jobs).
|
||||
|
||||
**Query-Parameter:**
|
||||
|
||||
| Parameter | Typ | Standard | Beschreibung |
|
||||
|----------|-----|---------|-------------|
|
||||
| `includeRelated` | bool | `true` | Verknüpfte Jobs in die Vorschau einbeziehen. |
|
||||
|
||||
**Response (Beispiel):**
|
||||
|
||||
```json
|
||||
{
|
||||
"imdbId": "tt1375666",
|
||||
"title": "Inception",
|
||||
"year": 2010,
|
||||
"poster": "https://...",
|
||||
"fromOmdb": true
|
||||
"preview": {
|
||||
"jobId": 42,
|
||||
"relatedJobs": [{ "id": 42 }, { "id": 73 }],
|
||||
"pathCandidates": {
|
||||
"raw": [{ "path": "/mnt/raw/...", "exists": true, "isDirectory": true }],
|
||||
"movie": [{ "path": "/mnt/movies/...", "exists": true, "isDirectory": true }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{ "job": { "id": 42, "imdb_id": "tt1375666" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/history/:id/delete-files
|
||||
@@ -162,23 +162,17 @@ Löscht Dateien eines Jobs, behält DB-Eintrag.
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "target": "both" }
|
||||
{
|
||||
"target": "both",
|
||||
"includeRelated": true,
|
||||
"selectedRawPaths": ["/mnt/raw/Inception - RAW - Disc1"],
|
||||
"selectedMoviePaths": ["/mnt/movies/Inception (2010)"]
|
||||
}
|
||||
```
|
||||
|
||||
`target`: `raw` | `movie` | `both`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"target": "both",
|
||||
"raw": { "attempted": true, "deleted": true, "filesDeleted": 12, "dirsRemoved": 3, "reason": null },
|
||||
"movie": { "attempted": true, "deleted": false, "filesDeleted": 0, "dirsRemoved": 0, "reason": "Movie-Datei/Pfad existiert nicht." }
|
||||
},
|
||||
"job": { "id": 42 }
|
||||
}
|
||||
```
|
||||
`selectedRawPaths` / `selectedMoviePaths` sind optional und begrenzen die Löschung auf ausgewählte Pfade aus der Vorschau.
|
||||
|
||||
---
|
||||
|
||||
@@ -189,12 +183,20 @@ Löscht Job aus DB; optional auch Dateien.
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "target": "none" }
|
||||
{
|
||||
"target": "both",
|
||||
"includeRelated": true,
|
||||
"resetDriveState": false,
|
||||
"keepDetectedDevice": true,
|
||||
"preserveRawForImportJobs": false,
|
||||
"selectedRawPaths": ["/mnt/raw/Inception - RAW - Disc1"],
|
||||
"selectedMoviePaths": ["/mnt/movies/Inception (2010)"]
|
||||
}
|
||||
```
|
||||
|
||||
`target`: `none` | `raw` | `movie` | `both`
|
||||
|
||||
**Response:**
|
||||
**Response (Beispiel):**
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -209,6 +211,11 @@ Löscht Job aus DB; optional auch Dateien.
|
||||
"uiReset": {
|
||||
"reset": true,
|
||||
"state": "IDLE"
|
||||
},
|
||||
"safeguards": {
|
||||
"containsOrphanRawImportJob": false,
|
||||
"resetDriveStateApplied": false,
|
||||
"keepDetectedDeviceApplied": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -218,4 +225,5 @@ Löscht Job aus DB; optional auch Dateien.
|
||||
## Hinweise
|
||||
|
||||
- Ein aktiver Pipeline-Job kann nicht gelöscht werden (`409`).
|
||||
- Alle Löschoperationen sind irreversibel.
|
||||
- Löschoperationen sind irreversibel.
|
||||
- Bei Serien-/Multipart-Containern steuern `includeRelated` und Pfadselektionen den genauen Scope.
|
||||
|
||||
+166
-1
@@ -95,6 +95,18 @@ Erzwingt erneute Laufwerksprüfung.
|
||||
|
||||
---
|
||||
|
||||
## POST /api/pipeline/rescan-drive
|
||||
|
||||
Erzwingt Rescan für ein konkretes Laufwerk.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "devicePath": "/dev/sr0" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /api/pipeline/omdb/search?q=<query>
|
||||
|
||||
OMDb-Titelsuche.
|
||||
@@ -117,6 +129,78 @@ OMDb-Titelsuche.
|
||||
|
||||
---
|
||||
|
||||
## GET /api/pipeline/tmdb/series/search?q=<query>&season=<n>
|
||||
|
||||
TMDb-Seriensuche (für Serien-Workflow bei DVD/Blu-ray).
|
||||
|
||||
---
|
||||
|
||||
## GET /api/pipeline/cd/drives
|
||||
|
||||
Liefert Snapshot der aktuell bekannten CD-Laufwerke.
|
||||
|
||||
---
|
||||
|
||||
## GET /api/pipeline/cd/musicbrainz/search?q=<query>
|
||||
|
||||
MusicBrainz-Suche für CD-Metadaten.
|
||||
|
||||
## GET /api/pipeline/cd/musicbrainz/release/:mbId
|
||||
|
||||
Lädt Release-Details zu einer MusicBrainz-ID.
|
||||
|
||||
## POST /api/pipeline/cd/select-metadata
|
||||
|
||||
Übernimmt CD-Metadaten für einen Job.
|
||||
|
||||
**Request (Beispiel):**
|
||||
|
||||
```json
|
||||
{
|
||||
"jobId": 91,
|
||||
"title": "Album",
|
||||
"artist": "Artist",
|
||||
"year": 2020,
|
||||
"mbId": "f5093c06-23e3-404f-aeaa-40f72885ee3a",
|
||||
"coverUrl": "https://...",
|
||||
"tracks": []
|
||||
}
|
||||
```
|
||||
|
||||
## POST /api/pipeline/cd/start/:jobId
|
||||
|
||||
Startet/queued CD-Rip mit Format-/Script-/Chain-Konfiguration.
|
||||
|
||||
---
|
||||
|
||||
## POST /api/pipeline/audiobook/upload
|
||||
|
||||
Upload von AAX-Datei (Multipart/FormData).
|
||||
|
||||
**FormData-Felder:**
|
||||
|
||||
- `file` (Pflicht)
|
||||
- `format` (optional)
|
||||
- `startImmediately` (optional)
|
||||
|
||||
## GET /api/pipeline/audiobook/pending-activation
|
||||
|
||||
Zeigt AAX-Jobs mit fehlenden Activation-Bytes.
|
||||
|
||||
## POST /api/pipeline/audiobook/start/:jobId
|
||||
|
||||
Startet Audiobook-Job mit optionaler Konfiguration.
|
||||
|
||||
## GET /api/pipeline/audiobook/jobs
|
||||
|
||||
Liefert Audiobook-Jobliste.
|
||||
|
||||
## GET /api/pipeline/audiobook/output-tree
|
||||
|
||||
Read-only Baumansicht des Audiobook-Ausgabeordners.
|
||||
|
||||
---
|
||||
|
||||
## POST /api/pipeline/select-metadata
|
||||
|
||||
Setzt Metadaten (und optional Playlist) für einen Job.
|
||||
@@ -135,12 +219,69 @@ Setzt Metadaten (und optional Playlist) für einen Job.
|
||||
}
|
||||
```
|
||||
|
||||
Wichtige optionale Felder:
|
||||
|
||||
- `selectedHandBrakeTitleId` / `selectedHandBrakeTitleIds`: Titel-Auswahl für Review/MediaInfo.
|
||||
- `metadataProvider`: `omdb` oder `tmdb`.
|
||||
- `workflowKind`: bei Disc-Jobs typischerweise `film` oder `series`.
|
||||
- `discNumber`: Pflicht für Serien-Disc-Zuordnung (`workflowKind=series`).
|
||||
- `duplicateAction`: Duplikatverhalten bei Film-Metadaten (`allow_new` oder `multipart_movie`).
|
||||
- `existingJobId`: optionaler Referenzjob für `multipart_movie`.
|
||||
- `existingDiscNumber`: Disc-Nummer des bestehenden Jobs für `multipart_movie`.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{ "job": { "id": 42, "status": "READY_TO_START" } }
|
||||
```
|
||||
|
||||
**Konflikt-Response (Beispiel, HTTP 409):**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Metadaten bereits in der Historie gefunden. Bitte Auswahl übernehmen oder Multipart movie wählen.",
|
||||
"details": [
|
||||
{
|
||||
"code": "METADATA_DUPLICATE_FOUND",
|
||||
"mediaProfile": "bluray",
|
||||
"existingJob": {
|
||||
"id": 17,
|
||||
"title": "Inception",
|
||||
"year": 2010,
|
||||
"status": "FINISHED",
|
||||
"jobKind": "bluray",
|
||||
"discNumber": 1,
|
||||
"isMultipartMovie": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Relevante Fehlercodes (`POST /api/pipeline/select-metadata`):**
|
||||
|
||||
| Code | Bedeutung |
|
||||
|---|---|
|
||||
| `METADATA_DUPLICATE_FOUND` | Film-Metadaten existieren bereits in der Historie (Konfliktdialog im UI). |
|
||||
| `MULTIPART_DISC_REQUIRED` | Für Multipart fehlen Disc-Nummern (`discNumber`/`existingDiscNumber`). |
|
||||
| `MULTIPART_DISC_ALREADY_EXISTS` | Disc-Nummer im Multipart-Container bereits vergeben oder doppelt. |
|
||||
| `MULTIPART_MEDIA_MISMATCH` | Multipart nur bei gleichem Medientyp (DVD oder Blu-ray). |
|
||||
| `MULTIPART_SERIES_NOT_ALLOWED` | Multipart ist nur für Film-Jobs erlaubt, nicht für Serien-Workflow. |
|
||||
| `MULTIPART_METADATA_MISMATCH` | Ausgewählter bestehender Job passt nicht zu den Film-Metadaten. |
|
||||
| `SERIES_DISC_ALREADY_EXISTS` | Serien-Disc-Nummer innerhalb einer Staffel bereits vorhanden. |
|
||||
|
||||
---
|
||||
|
||||
## POST /api/pipeline/jobs/:jobId/raw-decision
|
||||
|
||||
Trifft Entscheidung bei vorhandenem RAW (`continue` oder `restart` je nach Dialogfluss).
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "decision": "continue" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/pipeline/start/:jobId
|
||||
@@ -233,10 +374,28 @@ Berechnet Review aus RAW neu.
|
||||
|
||||
Startet Encoding mit letzter bestätigter Review neu.
|
||||
|
||||
## POST /api/pipeline/restart-cd-review/:jobId
|
||||
|
||||
Berechnet CD-Review aus vorhandenem RAW neu.
|
||||
|
||||
## POST /api/pipeline/resume-ready/:jobId
|
||||
|
||||
Lädt `READY_TO_ENCODE`-Job nach Neustart wieder in aktive Session.
|
||||
|
||||
## GET /api/pipeline/output-folders/:jobId
|
||||
|
||||
Liefert bekannte Output-Ordner entlang der Job-Lineage.
|
||||
|
||||
## POST /api/pipeline/delete-output-folders/:jobId
|
||||
|
||||
Löscht ausgewählte Output-Ordner.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "folderPaths": ["/mnt/movies/Inception (2010)"] }
|
||||
```
|
||||
|
||||
Alle Endpunkte liefern `{ result: ... }` bzw. `{ job: ... }`.
|
||||
|
||||
---
|
||||
@@ -358,12 +517,18 @@ Entfernt Queue-Eintrag.
|
||||
| `DISC_DETECTED` | Medium erkannt |
|
||||
| `ANALYZING` | MakeMKV-Analyse läuft |
|
||||
| `METADATA_SELECTION` | Metadaten-Auswahl |
|
||||
| `WAITING_FOR_USER_DECISION` | Playlist-Entscheidung erforderlich |
|
||||
| `WAITING_FOR_USER_DECISION` | Nutzerentscheidung erforderlich (Playlist-Auswahl oder RAW-Entscheidung) |
|
||||
| `READY_TO_START` | Übergang vor Start |
|
||||
| `RIPPING` | MakeMKV-Rip läuft |
|
||||
| `MEDIAINFO_CHECK` | Titel-/Track-Auswertung |
|
||||
| `READY_TO_ENCODE` | Review bereit |
|
||||
| `ENCODING` | HandBrake-Encoding läuft |
|
||||
| `CD_METADATA_SELECTION` | CD-Metadatenauswahl aktiv |
|
||||
| `CD_READY_TO_RIP` | CD-Job ist startbereit |
|
||||
| `CD_ANALYZING` | CD-Struktur/Tracks werden vorbereitet |
|
||||
| `CD_RIPPING` | CD-Ripping läuft |
|
||||
| `CD_ENCODING` | CD-Encode läuft |
|
||||
| `FINISHED` | Abgeschlossen |
|
||||
| `DONE` | Abgeschlossen (v. a. Audiobook/Converter/CD-Varianten) |
|
||||
| `CANCELLED` | Abgebrochen |
|
||||
| `ERROR` | Fehler |
|
||||
|
||||
@@ -105,7 +105,7 @@ Alle Aktivitäts-Endpunkte geben einen Snapshot zurück:
|
||||
|
||||
## Endpunkte
|
||||
|
||||
### GET `/api/activities`
|
||||
### GET `/api/runtime/activities`
|
||||
|
||||
Aktuellen Aktivitäts-Snapshot abrufen.
|
||||
|
||||
@@ -136,7 +136,7 @@ Aktuellen Aktivitäts-Snapshot abrufen.
|
||||
|
||||
---
|
||||
|
||||
### POST `/api/activities/:id/cancel`
|
||||
### POST `/api/runtime/activities/:id/cancel`
|
||||
|
||||
Aktive Aktivität abbrechen (nur wenn `canCancel: true`).
|
||||
|
||||
@@ -172,7 +172,7 @@ Aktive Aktivität abbrechen (nur wenn `canCancel: true`).
|
||||
|
||||
---
|
||||
|
||||
### POST `/api/activities/:id/next-step`
|
||||
### POST `/api/runtime/activities/:id/next-step`
|
||||
|
||||
Nächsten Schritt einer Aktivität auslösen (nur wenn `canNextStep: true`).
|
||||
|
||||
@@ -201,7 +201,7 @@ Nächsten Schritt einer Aktivität auslösen (nur wenn `canNextStep: true`).
|
||||
|
||||
---
|
||||
|
||||
### POST `/api/activities/clear-recent`
|
||||
### POST `/api/runtime/activities/clear-recent`
|
||||
|
||||
Alle abgeschlossenen Aktivitäten aus `recent` löschen.
|
||||
|
||||
@@ -232,4 +232,4 @@ Gekürzte Ausgaben erhalten den Suffix ` ...[gekürzt]` (bei Inline-Text) bzw. `
|
||||
|
||||
## Echtzeit-Updates
|
||||
|
||||
Änderungen werden automatisch als [`RUNTIME_ACTIVITY_CHANGED`](websocket.md#runtime_activity_changed) WebSocket-Event gesendet. Die Frontend-Komponente braucht `GET /api/activities` nur beim initialen Laden aufzurufen.
|
||||
Änderungen werden automatisch als [`RUNTIME_ACTIVITY_CHANGED`](websocket.md#runtime_activity_changed) WebSocket-Event gesendet. Die Frontend-Komponente braucht `GET /api/runtime/activities` nur beim initialen Laden aufzurufen.
|
||||
|
||||
@@ -336,3 +336,89 @@ Body mit beliebigen Feldern aus `POST`, Response `{ "preset": { ... } }`.
|
||||
### DELETE /api/settings/user-presets/:id
|
||||
|
||||
Response `{ "removed": { ... } }`.
|
||||
|
||||
---
|
||||
|
||||
## Weitere Endpunkte
|
||||
|
||||
### GET /api/settings/effective-paths
|
||||
|
||||
Liefert aufgelöste, effektiv genutzte Pfade aus den Settings (für UI- und Diagnosezwecke).
|
||||
|
||||
### POST /api/settings/coverart/recover
|
||||
|
||||
Startet eine manuelle Coverart-Recovery.
|
||||
|
||||
**Response (Beispiel):**
|
||||
|
||||
```json
|
||||
{
|
||||
"result": { "started": true },
|
||||
"scheduler": { "enabled": true }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activation Bytes (AAX)
|
||||
|
||||
### GET /api/settings/activation-bytes
|
||||
|
||||
Liest gecachte AAX-Activation-Bytes.
|
||||
|
||||
### POST /api/settings/activation-bytes
|
||||
|
||||
Speichert Activation-Bytes.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"checksum": "abc123...",
|
||||
"activationBytes": "1a2b3c4d"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Laufwerke
|
||||
|
||||
### GET /api/settings/drives
|
||||
|
||||
Liefert erkannte optische Laufwerke (`/dev/...`) inkl. MakeMKV-Disc-Index.
|
||||
|
||||
### POST /api/settings/drives/force-unlock
|
||||
|
||||
Erzwingt Entsperren aktiver Laufwerkslocks.
|
||||
|
||||
Hinweis: Expertenmodus (`ui_expert_mode`) ist erforderlich, sonst `403`.
|
||||
|
||||
**Request (ein Laufwerk):**
|
||||
|
||||
```json
|
||||
{ "devicePath": "/dev/sr0" }
|
||||
```
|
||||
|
||||
**Request (alle):**
|
||||
|
||||
```json
|
||||
{ "all": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Preferences
|
||||
|
||||
### GET /api/settings/prefs/:key
|
||||
|
||||
Liest einen persistierten UI-Preference-Wert.
|
||||
|
||||
### PUT /api/settings/prefs/:key
|
||||
|
||||
Speichert einen persistierten UI-Preference-Wert.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{ "value": "{\"columns\":[\"id\",\"status\"]}" }
|
||||
```
|
||||
|
||||
@@ -38,9 +38,25 @@ Zentrale Felder:
|
||||
- Tool-Ausgaben: `makemkv_info_json`, `handbrake_info_json`, `mediainfo_info_json`, `encode_plan_json`
|
||||
- Kontrolle: `encode_review_confirmed`, `rip_successful`, `error_message`
|
||||
- Medientyp: `media_type` (`bluray|dvd|cd|audiobook|converter`)
|
||||
- Job-Typ/Beziehung: `job_kind`, `parent_job_id`
|
||||
- Multipart-/Disc-Merkmale: `is_multipart_movie`, `disc_number`, `metadata_fingerprint`
|
||||
- Prüfsumme: `aax_checksum` (für AAX-Dateien)
|
||||
- Audit: `created_at`, `updated_at`
|
||||
|
||||
Typische `job_kind`-Werte:
|
||||
|
||||
- Standard: `bluray`, `dvd`, `cd`, `audiobook`, `converter_audio`, `converter_video`, `converter_iso`
|
||||
- Serien: `dvd_series_container`, `dvd_series_child`
|
||||
- Multipart Film: `multipart_movie_container`, `multipart_movie_child`
|
||||
|
||||
Wichtige Indizes:
|
||||
|
||||
- `idx_jobs_status`
|
||||
- `idx_jobs_parent_job_id`
|
||||
- `idx_jobs_job_kind`
|
||||
- `idx_jobs_disc_number`
|
||||
- `idx_jobs_metadata_fingerprint`
|
||||
|
||||
---
|
||||
|
||||
## `job_lineage_artifacts`
|
||||
@@ -180,6 +196,7 @@ sqlite3 backend/data/ripster.db
|
||||
|
||||
.mode table
|
||||
SELECT id, status, title, media_type, created_at FROM jobs ORDER BY created_at DESC;
|
||||
SELECT id, parent_job_id, job_kind, is_multipart_movie, disc_number FROM jobs ORDER BY created_at DESC;
|
||||
SELECT key, value FROM settings_values ORDER BY key;
|
||||
SELECT checksum, activation_bytes FROM aax_activation_bytes;
|
||||
SELECT rel_path, job_id FROM converter_scan_entries;
|
||||
|
||||
@@ -6,6 +6,14 @@ Frontend: React + PrimeReact + Vite.
|
||||
|
||||
## Hauptseiten
|
||||
|
||||
### `JobsInboxPage.jsx`
|
||||
|
||||
Zentrale Job-Inbox:
|
||||
|
||||
- einheitliche Liste über Ripper-, Converter- und Audiobook-Jobs
|
||||
- View-/Status-Filter, Suche, Paginierung
|
||||
- Schnellsprung in passende Detailseite (`Ripper`/`Converter`)
|
||||
|
||||
### `RipperPage.jsx`
|
||||
|
||||
Pipeline-Steuerung:
|
||||
@@ -49,6 +57,14 @@ Datei-Converter:
|
||||
- Job-Start, Abbruch, Löschung
|
||||
- Automatischer Scan-Status
|
||||
|
||||
### `AudiobooksPage.jsx`
|
||||
|
||||
Dedizierter AAX-/Audiobook-Flow:
|
||||
|
||||
- Upload-Panel für AAX-Dateien
|
||||
- aktive Audiobook-Jobkarten mit Start/Cancel/Retry/Delete
|
||||
- Audiobook-Konfigurationspanel und Output-Explorer
|
||||
|
||||
### `DownloadsPage.jsx`
|
||||
|
||||
Download-Queue:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Audiobooks
|
||||
|
||||
Die Seite `Audiobooks` ist die dedizierte Oberfläche für AAX- und Audiobook-Jobs.
|
||||
|
||||
---
|
||||
|
||||
## Aufbau
|
||||
|
||||
1. `Audiobooks Upload`
|
||||
2. `Audiobook Jobs`
|
||||
3. `Audiobook Output Explorer`
|
||||
|
||||
---
|
||||
|
||||
## 1) Audiobooks Upload
|
||||
|
||||
Hier werden AAX-Dateien hochgeladen und als Job vorbereitet.
|
||||
|
||||
Typische Schritte:
|
||||
|
||||
1. Datei auswählen
|
||||
2. optional Format/Startverhalten setzen
|
||||
3. Upload starten
|
||||
|
||||
Während des Uploads zeigt die Seite Fortschritt und Status an.
|
||||
|
||||
---
|
||||
|
||||
## 2) Audiobook Jobs
|
||||
|
||||
Aktive Audiobook-Jobs werden als Karten dargestellt (mit Status und Fortschritt).
|
||||
|
||||
Pro Job sind je nach Zustand Aktionen verfügbar:
|
||||
|
||||
- Starten (mit aktueller Audiobook-Konfiguration)
|
||||
- Abbrechen
|
||||
- Retry
|
||||
- Löschen
|
||||
|
||||
Zusätzlich:
|
||||
|
||||
- `Im Ripper` springt in die Hauptansicht
|
||||
- Jobs laden periodisch nach und reagieren auf WebSocket-Updates
|
||||
|
||||
---
|
||||
|
||||
## 3) Audiobook Output Explorer
|
||||
|
||||
Read-only Baumansicht des konfigurierten Audiobook-Ausgabeordners:
|
||||
|
||||
- Ordner- und Dateiansicht
|
||||
- Größen-/Zeitstempelübersicht
|
||||
- schnelle Kontrolle der erzeugten Ergebnisse
|
||||
|
||||
---
|
||||
|
||||
## Abgrenzung zum Converter
|
||||
|
||||
Audio/Video-Datei-Konvertierung läuft weiterhin auf der Seite `Converter`.
|
||||
Der AAX-spezifische Flow (Upload, Start, Jobsteuerung, Output-Explorer) liegt auf `Audiobooks`.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Converter
|
||||
|
||||
Die Converter-Seite ermöglicht das Konvertieren vorhandener Audio- und Video-Dateien sowie das Verarbeiten von Audiobooks (AAX/M4B).
|
||||
Die Converter-Seite ermöglicht das Konvertieren vorhandener Audio- und Video-Dateien.
|
||||
|
||||
---
|
||||
|
||||
@@ -65,15 +65,9 @@ Das Scan-Intervall wird unter `Converter > Polling-Intervall (Sekunden)` konfigu
|
||||
|
||||
---
|
||||
|
||||
## Audiobook-Verarbeitung
|
||||
## Hinweis zu Audiobooks
|
||||
|
||||
Für AAX-Dateien (Audible) ist folgendes erforderlich:
|
||||
|
||||
1. AAX-Datei in den Eingangsordner hochladen
|
||||
2. Job mit Medientyp `Audiobook` erstellen
|
||||
3. Activation Bytes müssen verfügbar sein (werden beim ersten Mal abgefragt und gecacht)
|
||||
4. Ausgabeformat wählen: M4B (empfohlen), MP3 oder FLAC
|
||||
5. Kapitel-Splitting konfigurieren (optional)
|
||||
Der dedizierte AAX-/Audiobook-Flow ist auf der Seite [Audiobooks](audiobooks.md) dokumentiert.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ Bereiche:
|
||||
- strukturierte JSON-Blöcke (OMDb/MakeMKV/MediaInfo/EncodePlan/HandBrake)
|
||||
- Log-Ladefunktionen (`Tail`, `Vollständig`)
|
||||
|
||||
Bei Serien-Containern zeigt der Dialog statt eines globalen Log-Blocks einen `Disks`-Bereich mit einzelnen Disk-Akkordeons.
|
||||
Jede Disk kann dort separat geprüft und bearbeitet werden.
|
||||
|
||||
## Typische Aktionen im Detaildialog
|
||||
|
||||
- `OMDb neu zuordnen`
|
||||
@@ -46,6 +49,12 @@ Bereiche:
|
||||
- `Historieneintrag löschen`
|
||||
- bei Queue-Lock: `Aus Queue löschen`
|
||||
|
||||
Zusätzlich bei Serien-Container-Disks:
|
||||
|
||||
- `Im Ripper öffnen` (für `READY_TO_ENCODE`-Kinder)
|
||||
- diskbezogene Löschaktionen (`RAW`, `Folgen`, `Beides`) mit Child-/Subjob-Scope
|
||||
- diskbezogenes Löschen des Historieneintrags (inkl. Subjobs)
|
||||
|
||||
## Wann welche Aktion?
|
||||
|
||||
| Ziel | Aktion |
|
||||
@@ -56,6 +65,23 @@ Bereiche:
|
||||
| aus vorhandenem RAW erneut encodieren | `RAW neu encodieren` |
|
||||
| Speicher freigeben | Dateilöschaktionen |
|
||||
|
||||
---
|
||||
|
||||
## Löschen mit Vorschau
|
||||
|
||||
Beim Löschen eines Historieneintrags (`Eintrag löschen`) gibt es eine Pfadvorschau:
|
||||
|
||||
- getrennt nach `RAW` und Ausgabe (`Movie`/`Folgen`)
|
||||
- für Serien-/Multipart-Fälle mit Scope auf verknüpfte Jobs
|
||||
- selektierbare Pfade vor dem finalen Löschen
|
||||
|
||||
Neu im RAW-Bereich:
|
||||
|
||||
- bei mehreren RAW-Kandidaten kann pro Pfad gewählt werden, was entfernt wird
|
||||
- mindestens ein RAW-Pfad muss gewählt sein, wenn RAW-Löschung Teil der Aktion ist
|
||||
|
||||
Die Buttons (`Eintrag + nur RAW`, `Eintrag + nur ...`, `Eintrag + RAW + ...`) berücksichtigen die aktuelle Pfadauswahl und deaktivieren sich bei unvollständiger Auswahl.
|
||||
|
||||
## Logs
|
||||
|
||||
- `Tail laden (800)` für schnelle Fehleranalyse
|
||||
|
||||
+12
-8
@@ -1,12 +1,14 @@
|
||||
# GUI-Seiten
|
||||
|
||||
Ripster hat fünf Hauptseiten in der Navigation plus eine Expert-Seite.
|
||||
Ripster hat sieben Hauptseiten in der Navigation plus eine Expert-Seite.
|
||||
|
||||
## Seitenüberblick
|
||||
|
||||
| Seite | Zweck |
|
||||
|---|---|
|
||||
| [Jobs](jobs.md) | zentrale Inbox mit allen Jobs (Ripper, Converter, Audiobooks) |
|
||||
| [Ripper](ripper.md) | Live-Betrieb: Pipeline, Queue, Aktivitäten, Disc-Infos |
|
||||
| [Audiobooks](audiobooks.md) | AAX-Upload, Audiobook-Jobsteuerung und Output-Explorer |
|
||||
| [Settings](settings.md) | Konfiguration, Skripte, Ketten, Presets, Cronjobs |
|
||||
| [Historie](history.md) | abgeschlossene/laufende Jobs durchsuchen und nachbearbeiten |
|
||||
| [Converter](converter.md) | Audio/Video-Dateien konvertieren, Datei-Explorer, Upload |
|
||||
@@ -15,14 +17,16 @@ Ripster hat fünf Hauptseiten in der Navigation plus eine Expert-Seite.
|
||||
|
||||
## Empfohlene Nutzung im Alltag
|
||||
|
||||
1. **Start eines neuen Disc-Jobs:** `Ripper`
|
||||
2. **Dateien konvertieren oder Audiobooks verarbeiten:** `Converter`
|
||||
3. **Regeln/Automatisierung anpassen:** `Settings`
|
||||
4. **Ergebnisse prüfen oder Jobs nachbearbeiten:** `Historie`
|
||||
5. **Dateien herunterladen:** `Downloads`
|
||||
6. **Sonderfälle/Recovery:** `Database`
|
||||
1. **Ersten Überblick über alle Jobs:** `Jobs`
|
||||
2. **Start eines neuen Disc-Jobs:** `Ripper`
|
||||
3. **Audio/Video-Dateien konvertieren:** `Converter`
|
||||
4. **AAX/Audiobook-Flow:** `Audiobooks`
|
||||
5. **Regeln/Automatisierung anpassen:** `Settings`
|
||||
6. **Ergebnisse prüfen oder Jobs nachbearbeiten:** `Historie`
|
||||
7. **Dateien herunterladen:** `Downloads`
|
||||
8. **Sonderfälle/Recovery:** `Database`
|
||||
|
||||
## Hinweise zur Navigation
|
||||
|
||||
- `Ripper`, `Settings`, `Historie`, `Converter` und `Downloads` sind direkt in der Kopfnavigation.
|
||||
- `Jobs`, `Ripper`, `Converter`, `Audiobooks`, `Settings`, `Historie` und `Downloads` sind direkt in der Kopfnavigation.
|
||||
- `Database` ist als Expert-Route verfügbar: `/database`.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Jobs
|
||||
|
||||
Die Seite `Jobs` ist die zentrale Inbox über alle Job-Typen.
|
||||
|
||||
---
|
||||
|
||||
## Zweck
|
||||
|
||||
`Jobs` zeigt denselben Datenbestand in einer einheitlichen Liste:
|
||||
|
||||
- Ripper-Jobs (Blu-ray/DVD/CD)
|
||||
- Converter-Jobs (Audio/Video/ISO)
|
||||
- Audiobook-Jobs
|
||||
|
||||
Die Seite eignet sich als schneller Einstieg, bevor man in den jeweiligen Fachbereich springt.
|
||||
|
||||
---
|
||||
|
||||
## Filter und Ansicht
|
||||
|
||||
### View-Filter
|
||||
|
||||
- `Alle Jobs`
|
||||
- `Ripper-View`
|
||||
- `Converter-View`
|
||||
- `Audiobooks`
|
||||
|
||||
### Status-Filter
|
||||
|
||||
- `Alle`
|
||||
- `Aktiv`
|
||||
- `Fehler/Abbruch`
|
||||
|
||||
Zusätzlich gibt es eine Freitextsuche (Titel, Status, IMDb, Job-Kind etc.) und eine Paginierung.
|
||||
|
||||
---
|
||||
|
||||
## Was ein Eintrag zeigt
|
||||
|
||||
- Job-ID und Titel
|
||||
- Status-Badge (inkl. Queue-Berücksichtigung)
|
||||
- Job-Typ-Badge (z. B. Blu-ray, Converter Audio, Audiobook)
|
||||
- Zeitstempel der letzten Aktualisierung
|
||||
|
||||
Die Liste aktualisiert sich zyklisch automatisch.
|
||||
|
||||
---
|
||||
|
||||
## Aktion
|
||||
|
||||
Pro Eintrag gibt es einen Schnellzugriff:
|
||||
|
||||
- `Im Ripper öffnen` für Disc-/Audiobook-orientierte Jobs
|
||||
- `Im Converter öffnen` für Converter-Jobs
|
||||
|
||||
So gelangst du mit einem Klick in die passende Detailoberfläche.
|
||||
@@ -65,6 +65,12 @@ Mögliche Aktionen:
|
||||
|
||||
Kompakte Jobliste mit Status, Fortschritt, ETA. Klick auf einen Job klappt die Detailsteuerung auf.
|
||||
|
||||
Zusätzliche Kennzeichnungen in der Liste:
|
||||
|
||||
- `Serien-Container`/`Serien-Disk` für Staffel-Workflows
|
||||
- `Multipart Movie` bei zusammengeführten Film-Discs
|
||||
- Untertitel mit Disc-/Container-Infos (z. B. `Disc 2 | Container #123`)
|
||||
|
||||
Im aufgeklappten Zustand erscheint die Karte `Pipeline-Status` mit allen zustandsabhängigen Aktionen.
|
||||
|
||||
### Zustandsabhängige Hauptaktionen
|
||||
@@ -105,6 +111,10 @@ Aktionen:
|
||||
- `Disk neu analysieren`
|
||||
- `Metadaten-Modal öffnen`
|
||||
|
||||
Hinweis:
|
||||
|
||||
- `Laufwerk neu lesen` ist nicht mehr an einen festen Pipeline-State gebunden und kann genutzt werden, solange kein Rescan-Request läuft.
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Dialoge im Ripper
|
||||
@@ -115,6 +125,12 @@ Aktionen:
|
||||
- manuelle Eingabe als Fallback
|
||||
- `Auswahl übernehmen` startet den nächsten Pipeline-Schritt
|
||||
|
||||
Bei Film-Duplikaten in der Historie öffnet sich ein Konfliktdialog:
|
||||
|
||||
- `Übernahme erzwingen`: legt trotzdem einen neuen Job mit denselben Metadaten an
|
||||
- `Multipart movie`: verknüpft bestehenden und neuen Job in einem Multipart-Container (mit separaten Disc-Nummern)
|
||||
- `Zurück zur Auswahl`: zurück in die Metadatenliste
|
||||
|
||||
### Abbruch-Bereinigung
|
||||
|
||||
Nach Abbruch kann Ripster optional fragen, ob erzeugte RAW- oder Movie-Dateien gelöscht werden sollen.
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ Dieses Dokumentationsset ist als **Benutzerhandbuch** aufgebaut: erst Bedienung
|
||||
|
||||
- **Benutzerhandbuch**
|
||||
- Installation
|
||||
- GUI-Seiten im Detail (`Ripper`, `Settings`, `Historie`, `Database`)
|
||||
- GUI-Seiten im Detail (`Jobs`, `Ripper`, `Converter`, `Audiobooks`, `Settings`, `Historie`, `Downloads`, `Database`)
|
||||
- typische Arbeitsabläufe aus Anwendersicht
|
||||
- **Technischer Anhang**
|
||||
- vollständige Einstellungsreferenz
|
||||
|
||||
@@ -35,11 +35,17 @@ flowchart LR
|
||||
| `Analyse` | MakeMKV-Analyse läuft |
|
||||
| `Metadatenauswahl` | Metadaten müssen bestätigt werden |
|
||||
| `Warte auf Auswahl` | Playlist-Auswahl ist erforderlich |
|
||||
| `Warte auf Auswahl` | auch für RAW-Entscheidung bei vorhandenen Quelldaten genutzt |
|
||||
| `Startbereit` | kurzer Übergang vor Start |
|
||||
| `Rippen` | MakeMKV-Rip läuft |
|
||||
| `Mediainfo-Pruefung` | Titel/Spuren werden ausgewertet |
|
||||
| `Bereit zum Encodieren` | Review ist bereit |
|
||||
| `Encodieren` | HandBrake läuft |
|
||||
| `CD-Metadatenauswahl` | CD-Metadaten müssen bestätigt werden |
|
||||
| `CD-Startbereit` | CD-Job wartet auf Start |
|
||||
| `CD-Analyse` | CD-Track-/Strukturaufbereitung |
|
||||
| `CD-Rippen` | Audio-CD-Rip läuft |
|
||||
| `CD-Encodieren` | CD-Encode läuft |
|
||||
| `Fertig` | erfolgreich abgeschlossen |
|
||||
| `Abgebrochen` | manuell oder technisch abgebrochen |
|
||||
| `Fehler` | fehlgeschlagen |
|
||||
@@ -66,6 +72,14 @@ flowchart LR
|
||||
|
||||
`Mediainfo-Pruefung` -> `Warte auf Auswahl` bis Playlist bestätigt wurde.
|
||||
|
||||
### Vorhandenes RAW mit Nutzerentscheidung
|
||||
|
||||
`Metadatenauswahl` -> `Warte auf Auswahl` bis Entscheidung für Weiterverarbeitung (`continue`) oder Neustart getroffen wurde.
|
||||
|
||||
### CD-Flow
|
||||
|
||||
`CD-Metadatenauswahl` -> `CD-Startbereit` -> `CD-Rippen` -> `CD-Encodieren` -> `Fertig`
|
||||
|
||||
---
|
||||
|
||||
## Queue-Verhalten
|
||||
|
||||
@@ -59,3 +59,26 @@ In `Historie` -> Detaildialog:
|
||||
- `/database` öffnen
|
||||
- Bereich `Gefundene RAW-Einträge`
|
||||
- `Job anlegen`
|
||||
|
||||
## Workflow 7: Duplikat als Multipart Movie zusammenführen
|
||||
|
||||
1. `Ripper` -> `Metadaten auswählen` für einen Film-Job
|
||||
2. bei Duplikat-Konflikt `Multipart movie` wählen
|
||||
3. Disc-Nummern für bestehenden und neuen Job setzen (jeweils >= 1, unterschiedlich)
|
||||
4. Metadaten übernehmen
|
||||
5. in `Historie` den gemeinsamen Multipart-Container prüfen
|
||||
|
||||
## Workflow 8: Selektives Löschen in der Historie
|
||||
|
||||
1. in `Historie` einen Job öffnen und `Historieneintrag löschen`
|
||||
2. Löschvorschau prüfen (`RAW` + Ausgabe)
|
||||
3. gewünschte RAW-/Ausgabepfade gezielt auswählen
|
||||
4. passende Aktion ausführen (`Eintrag + nur RAW`, `Eintrag + nur ...`, `Eintrag + RAW + ...`)
|
||||
|
||||
## Workflow 9: Audiobook (AAX) verarbeiten
|
||||
|
||||
1. Seite `Audiobooks` öffnen
|
||||
2. AAX-Datei hochladen
|
||||
3. Job-Konfiguration prüfen und starten
|
||||
4. Fortschritt im Job-Panel verfolgen
|
||||
5. Ergebnis im `Audiobook Output Explorer` und in der `Historie` prüfen
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"dependencies": {
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripster-frontend",
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -808,6 +808,11 @@ export const api = {
|
||||
},
|
||||
async deleteJobFiles(jobId, target = 'both', options = {}) {
|
||||
const includeRelated = Boolean(options?.includeRelated);
|
||||
const selectedRawPaths = Array.isArray(options?.selectedRawPaths)
|
||||
? options.selectedRawPaths
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const selectedMoviePaths = Array.isArray(options?.selectedMoviePaths)
|
||||
? options.selectedMoviePaths
|
||||
.map((item) => String(item || '').trim())
|
||||
@@ -818,6 +823,7 @@ export const api = {
|
||||
body: JSON.stringify({
|
||||
target,
|
||||
includeRelated,
|
||||
...(selectedRawPaths ? { selectedRawPaths } : {}),
|
||||
...(selectedMoviePaths ? { selectedMoviePaths } : {})
|
||||
})
|
||||
});
|
||||
@@ -834,6 +840,11 @@ export const api = {
|
||||
const includeRelated = Boolean(options?.includeRelated);
|
||||
const resetDriveState = Boolean(options?.resetDriveState);
|
||||
const preserveRawForImportJobs = Boolean(options?.preserveRawForImportJobs);
|
||||
const selectedRawPaths = Array.isArray(options?.selectedRawPaths)
|
||||
? options.selectedRawPaths
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const selectedMoviePaths = Array.isArray(options?.selectedMoviePaths)
|
||||
? options.selectedMoviePaths
|
||||
.map((item) => String(item || '').trim())
|
||||
@@ -851,6 +862,7 @@ export const api = {
|
||||
resetDriveState,
|
||||
preserveRawForImportJobs,
|
||||
...(hasKeepDetectedDevice ? { keepDetectedDevice } : {}),
|
||||
...(selectedRawPaths ? { selectedRawPaths } : {}),
|
||||
...(selectedMoviePaths ? { selectedMoviePaths } : {})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1216,6 +1216,55 @@ export default function JobDetailDialog({
|
||||
|| typeof onRestartEncode === 'function'
|
||||
|| typeof onRestartReview === 'function'
|
||||
|| typeof onReencode === 'function';
|
||||
const isContainerWithDiskActions = isSeriesContainer && childJobs.length > 0;
|
||||
const resolveChildActionState = (child) => {
|
||||
const childStatusUpper = String(child?.status || '').trim().toUpperCase();
|
||||
const childLastStateUpper = String(child?.last_state || '').trim().toUpperCase();
|
||||
const childRunning = ['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING', 'CD_RIPPING', 'CD_ENCODING'].includes(childStatusUpper);
|
||||
const childCanResumeReady = Boolean(
|
||||
(childStatusUpper === 'READY_TO_ENCODE' || childLastStateUpper === 'READY_TO_ENCODE')
|
||||
&& !childRunning
|
||||
&& typeof onResumeReady === 'function'
|
||||
);
|
||||
const childMediaType = resolveMediaType(child);
|
||||
const childMkDone = Boolean(child?.ripSuccessful) || !child?.makemkvInfo || child?.makemkvInfo?.status === 'SUCCESS';
|
||||
const childHasReencodeRawInput = childMediaType === 'cd'
|
||||
? Boolean(child?.rawStatus?.exists || child?.raw_path || child?.output_path)
|
||||
: Boolean(child?.rawStatus?.exists && child?.rawStatus?.isEmpty !== true);
|
||||
const childCanReencode = !!(childHasReencodeRawInput && !childRunning && (childMediaType === 'cd' || childMkDone));
|
||||
const childHasConfirmedPlan = Boolean(
|
||||
child?.encodePlan
|
||||
&& Array.isArray(child?.encodePlan?.titles)
|
||||
&& child?.encodePlan?.titles.length > 0
|
||||
&& Number(child?.encode_review_confirmed || 0) === 1
|
||||
);
|
||||
const childHasRestartInput = Boolean(child?.encode_input_path || child?.raw_path || child?.encodePlan?.encodeInputPath);
|
||||
const childHasRaw = Boolean(child?.rawStatus?.exists || child?.raw_path || child?.encode_input_path);
|
||||
const childCanRestartEncode = Boolean(childHasConfirmedPlan && childHasRestartInput && !childRunning && childHasRaw);
|
||||
const childCanRestartReview = Boolean(
|
||||
child?.rawStatus?.exists
|
||||
&& child?.rawStatus?.isEmpty !== true
|
||||
&& !childRunning
|
||||
&& childMediaType !== 'audiobook'
|
||||
&& typeof onRestartReview === 'function'
|
||||
);
|
||||
const childCanAssignMetadata = !childRunning;
|
||||
const childOutputFolders = Array.isArray(child?.outputFolders) ? child.outputFolders : [];
|
||||
const childSeriesOutputExisting = Number(child?.seriesOutputSummary?.existing || 0);
|
||||
const childHasAnyOutput = childOutputFolders.length > 0
|
||||
|| childSeriesOutputExisting > 0
|
||||
|| Boolean(child?.outputStatus?.exists || child?.output_path);
|
||||
return {
|
||||
childRunning,
|
||||
childCanResumeReady,
|
||||
childCanReencode,
|
||||
childCanRestartEncode,
|
||||
childCanRestartReview,
|
||||
childCanAssignMetadata,
|
||||
childHasRaw,
|
||||
childHasAnyOutput
|
||||
};
|
||||
};
|
||||
const useAudioPosterLayout = isCd || isAudiobook || (isConverter && converterMediaType === 'audio');
|
||||
|
||||
return (
|
||||
@@ -2039,7 +2088,7 @@ export default function JobDetailDialog({
|
||||
) : null}
|
||||
|
||||
<section className="job-meta-block job-meta-block-full">
|
||||
<h4>Logs</h4>
|
||||
<h4>{isSeriesContainer ? 'Disks' : 'Logs'}</h4>
|
||||
{isSeriesContainer && childJobs.length > 0 ? (
|
||||
<>
|
||||
<div className="job-json-grid">
|
||||
@@ -2048,6 +2097,11 @@ export default function JobDetailDialog({
|
||||
{childJobs.map((child) => {
|
||||
const childDiscNumber = resolveSeriesDiscNumber(child);
|
||||
const childDiscLabel = childDiscNumber ? `Disk ${childDiscNumber}` : 'Disk unbekannt';
|
||||
const childActionState = resolveChildActionState(child);
|
||||
const childCanShowGeneralEncodeActions = childActionState.childCanResumeReady
|
||||
|| typeof onRestartEncode === 'function'
|
||||
|| typeof onRestartReview === 'function'
|
||||
|| typeof onReencode === 'function';
|
||||
return (
|
||||
<details key={`child-log-${child.id}`} className="episode-track-accordion">
|
||||
<summary className="series-batch-episode-head">
|
||||
@@ -2059,6 +2113,152 @@ export default function JobDetailDialog({
|
||||
<JsonView title={isCd ? 'Rip-Plan' : (isConverter ? 'Converter Plan' : 'Encode Plan')} value={child.encodePlan} />
|
||||
<JsonView title={isCd ? 'Rip-Info' : (isAudiobook ? 'FFmpeg Info' : (isConverter ? 'Converter Encode Info' : 'HandBrake Info'))} value={child.handbrakeInfo} />
|
||||
</div>
|
||||
<div className="actions-section">
|
||||
{childCanShowGeneralEncodeActions ? (
|
||||
<div className="actions-group">
|
||||
<div className="actions-group-label"><i className="pi pi-play-circle" /> Kodierung</div>
|
||||
{childActionState.childCanResumeReady ? (
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="Im Ripper öffnen"
|
||||
icon="pi pi-window-maximize"
|
||||
severity="info"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onResumeReady?.(child)}
|
||||
loading={actionBusy}
|
||||
/>
|
||||
<span className="action-desc">Öffnet den wartenden Job im Ripper zur Weiterverarbeitung.</span>
|
||||
</div>
|
||||
) : null}
|
||||
{typeof onRestartEncode === 'function' ? (
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="Encode neu starten"
|
||||
icon="pi pi-play"
|
||||
severity="success"
|
||||
size="small"
|
||||
onClick={() => onRestartEncode?.(child)}
|
||||
loading={actionBusy}
|
||||
disabled={!childActionState.childCanRestartEncode}
|
||||
/>
|
||||
<span className="action-desc">Startet HandBrake mit den zuletzt gespeicherten Einstellungen direkt neu — ohne Review.</span>
|
||||
</div>
|
||||
) : null}
|
||||
{typeof onRestartReview === 'function' ? (
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="Spur-Auswahl öffnen"
|
||||
icon="pi pi-list"
|
||||
severity="info"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onRestartReview?.(child)}
|
||||
loading={actionBusy}
|
||||
disabled={!childActionState.childCanRestartReview}
|
||||
/>
|
||||
<span className="action-desc">Öffnet die Spur- und Preset-Auswahl erneut, um Einstellungen vor dem Encode zu ändern.</span>
|
||||
</div>
|
||||
) : null}
|
||||
{typeof onReencode === 'function' ? (
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="Neustart"
|
||||
icon="pi pi-sync"
|
||||
severity="info"
|
||||
size="small"
|
||||
onClick={() => onReencode?.(child)}
|
||||
loading={reencodeBusy}
|
||||
disabled={!childActionState.childCanReencode}
|
||||
/>
|
||||
<span className="action-desc">Analysiert die Quelldatei neu (MediaInfo, Playlist-Check) und öffnet dann die Spur-Auswahl. Unterschied zu „Spur-Auswahl öffnen": der Analyse-Schritt wird komplett wiederholt.</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isCd && !isAudiobook && !isConverter && typeof onAssignOmdb === 'function' ? (
|
||||
<div className="actions-group">
|
||||
<div className="actions-group-label"><i className="pi pi-search" /> Metadaten</div>
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label={metadataAssignButtonLabel}
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onAssignOmdb?.(child)}
|
||||
loading={omdbAssignBusy}
|
||||
disabled={!childActionState.childCanAssignMetadata}
|
||||
/>
|
||||
<span className="action-desc">{metadataAssignActionDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{typeof onDeleteFiles === 'function' ? (
|
||||
<div className="actions-group">
|
||||
<div className="actions-group-label"><i className="pi pi-trash" /> Dateien löschen</div>
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="RAW löschen (Eintrag bleibt)"
|
||||
icon="pi pi-trash"
|
||||
severity="warning"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onDeleteFiles?.(child, 'raw')}
|
||||
loading={actionBusy}
|
||||
disabled={!childActionState.childHasRaw}
|
||||
/>
|
||||
<span className="action-desc">Löscht nur die RAW-Quelldatei dieser Disk (nicht die Folgen-Subjobs).</span>
|
||||
</div>
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="Folgen löschen (Eintrag bleibt)"
|
||||
icon="pi pi-trash"
|
||||
severity="warning"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onDeleteFiles?.(child, 'movie')}
|
||||
loading={actionBusy}
|
||||
disabled={!childActionState.childHasAnyOutput}
|
||||
/>
|
||||
<span className="action-desc">Löscht die encodierten Folgen dieser Disk (inkl. Child-/Subjobs).</span>
|
||||
</div>
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="Beides löschen (Eintrag bleibt)"
|
||||
icon="pi pi-times"
|
||||
severity="danger"
|
||||
size="small"
|
||||
onClick={() => onDeleteFiles?.(child, 'both')}
|
||||
loading={actionBusy}
|
||||
disabled={!childActionState.childHasRaw && !childActionState.childHasAnyOutput}
|
||||
/>
|
||||
<span className="action-desc">Löscht Disk-RAW plus Folgen dieser Disk gemeinsam.</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{typeof onDeleteEntry === 'function' ? (
|
||||
<div className="actions-group">
|
||||
<div className="actions-group-label"><i className="pi pi-database" /> Historie</div>
|
||||
<div className="action-item">
|
||||
<Button
|
||||
label="Historieneintrag löschen"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
outlined
|
||||
size="small"
|
||||
onClick={() => onDeleteEntry?.(child, { includeRelated: true })}
|
||||
loading={deleteEntryBusy}
|
||||
disabled={childActionState.childRunning}
|
||||
/>
|
||||
<span className="action-desc">Öffnet die Löschauswahl für diese Disk (Child-Job inkl. Subjobs). Wenn dies die letzte Disk ist, wird auch der Container entfernt.</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
@@ -2146,6 +2346,8 @@ export default function JobDetailDialog({
|
||||
<p>Live-Log wird nur im Ripper während laufender Analyse/Rip/Encode angezeigt.</p>
|
||||
)}
|
||||
|
||||
{queueLocked || !isContainerWithDiskActions ? (
|
||||
<>
|
||||
<h4>Aktionen</h4>
|
||||
{queueLocked ? (
|
||||
<div className="actions-row">
|
||||
@@ -2365,6 +2567,8 @@ export default function JobDetailDialog({
|
||||
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!queueLocked ? (
|
||||
<div className="action-delete-entry">
|
||||
|
||||
@@ -49,6 +49,10 @@ export default function MetadataSelectionDialog({
|
||||
const [searchBusy, setSearchBusy] = useState(false);
|
||||
const [searchError, setSearchError] = useState('');
|
||||
const [submitError, setSubmitError] = useState('');
|
||||
const [conflictState, setConflictState] = useState(null);
|
||||
const [conflictExistingDiscNumber, setConflictExistingDiscNumber] = useState('');
|
||||
const [conflictNewDiscNumber, setConflictNewDiscNumber] = useState('');
|
||||
const [conflictError, setConflictError] = useState('');
|
||||
const [seasonFilter, setSeasonFilter] = useState('');
|
||||
const [discValidationError, setDiscValidationError] = useState('');
|
||||
const activeSearchRequestRef = useRef(0);
|
||||
@@ -108,6 +112,10 @@ export default function MetadataSelectionDialog({
|
||||
setSearchBusy(false);
|
||||
setSearchError('');
|
||||
setSubmitError('');
|
||||
setConflictState(null);
|
||||
setConflictExistingDiscNumber('');
|
||||
setConflictNewDiscNumber('');
|
||||
setConflictError('');
|
||||
setSeasonFilter('');
|
||||
setDiscValidationError('');
|
||||
setSelectedMetadataProvider(contextMetadataProvider);
|
||||
@@ -127,7 +135,7 @@ export default function MetadataSelectionDialog({
|
||||
if (submitError) {
|
||||
setSubmitError('');
|
||||
}
|
||||
}, [manualDiscNumber, seasonFilter, selected, metadataProvider]);
|
||||
}, [manualDiscNumber, seasonFilter, selected, metadataProvider, conflictExistingDiscNumber, conflictNewDiscNumber]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const base = Array.isArray(context?.metadataCandidates)
|
||||
@@ -222,9 +230,49 @@ export default function MetadataSelectionDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitError = (error, pendingPayload = null) => {
|
||||
const details = Array.isArray(error?.details) ? error.details : [];
|
||||
const duplicateDetail = details.find(
|
||||
(detail) => String(detail?.code || '').trim().toUpperCase() === 'METADATA_DUPLICATE_FOUND'
|
||||
);
|
||||
if (duplicateDetail) {
|
||||
const existingJob = duplicateDetail?.existingJob && typeof duplicateDetail.existingJob === 'object'
|
||||
? duplicateDetail.existingJob
|
||||
: null;
|
||||
const nextPendingPayload = pendingPayload && typeof pendingPayload === 'object'
|
||||
? pendingPayload
|
||||
: (conflictState?.pendingPayload && typeof conflictState.pendingPayload === 'object'
|
||||
? conflictState.pendingPayload
|
||||
: null);
|
||||
const pendingDiscNumber = parseDiscNumber(nextPendingPayload?.discNumber);
|
||||
const existingDiscNumber = parseDiscNumber(existingJob?.discNumber);
|
||||
setConflictState({
|
||||
existingJob,
|
||||
pendingPayload: nextPendingPayload
|
||||
});
|
||||
setConflictExistingDiscNumber(existingDiscNumber ? String(existingDiscNumber) : '');
|
||||
setConflictNewDiscNumber(pendingDiscNumber ? String(pendingDiscNumber) : '');
|
||||
setConflictError('');
|
||||
setSubmitError('');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDuplicateDiscError = details.some(
|
||||
(detail) => String(detail?.code || '').trim().toUpperCase() === 'SERIES_DISC_ALREADY_EXISTS'
|
||||
);
|
||||
const message = String(error?.message || 'Metadaten konnten nicht uebernommen werden.').trim()
|
||||
|| 'Metadaten konnten nicht uebernommen werden.';
|
||||
if (hasDuplicateDiscError) {
|
||||
setSubmitError(message);
|
||||
return;
|
||||
}
|
||||
setSubmitError(message);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setDiscValidationError('');
|
||||
setSubmitError('');
|
||||
setConflictError('');
|
||||
if (requiresDiscNumber && !hasValidDiscNumber) {
|
||||
setDiscValidationError('Disk-Nummer ist Pflicht und muss eine ganze Zahl >= 1 sein.');
|
||||
return;
|
||||
@@ -265,14 +313,65 @@ export default function MetadataSelectionDialog({
|
||||
try {
|
||||
await onSubmit(payload);
|
||||
} catch (error) {
|
||||
const details = Array.isArray(error?.details) ? error.details : [];
|
||||
const hasDuplicateDiscError = details.some((detail) => String(detail?.code || '').trim().toUpperCase() === 'SERIES_DISC_ALREADY_EXISTS');
|
||||
const message = String(error?.message || 'Metadaten konnten nicht uebernommen werden.').trim() || 'Metadaten konnten nicht uebernommen werden.';
|
||||
if (hasDuplicateDiscError) {
|
||||
setSubmitError(message);
|
||||
handleSubmitError(error, payload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceDuplicateSubmit = async () => {
|
||||
const pendingPayload = conflictState?.pendingPayload && typeof conflictState.pendingPayload === 'object'
|
||||
? conflictState.pendingPayload
|
||||
: null;
|
||||
if (!pendingPayload) {
|
||||
setConflictError('Konflikt-Zustand ist unvollständig. Bitte erneut suchen.');
|
||||
return;
|
||||
}
|
||||
setSubmitError(message);
|
||||
setConflictError('');
|
||||
setSubmitError('');
|
||||
try {
|
||||
await onSubmit({
|
||||
...pendingPayload,
|
||||
duplicateAction: 'allow_new'
|
||||
});
|
||||
} catch (error) {
|
||||
handleSubmitError(error, pendingPayload);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitMultipartMovie = async () => {
|
||||
const pendingPayload = conflictState?.pendingPayload && typeof conflictState.pendingPayload === 'object'
|
||||
? conflictState.pendingPayload
|
||||
: null;
|
||||
const existingJob = conflictState?.existingJob && typeof conflictState.existingJob === 'object'
|
||||
? conflictState.existingJob
|
||||
: null;
|
||||
if (!pendingPayload || !existingJob?.id) {
|
||||
setConflictError('Konflikt-Zustand ist unvollständig. Bitte erneut suchen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const existingDisc = parseDiscNumber(conflictExistingDiscNumber);
|
||||
const newDisc = parseDiscNumber(conflictNewDiscNumber);
|
||||
if (!existingDisc || !newDisc) {
|
||||
setConflictError('Für Multipart movie müssen beide Disc-Nummern gesetzt werden (>= 1).');
|
||||
return;
|
||||
}
|
||||
if (existingDisc === newDisc) {
|
||||
setConflictError('Disc-Nummern müssen unterschiedlich sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
setConflictError('');
|
||||
setSubmitError('');
|
||||
try {
|
||||
await onSubmit({
|
||||
...pendingPayload,
|
||||
duplicateAction: 'multipart_movie',
|
||||
existingJobId: existingJob.id,
|
||||
discNumber: newDisc,
|
||||
existingDiscNumber: existingDisc
|
||||
});
|
||||
} catch (error) {
|
||||
handleSubmitError(error, pendingPayload);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -280,6 +379,80 @@ export default function MetadataSelectionDialog({
|
||||
onHide?.();
|
||||
};
|
||||
|
||||
if (conflictState) {
|
||||
const existingJob = conflictState?.existingJob && typeof conflictState.existingJob === 'object'
|
||||
? conflictState.existingJob
|
||||
: null;
|
||||
return (
|
||||
<Dialog
|
||||
header="Metadaten bereits vorhanden"
|
||||
visible={visible}
|
||||
onHide={handleHideRequest}
|
||||
style={{ width: '38rem', maxWidth: '96vw' }}
|
||||
className="metadata-selection-dialog"
|
||||
breakpoints={{ '1200px': '92vw', '768px': '96vw', '560px': '98vw' }}
|
||||
modal
|
||||
>
|
||||
<p>
|
||||
Für diese Film-Metadaten existiert bereits ein Job in der Historie.
|
||||
</p>
|
||||
<p>
|
||||
Bestehender Job: <strong>#{existingJob?.id || '-'}</strong>
|
||||
{' | '}
|
||||
<strong>{existingJob?.title || '-'}</strong>
|
||||
{existingJob?.year ? ` (${existingJob.year})` : ''}
|
||||
</p>
|
||||
<p>
|
||||
Status: {existingJob?.status || '-'}
|
||||
{existingJob?.discNumber ? ` | Disc ${existingJob.discNumber}` : ''}
|
||||
</p>
|
||||
<div className="metadata-grid">
|
||||
<InputText
|
||||
value={conflictExistingDiscNumber}
|
||||
onChange={(event) => setConflictExistingDiscNumber(event.target.value)}
|
||||
placeholder="Disc-Nr bestehender Job"
|
||||
inputMode="numeric"
|
||||
disabled={busy}
|
||||
/>
|
||||
<InputText
|
||||
value={conflictNewDiscNumber}
|
||||
onChange={(event) => setConflictNewDiscNumber(event.target.value)}
|
||||
placeholder="Disc-Nr neuer Job"
|
||||
inputMode="numeric"
|
||||
disabled={busy}
|
||||
/>
|
||||
</div>
|
||||
{conflictError ? <small className="error-text">{conflictError}</small> : null}
|
||||
{submitError ? <small className="error-text">{submitError}</small> : null}
|
||||
<div className="dialog-actions">
|
||||
<Button
|
||||
label="Zurück zur Auswahl"
|
||||
severity="secondary"
|
||||
text
|
||||
onClick={() => {
|
||||
setConflictState(null);
|
||||
setConflictError('');
|
||||
}}
|
||||
disabled={busy}
|
||||
/>
|
||||
<Button
|
||||
label="Übernahme erzwingen"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={handleForceDuplicateSubmit}
|
||||
loading={busy}
|
||||
/>
|
||||
<Button
|
||||
label="Multipart movie"
|
||||
icon="pi pi-sitemap"
|
||||
onClick={handleSubmitMultipartMovie}
|
||||
loading={busy}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header="Metadaten auswählen"
|
||||
|
||||
@@ -183,6 +183,29 @@ function isSeriesBatchChildHistoryRow(row) {
|
||||
return Boolean(encodePlan?.seriesBatchChild || encodePlan?.seriesBatchVirtualEpisode);
|
||||
}
|
||||
|
||||
function isContainerHistoryRow(row) {
|
||||
const kind = String(
|
||||
row?.job_kind
|
||||
|| row?.jobKind
|
||||
|| row?.encodePlan?.jobKind
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
return kind === 'dvd_series_container' || kind === 'multipart_movie_container';
|
||||
}
|
||||
|
||||
function isSeriesChildHistoryRow(row) {
|
||||
const kind = String(
|
||||
row?.job_kind
|
||||
|| row?.jobKind
|
||||
|| row?.encodePlan?.jobKind
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
if (kind === 'dvd_series_child' || kind === 'multipart_movie_child') {
|
||||
return true;
|
||||
}
|
||||
return Boolean(normalizeJobId(row?.parent_job_id)) && isSeriesVideoJob(row);
|
||||
}
|
||||
|
||||
function formatDurationSeconds(totalSeconds) {
|
||||
const parsed = Number(totalSeconds);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
@@ -315,6 +338,9 @@ function hasAudioOutput(row) {
|
||||
}
|
||||
|
||||
function getOutputLabelForRow(row) {
|
||||
if (isSeriesVideoJob(row)) {
|
||||
return 'Folgen-Datei(en)';
|
||||
}
|
||||
if (resolveMediaType(row) === 'audiobook') {
|
||||
return 'Audiobook-Datei(en)';
|
||||
}
|
||||
@@ -325,6 +351,9 @@ function getOutputLabelForRow(row) {
|
||||
}
|
||||
|
||||
function getOutputShortLabelForRow(row) {
|
||||
if (isSeriesVideoJob(row)) {
|
||||
return 'Folgen';
|
||||
}
|
||||
if (resolveMediaType(row) === 'audiobook') {
|
||||
return 'Audiobook';
|
||||
}
|
||||
@@ -449,6 +478,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
const [deleteEntryPreview, setDeleteEntryPreview] = useState(null);
|
||||
const [deleteEntryPreviewLoading, setDeleteEntryPreviewLoading] = useState(false);
|
||||
const [deleteEntryTargetBusy, setDeleteEntryTargetBusy] = useState(null);
|
||||
const [deleteEntryIncludeRelated, setDeleteEntryIncludeRelated] = useState(true);
|
||||
const [deleteEntrySelectedRawPaths, setDeleteEntrySelectedRawPaths] = useState([]);
|
||||
const [deleteEntrySelectedMoviePaths, setDeleteEntrySelectedMoviePaths] = useState([]);
|
||||
const [downloadBusyTarget, setDownloadBusyTarget] = useState(null);
|
||||
const [downloadFolderBusyPath, setDownloadFolderBusyPath] = useState(null);
|
||||
@@ -899,9 +930,12 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
// ── File deletion ──────────────────────────────────────────────────────────
|
||||
|
||||
const handleDeleteFiles = async (row, target) => {
|
||||
const includeRelated = isSeriesVideoJob(row);
|
||||
const isContainerRow = isContainerHistoryRow(row);
|
||||
const isSeriesChildRow = isSeriesChildHistoryRow(row);
|
||||
const includeRelated = isContainerRow
|
||||
|| (isSeriesChildRow && (target === 'movie' || target === 'both'));
|
||||
|
||||
if (target === 'movie') {
|
||||
if (target === 'movie' && !isSeriesChildRow) {
|
||||
const outputFolders = await loadOutputFoldersForJob(row);
|
||||
if (outputFolders.length > 1) {
|
||||
await openConflictModal(row, 'delete_output', 'delete', outputFolders);
|
||||
@@ -913,8 +947,14 @@ 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
|
||||
const scopeSuffix = isContainerRow
|
||||
? '\nScope: kompletter Serien-Container (alle zugehörigen RAWs/Outputs).'
|
||||
: isSeriesChildRow && target === 'raw'
|
||||
? '\nScope: nur RAW dieses Disk-Jobs.'
|
||||
: isSeriesChildRow && (target === 'movie' || target === 'both')
|
||||
? '\nScope: diese Disk inkl. zugehöriger Child-/Subjobs (Folgen).'
|
||||
: includeRelated
|
||||
? '\nScope: verknüpfte Jobs werden einbezogen.'
|
||||
: '';
|
||||
const confirmed = await confirmModal({
|
||||
header: 'Dateien löschen',
|
||||
@@ -929,8 +969,20 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
|
||||
setActionBusy(true);
|
||||
try {
|
||||
let summary = {};
|
||||
if (target === 'both' && isSeriesChildRow) {
|
||||
// Disk-RAW nur am Disk-Job löschen, Folgen jedoch diskweit inkl. Subjobs.
|
||||
const rawResponse = await api.deleteJobFiles(row.id, 'raw', { includeRelated: false });
|
||||
const movieResponse = await api.deleteJobFiles(row.id, 'movie', { includeRelated: true });
|
||||
summary = {
|
||||
target: 'both',
|
||||
raw: rawResponse?.summary?.raw || {},
|
||||
movie: movieResponse?.summary?.movie || {}
|
||||
};
|
||||
} else {
|
||||
const response = await api.deleteJobFiles(row.id, target, { includeRelated });
|
||||
const summary = response.summary || {};
|
||||
summary = response?.summary || {};
|
||||
}
|
||||
const rawSummary = summary.raw || {};
|
||||
const movieSummary = summary.movie || {};
|
||||
const deletedSomething = target === 'raw'
|
||||
@@ -1367,24 +1419,35 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
setDeleteEntryPreview(null);
|
||||
setDeleteEntryPreviewLoading(false);
|
||||
setDeleteEntryTargetBusy(null);
|
||||
setDeleteEntryIncludeRelated(true);
|
||||
setDeleteEntrySelectedRawPaths([]);
|
||||
setDeleteEntrySelectedMoviePaths([]);
|
||||
};
|
||||
|
||||
const handleDeleteEntry = async (row) => {
|
||||
const handleDeleteEntry = async (row, options = {}) => {
|
||||
const jobId = Number(row?.id || 0);
|
||||
if (!jobId) {
|
||||
return;
|
||||
}
|
||||
const includeRelated = options?.includeRelated !== false;
|
||||
setDeleteEntryDialogRow(row);
|
||||
setDeleteEntryPreview(null);
|
||||
setDeleteEntryIncludeRelated(includeRelated);
|
||||
setDeleteEntrySelectedRawPaths([]);
|
||||
setDeleteEntrySelectedMoviePaths([]);
|
||||
setDeleteEntryDialogVisible(true);
|
||||
setDeleteEntryPreviewLoading(true);
|
||||
setDeleteEntryBusy(true);
|
||||
try {
|
||||
const response = await api.getJobDeletePreview(jobId, { includeRelated: true });
|
||||
const response = await api.getJobDeletePreview(jobId, { includeRelated });
|
||||
const preview = response?.preview || null;
|
||||
setDeleteEntryPreview(preview);
|
||||
const rawCandidates = Array.isArray(preview?.pathCandidates?.raw) ? preview.pathCandidates.raw : [];
|
||||
const defaultSelectedRawPaths = rawCandidates
|
||||
.filter((item) => Boolean(item?.exists))
|
||||
.map((item) => String(item?.path || '').trim())
|
||||
.filter(Boolean);
|
||||
setDeleteEntrySelectedRawPaths(defaultSelectedRawPaths);
|
||||
const movieCandidates = Array.isArray(preview?.pathCandidates?.movie) ? preview.pathCandidates.movie : [];
|
||||
const defaultSelectedMoviePaths = movieCandidates
|
||||
.filter((item) => Boolean(item?.exists))
|
||||
@@ -1401,6 +1464,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
setDeleteEntryDialogVisible(false);
|
||||
setDeleteEntryDialogRow(null);
|
||||
setDeleteEntryPreview(null);
|
||||
setDeleteEntryIncludeRelated(true);
|
||||
setDeleteEntrySelectedRawPaths([]);
|
||||
} finally {
|
||||
setDeleteEntryPreviewLoading(false);
|
||||
setDeleteEntryBusy(false);
|
||||
@@ -1416,6 +1481,19 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
if (!jobId) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(normalizedTarget === 'raw' || normalizedTarget === 'both')
|
||||
&& rawDeleteSelectionEnabled
|
||||
&& deleteEntrySelectedRawPaths.length === 0
|
||||
) {
|
||||
toastRef.current?.show({
|
||||
severity: 'warn',
|
||||
summary: 'RAW-Auswahl erforderlich',
|
||||
detail: 'Bitte mindestens einen RAW-Pfad zum Löschen auswählen.',
|
||||
life: 3500
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(normalizedTarget === 'movie' || normalizedTarget === 'both')
|
||||
&& previewMovieExisting.length > 0
|
||||
@@ -1434,7 +1512,8 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
setDeleteEntryTargetBusy(normalizedTarget);
|
||||
try {
|
||||
const response = await api.deleteJobEntry(jobId, normalizedTarget, {
|
||||
includeRelated: true,
|
||||
includeRelated: deleteEntryIncludeRelated,
|
||||
selectedRawPaths: deleteEntrySelectedRawPaths,
|
||||
selectedMoviePaths: deleteEntrySelectedMoviePaths
|
||||
});
|
||||
const deletedJobIds = Array.isArray(response?.deletedJobIds) ? response.deletedJobIds : [];
|
||||
@@ -1489,6 +1568,24 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDeleteRawPathSelection = (rawPath, checked) => {
|
||||
const normalizedPath = String(rawPath || '').trim();
|
||||
if (!normalizedPath) {
|
||||
return;
|
||||
}
|
||||
setDeleteEntrySelectedRawPaths((previous) => {
|
||||
const nextSet = new Set((Array.isArray(previous) ? previous : []).map((item) => String(item || '').trim()).filter(Boolean));
|
||||
if (checked) {
|
||||
nextSet.add(normalizedPath);
|
||||
} else {
|
||||
nextSet.delete(normalizedPath);
|
||||
}
|
||||
return previewRawPaths
|
||||
.map((item) => String(item?.path || '').trim())
|
||||
.filter((itemPath) => itemPath && nextSet.has(itemPath));
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromQueue = async (row) => {
|
||||
const jobId = normalizeJobId(row?.id || row);
|
||||
if (!jobId) {
|
||||
@@ -1848,12 +1945,20 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
const previewMoviePaths = Array.isArray(deleteEntryPreview?.pathCandidates?.movie) ? deleteEntryPreview.pathCandidates.movie : [];
|
||||
const previewRawExisting = previewRawPaths.filter((item) => Boolean(item?.exists));
|
||||
const previewMovieExisting = previewMoviePaths.filter((item) => Boolean(item?.exists));
|
||||
const previewRawDisplay = previewRawExisting.length > 0 ? previewRawExisting : previewRawPaths.slice(0, 1);
|
||||
const rawDeleteSelectionEnabled = previewRawExisting.length > 2;
|
||||
const previewRawDisplay = rawDeleteSelectionEnabled
|
||||
? previewRawPaths
|
||||
: (previewRawExisting.length > 0 ? previewRawExisting : previewRawPaths.slice(0, 1));
|
||||
const previewMovieDisplay = previewMovieExisting.length > 0 ? previewMovieExisting : previewMoviePaths.slice(0, 1);
|
||||
const selectedDeleteRawPathSet = useMemo(
|
||||
() => new Set(deleteEntrySelectedRawPaths.map((item) => String(item || '').trim()).filter(Boolean)),
|
||||
[deleteEntrySelectedRawPaths]
|
||||
);
|
||||
const selectedDeleteMoviePathSet = useMemo(
|
||||
() => new Set(deleteEntrySelectedMoviePaths.map((item) => String(item || '').trim()).filter(Boolean)),
|
||||
[deleteEntrySelectedMoviePaths]
|
||||
);
|
||||
const rawDeleteSelectionRequired = rawDeleteSelectionEnabled && deleteEntrySelectedRawPaths.length === 0;
|
||||
const movieDeleteSelectionRequired = previewMovieExisting.length > 0 && deleteEntrySelectedMoviePaths.length === 0;
|
||||
const deleteTargetActionsDisabled = deleteEntryPreviewLoading || Boolean(deleteEntryTargetBusy) || !deleteEntryPreview;
|
||||
const deleteEntryOutputLabel = getOutputLabelForRow(deleteEntryDialogRow);
|
||||
@@ -2010,10 +2115,26 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
<ul className="history-delete-preview-list">
|
||||
{previewRawDisplay.map((item) => (
|
||||
<li key={`delete-raw-${item.path}`}>
|
||||
{rawDeleteSelectionEnabled && item.exists ? (
|
||||
<label className="history-delete-preview-checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedDeleteRawPathSet.has(String(item.path || '').trim())}
|
||||
onChange={(event) => toggleDeleteRawPathSelection(item.path, Boolean(event.target.checked))}
|
||||
/>
|
||||
<span className={item.exists ? 'exists-yes' : 'exists-no'}>
|
||||
{item.exists ? 'vorhanden' : 'nicht gefunden'}
|
||||
</span>
|
||||
<span>| {item.path}</span>
|
||||
</label>
|
||||
) : (
|
||||
<>
|
||||
<span className={item.exists ? 'exists-yes' : 'exists-no'}>
|
||||
{item.exists ? 'vorhanden' : 'nicht gefunden'}
|
||||
</span>
|
||||
{' '}| {item.path}
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -2021,6 +2142,11 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
})() : (
|
||||
<small className="history-dv-subtle">Keine RAW-Pfade.</small>
|
||||
)}
|
||||
{rawDeleteSelectionEnabled ? (
|
||||
<small className="history-dv-subtle">
|
||||
RAW-Auswahl aktiv: {deleteEntrySelectedRawPaths.length}/{previewRawExisting.length} ausgewählt.
|
||||
</small>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -2069,7 +2195,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
outlined
|
||||
onClick={() => confirmDeleteEntry('raw')}
|
||||
loading={deleteEntryTargetBusy === 'raw'}
|
||||
disabled={deleteTargetActionsDisabled}
|
||||
disabled={deleteTargetActionsDisabled || rawDeleteSelectionRequired}
|
||||
/>
|
||||
<Button
|
||||
label={`Eintrag + nur ${deleteEntryOutputShortLabel}`}
|
||||
@@ -2086,7 +2212,7 @@ export default function HistoryPage({ refreshToken = 0 }) {
|
||||
severity="danger"
|
||||
onClick={() => confirmDeleteEntry('both')}
|
||||
loading={deleteEntryTargetBusy === 'both'}
|
||||
disabled={deleteTargetActionsDisabled || movieDeleteSelectionRequired}
|
||||
disabled={deleteTargetActionsDisabled || movieDeleteSelectionRequired || rawDeleteSelectionRequired}
|
||||
/>
|
||||
<Button
|
||||
label="Nur Eintrag löschen"
|
||||
|
||||
@@ -24,7 +24,6 @@ import { confirmModal } from '../utils/confirmModal';
|
||||
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'];
|
||||
const activeCdDriveStates = ['CD_ANALYZING', 'CD_METADATA_SELECTION', 'CD_READY_TO_RIP', 'CD_RIPPING', 'CD_ENCODING'];
|
||||
const ripperStatuses = new Set([
|
||||
'ANALYZING',
|
||||
@@ -1008,7 +1007,6 @@ export default function RipperPage({
|
||||
const [metadataDialogVisible, setMetadataDialogVisible] = useState(false);
|
||||
const [metadataDialogContext, setMetadataDialogContext] = useState(null);
|
||||
const [metadataDialogReassignMode, setMetadataDialogReassignMode] = useState(false);
|
||||
const [duplicateJobDialog, setDuplicateJobDialog] = useState({ visible: false, existingJob: null, pendingPayload: null });
|
||||
const [cdMetadataDialogVisible, setCdMetadataDialogVisible] = useState(false);
|
||||
const [cdMetadataDialogContext, setCdMetadataDialogContext] = useState(null);
|
||||
const [cdRipPanelJobId, setCdRipPanelJobId] = useState(null);
|
||||
@@ -1131,7 +1129,10 @@ export default function RipperPage({
|
||||
if (isConverterJob(job)) {
|
||||
return false;
|
||||
}
|
||||
if (String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container') {
|
||||
if (
|
||||
String(job?.job_kind || '').trim().toLowerCase() === 'dvd_series_container'
|
||||
|| String(job?.job_kind || '').trim().toLowerCase() === 'multipart_movie_container'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const normalizedStatus = String(job?.status || '').trim().toUpperCase();
|
||||
@@ -2302,6 +2303,7 @@ export default function RipperPage({
|
||||
setBusy(true);
|
||||
try {
|
||||
if (metadataDialogReassignMode) {
|
||||
const duplicateAction = String(payload?.duplicateAction || '').trim().toLowerCase();
|
||||
const providerRaw = String(payload?.metadataProvider || '').trim().toLowerCase();
|
||||
const nextWorkflowKind = String(
|
||||
payload?.workflowKind
|
||||
@@ -2309,6 +2311,12 @@ export default function RipperPage({
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
const reassignJob = ripperJobs.find((item) => normalizeJobId(item?.id) === normalizeJobId(payload?.jobId)) || null;
|
||||
const reassignMediaProfile = String(
|
||||
reassignJob?.media_type
|
||||
|| reassignJob?.mediaType
|
||||
|| effectiveMetadataDialogContext?.mediaProfile
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
const currentWorkflowKind = String(
|
||||
reassignJob?.makemkvInfo?.analyzeContext?.selectedMetadata?.workflowKind
|
||||
|| reassignJob?.makemkvInfo?.analyzeContext?.workflowKind
|
||||
@@ -2331,7 +2339,8 @@ export default function RipperPage({
|
||||
&& providerRaw
|
||||
&& currentProvider !== providerRaw
|
||||
);
|
||||
if (shouldRestartReview) {
|
||||
const forceFilmMetadataPath = ['dvd', 'bluray'].includes(reassignMediaProfile) && nextWorkflowKind === 'film';
|
||||
if (duplicateAction || shouldRestartReview || forceFilmMetadataPath) {
|
||||
await api.selectMetadata(payload);
|
||||
} else {
|
||||
await api.assignJobOmdb(payload.jobId, payload);
|
||||
@@ -2355,58 +2364,7 @@ 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' || currentJobMediaProfile === 'bluray');
|
||||
|
||||
if (metadataDialogReassignMode) {
|
||||
await doSelectMetadata(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
// Serien-Discs laufen ohne Film-Duplikatdialog weiter; die Disk-Pruefung
|
||||
// erfolgt serverseitig gegen den vorhandenen Staffel-Container.
|
||||
if (isSeriesDvdPayload) {
|
||||
await doSelectMetadata(payload, { suppressErrorToast: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Duplikatprüfung: nur bei OMDB-Auswahl mit imdbId sinnvoll
|
||||
const searchTitle = payload.title || '';
|
||||
const searchImdbId = payload.imdbId || null;
|
||||
if (searchTitle) {
|
||||
try {
|
||||
const historyResponse = await api.getJobs({ search: searchTitle, limit: 50, lite: true });
|
||||
const historyJobs = Array.isArray(historyResponse?.jobs) ? historyResponse.jobs : [];
|
||||
const duplicate = historyJobs.find((job) => {
|
||||
if (normalizeJobId(job.id) === normalizeJobId(payload.jobId)) {
|
||||
return false; // aktueller Job selbst
|
||||
}
|
||||
// Gleicher Titel / imdbId?
|
||||
const titleMatch = searchImdbId
|
||||
? (job.imdb_id && job.imdb_id === searchImdbId)
|
||||
: (String(job.title || '').toLowerCase() === searchTitle.toLowerCase());
|
||||
if (!titleMatch) {
|
||||
return false;
|
||||
}
|
||||
// Gleiches Medium? Verschiedene Medien (DVD vs. Bluray) → kein Duplikat
|
||||
if (!currentJobMediaProfile || currentJobMediaProfile === 'other') {
|
||||
return false;
|
||||
}
|
||||
const jobMediaType = resolveMediaType(job);
|
||||
return jobMediaType === currentJobMediaProfile;
|
||||
});
|
||||
|
||||
if (duplicate) {
|
||||
setDuplicateJobDialog({ visible: true, existingJob: duplicate, pendingPayload: payload });
|
||||
return;
|
||||
}
|
||||
} catch (_error) {
|
||||
// Bei Fehler einfach fortfahren
|
||||
}
|
||||
}
|
||||
|
||||
await doSelectMetadata(payload);
|
||||
};
|
||||
|
||||
const handleMusicBrainzSearch = async (query) => {
|
||||
@@ -2527,8 +2485,7 @@ export default function RipperPage({
|
||||
.filter((drv) => !String(drv.path || '').startsWith('__virtual__'))
|
||||
.sort((a, b) => (a.path || '').localeCompare(b.path || ''));
|
||||
}, [knownDrives, pipeline?.cdDrives, pipeline?.detectedDiscs, device, state, lastNonCdDiscEvent]);
|
||||
const isDriveActive = driveActiveStates.includes(state);
|
||||
const canRescan = !isDriveActive;
|
||||
const canRescan = !busy;
|
||||
const canOpenMetadataModal = Boolean(defaultMetadataDialogContext?.jobId);
|
||||
const queueRunningJobs = Array.isArray(queueState?.runningJobs) ? queueState.runningJobs : [];
|
||||
const queueIdleJobs = Array.isArray(queueState?.idleJobs) ? queueState.idleJobs : [];
|
||||
@@ -2813,7 +2770,7 @@ export default function RipperPage({
|
||||
severity="secondary"
|
||||
className="drive-list-action-btn"
|
||||
loading={isRescanBusy}
|
||||
disabled={isRescanBusy || isDriveActive}
|
||||
disabled={isRescanBusy}
|
||||
onClick={() => handleRescanDrive(drivePath)}
|
||||
title="Laufwerk neu lesen"
|
||||
aria-label="Laufwerk neu lesen"
|
||||
@@ -3028,6 +2985,18 @@ export default function RipperPage({
|
||||
const seriesTitleSuffix = mediaIndicator.isSeriesDvd
|
||||
? `${seriesSeasonLabel}${seriesDiscLabel}${seriesContainerLabel}`
|
||||
: '';
|
||||
const jobKindRaw = String(job?.job_kind || '').trim().toLowerCase();
|
||||
const isMultipartMovie = Number(job?.is_multipart_movie || 0) === 1
|
||||
|| jobKindRaw === 'multipart_movie_child'
|
||||
|| jobKindRaw === 'multipart_movie_container';
|
||||
const multipartDiscNumber = Number(job?.disc_number || selectedMetadata?.discNumber || 0) || null;
|
||||
const multipartContainerId = normalizeJobId(job?.parent_job_id);
|
||||
const multipartSubtitle = isMultipartMovie
|
||||
? (
|
||||
`${multipartDiscNumber ? ` Disc ${multipartDiscNumber}` : ''}`
|
||||
+ `${multipartContainerId ? ` | Container #${multipartContainerId}` : ''}`
|
||||
).trim()
|
||||
: '';
|
||||
const mediaProfile = String(pipelineForJob?.context?.mediaProfile || '').trim().toLowerCase();
|
||||
const jobState = String(pipelineForJob?.state || normalizedStatus).trim().toUpperCase();
|
||||
const pipelineStage = String(pipelineForJob?.context?.stage || '').trim().toUpperCase();
|
||||
@@ -3106,9 +3075,13 @@ export default function RipperPage({
|
||||
{mediaIndicator.isSeriesDvd ? (
|
||||
<small className="ripper-job-subtitle">{seriesTitleSuffix || '-'}</small>
|
||||
) : null}
|
||||
{!mediaIndicator.isSeriesDvd && isMultipartMovie ? (
|
||||
<small className="ripper-job-subtitle">{multipartSubtitle || 'Multipart Movie'}</small>
|
||||
) : null}
|
||||
<div className="ripper-job-badges">
|
||||
<Tag value={statusBadgeValue} severity={statusBadgeSeverity} />
|
||||
{mediaIndicator.isSeriesDvd ? <Tag value={seriesTagLabel} severity="secondary" /> : null}
|
||||
{isMultipartMovie ? <Tag value="Multipart Movie" severity="secondary" /> : null}
|
||||
{isCurrentSession ? <Tag value="Aktive Session" severity="info" /> : null}
|
||||
{isResumable ? <Tag value="Fortsetzbar" severity="success" /> : null}
|
||||
{normalizedStatus === 'READY_TO_ENCODE'
|
||||
@@ -3245,6 +3218,9 @@ export default function RipperPage({
|
||||
+ (mediaIndicator.isSeriesDvd ? seriesDiscLabel : '')
|
||||
+ (mediaIndicator.isSeriesDvd ? seriesContainerLabel : '')
|
||||
+ (mediaIndicator.isSeriesDvd ? seriesTmdbLabel : '')
|
||||
+ (!mediaIndicator.isSeriesDvd && isMultipartMovie
|
||||
? `${multipartDiscNumber ? ` | Disc ${multipartDiscNumber}` : ''}${multipartContainerId ? ` | Container #${multipartContainerId}` : ''}`
|
||||
: '')
|
||||
+ (mediaIndicator.isSeriesDvd ? '' : `${job?.imdb_id ? ` | ${job.imdb_id}` : ''}`)
|
||||
))
|
||||
)}
|
||||
@@ -3253,6 +3229,7 @@ export default function RipperPage({
|
||||
<div className="ripper-job-badges">
|
||||
<Tag value={statusBadgeValue} severity={statusBadgeSeverity} />
|
||||
{mediaIndicator.isSeriesDvd ? <Tag value={seriesTagLabel} severity="secondary" /> : null}
|
||||
{isMultipartMovie ? <Tag value="Multipart Movie" severity="secondary" /> : null}
|
||||
{isCurrentSession ? <Tag value="Aktive Session" severity="info" /> : null}
|
||||
{isResumable ? <Tag value="Fortsetzbar" severity="success" /> : null}
|
||||
{normalizedStatus === 'READY_TO_ENCODE'
|
||||
@@ -3334,12 +3311,12 @@ export default function RipperPage({
|
||||
<div className="pipeline-queue-item running queue-job-item">
|
||||
<div className="pipeline-queue-item-main">
|
||||
<strong>
|
||||
#{item.jobId} | {item.title || `Job #${item.jobId}`}
|
||||
{item.title || `Job #${item.jobId}`}
|
||||
{item.hasScripts ? <i className="pi pi-code queue-job-tag" title="Skripte hinterlegt" /> : null}
|
||||
{item.hasChains ? <i className="pi pi-link queue-job-tag" title="Skriptketten hinterlegt" /> : null}
|
||||
</strong>
|
||||
{item.subtitle ? <small>{item.subtitle}</small> : null}
|
||||
<small>{getStatusLabel(item.status)}</small>
|
||||
<small>#{item.jobId} | {getStatusLabel(item.status)}</small>
|
||||
{clampedQueueProgress > 0 && (
|
||||
<ProgressBar value={clampedQueueProgress} style={{ height: '5px' }} showValue={false} />
|
||||
)}
|
||||
@@ -3451,12 +3428,12 @@ export default function RipperPage({
|
||||
) : (
|
||||
<>
|
||||
<strong>
|
||||
{item.position || '-'} | #{item.jobId} | {item.title || `Job #${item.jobId}`}
|
||||
{item.title || `Job #${item.jobId}`}
|
||||
{item.hasScripts ? <i className="pi pi-code queue-job-tag" title="Skripte hinterlegt" /> : null}
|
||||
{item.hasChains ? <i className="pi pi-link queue-job-tag" title="Skriptketten hinterlegt" /> : null}
|
||||
</strong>
|
||||
{item.subtitle ? <small>{item.subtitle}</small> : null}
|
||||
<small>{item.actionLabel || item.action || '-'} | {getStatusLabel(item.status)}</small>
|
||||
<small>{item.position || '-'} | #{item.jobId} | {item.actionLabel || item.action || '-'} | {getStatusLabel(item.status)}</small>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -3848,69 +3825,6 @@ export default function RipperPage({
|
||||
busy={busy}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
header="Titel bereits in der Historie"
|
||||
visible={duplicateJobDialog.visible}
|
||||
onHide={() => setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null })}
|
||||
style={{ width: '30rem', maxWidth: '96vw' }}
|
||||
modal
|
||||
>
|
||||
{(() => {
|
||||
const pendingPayload = duplicateJobDialog.pendingPayload || {};
|
||||
const metadataProvider = String(pendingPayload?.metadataProvider || '').trim().toLowerCase();
|
||||
const seasonNumber = Number(pendingPayload?.seasonNumber || 0) || 0;
|
||||
const tmdbId = Number(pendingPayload?.tmdbId || 0) || 0;
|
||||
const isSeriesContainerAssign = metadataProvider === 'tmdb' && seasonNumber > 0 && tmdbId > 0;
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<strong>{duplicateJobDialog.existingJob?.title || duplicateJobDialog.pendingPayload?.title}</strong> ist bereits als Job #{duplicateJobDialog.existingJob?.id} in der Historie vorhanden.
|
||||
</p>
|
||||
<p>
|
||||
{isSeriesContainerAssign
|
||||
? 'Dieser Job kann dem vorhandenen Serien-Container automatisch zugewiesen werden.'
|
||||
: 'Neuen Job anlegen oder mit dem vorhandenen Eintrag weiterarbeiten?'}
|
||||
</p>
|
||||
<div className="dialog-actions">
|
||||
<Button
|
||||
label={isSeriesContainerAssign ? 'Vorhandenem Container zuweisen' : 'Vorhandenen Job öffnen'}
|
||||
icon={isSeriesContainerAssign ? 'pi pi-sitemap' : 'pi pi-history'}
|
||||
onClick={async () => {
|
||||
const payload = duplicateJobDialog.pendingPayload;
|
||||
const jobId = duplicateJobDialog.existingJob?.id;
|
||||
setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null });
|
||||
if (isSeriesContainerAssign) {
|
||||
await doSelectMetadata(payload);
|
||||
return;
|
||||
}
|
||||
navigate(`/history?open=${jobId}`);
|
||||
}}
|
||||
/>
|
||||
{isSeriesContainerAssign ? (
|
||||
<Button
|
||||
label="Abbrechen"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={() => setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null })}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
label="Neuen Job anlegen"
|
||||
severity="secondary"
|
||||
outlined
|
||||
onClick={async () => {
|
||||
const payload = duplicateJobDialog.pendingPayload;
|
||||
setDuplicateJobDialog({ visible: false, existingJob: null, pendingPayload: null });
|
||||
await doSelectMetadata(payload);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
header={cancelCleanupDialog?.target === 'raw' ? 'Rip abgebrochen' : 'Encode abgebrochen'}
|
||||
visible={Boolean(cancelCleanupDialog.visible)}
|
||||
|
||||
@@ -55,7 +55,21 @@ function normalizeJobKind(value) {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
if (['audiobook', 'cd', 'dvd', 'bluray', 'converter_audio', 'converter_video', 'converter_iso'].includes(raw)) {
|
||||
if (
|
||||
[
|
||||
'audiobook',
|
||||
'cd',
|
||||
'dvd',
|
||||
'bluray',
|
||||
'dvd_series_container',
|
||||
'dvd_series_child',
|
||||
'multipart_movie_container',
|
||||
'multipart_movie_child',
|
||||
'converter_audio',
|
||||
'converter_video',
|
||||
'converter_iso'
|
||||
].includes(raw)
|
||||
) {
|
||||
return raw;
|
||||
}
|
||||
if (raw === 'converter') {
|
||||
@@ -92,6 +106,14 @@ function mediaTypeFromJobKind(jobKind) {
|
||||
if (normalized.startsWith('converter_')) {
|
||||
return 'converter';
|
||||
}
|
||||
if (
|
||||
normalized === 'dvd_series_container'
|
||||
|| normalized === 'dvd_series_child'
|
||||
|| normalized === 'multipart_movie_container'
|
||||
|| normalized === 'multipart_movie_child'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ nav:
|
||||
- Erster Lauf: getting-started/quickstart.md
|
||||
- GUI-Seiten:
|
||||
- gui/index.md
|
||||
- Jobs: gui/jobs.md
|
||||
- Ripper: gui/ripper.md
|
||||
- Audiobooks: gui/audiobooks.md
|
||||
- Settings: gui/settings.md
|
||||
- Historie: gui/history.md
|
||||
- Converter: gui/converter.md
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ripster",
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ripster",
|
||||
"private": true,
|
||||
"version": "0.14.0-3",
|
||||
"version": "0.14.0-4",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||
"dev:backend": "npm run dev --prefix backend",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
+16
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* Lunr languages, `Danish` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.da=function(){this.pipeline.reset(),this.pipeline.add(e.da.trimmer,e.da.stopWordFilter,e.da.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.da.stemmer))},e.da.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.da.trimmer=e.trimmerSupport.generateTrimmer(e.da.wordCharacters),e.Pipeline.registerFunction(e.da.trimmer,"trimmer-da"),e.da.stemmer=function(){var r=e.stemmerSupport.Among,i=e.stemmerSupport.SnowballProgram,n=new function(){function e(){var e,r=f.cursor+3;if(d=f.limit,0<=r&&r<=f.limit){for(a=r;;){if(e=f.cursor,f.in_grouping(w,97,248)){f.cursor=e;break}if(f.cursor=e,e>=f.limit)return;f.cursor++}for(;!f.out_grouping(w,97,248);){if(f.cursor>=f.limit)return;f.cursor++}d=f.cursor,d<a&&(d=a)}}function n(){var e,r;if(f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(c,32),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del();break;case 2:f.in_grouping_b(p,97,229)&&f.slice_del()}}function t(){var e,r=f.limit-f.cursor;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.find_among_b(l,4)?(f.bra=f.cursor,f.limit_backward=e,f.cursor=f.limit-r,f.cursor>f.limit_backward&&(f.cursor--,f.bra=f.cursor,f.slice_del())):f.limit_backward=e)}function s(){var e,r,i,n=f.limit-f.cursor;if(f.ket=f.cursor,f.eq_s_b(2,"st")&&(f.bra=f.cursor,f.eq_s_b(2,"ig")&&f.slice_del()),f.cursor=f.limit-n,f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(m,5),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del(),i=f.limit-f.cursor,t(),f.cursor=f.limit-i;break;case 2:f.slice_from("løs")}}function o(){var e;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.out_grouping_b(w,97,248)?(f.bra=f.cursor,u=f.slice_to(u),f.limit_backward=e,f.eq_v_b(u)&&f.slice_del()):f.limit_backward=e)}var a,d,u,c=[new r("hed",-1,1),new r("ethed",0,1),new r("ered",-1,1),new r("e",-1,1),new r("erede",3,1),new r("ende",3,1),new r("erende",5,1),new r("ene",3,1),new r("erne",3,1),new r("ere",3,1),new r("en",-1,1),new r("heden",10,1),new r("eren",10,1),new r("er",-1,1),new r("heder",13,1),new r("erer",13,1),new r("s",-1,2),new r("heds",16,1),new r("es",16,1),new r("endes",18,1),new r("erendes",19,1),new r("enes",18,1),new r("ernes",18,1),new r("eres",18,1),new r("ens",16,1),new r("hedens",24,1),new r("erens",24,1),new r("ers",16,1),new r("ets",16,1),new r("erets",28,1),new r("et",-1,1),new r("eret",30,1)],l=[new r("gd",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("elig",1,1),new r("els",-1,1),new r("løst",-1,2)],w=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],p=[239,254,42,3,0,0,0,0,0,0,0,0,0,0,0,0,16],f=new i;this.setCurrent=function(e){f.setCurrent(e)},this.getCurrent=function(){return f.getCurrent()},this.stem=function(){var r=f.cursor;return e(),f.limit_backward=r,f.cursor=f.limit,n(),f.cursor=f.limit,t(),f.cursor=f.limit,s(),f.cursor=f.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.da.stemmer,"stemmer-da"),e.da.stopWordFilter=e.generateStopWordFilter("ad af alle alt anden at blev blive bliver da de dem den denne der deres det dette dig din disse dog du efter eller en end er et for fra ham han hans har havde have hende hendes her hos hun hvad hvis hvor i ikke ind jeg jer jo kunne man mange med meget men mig min mine mit mod ned noget nogle nu når og også om op os over på selv sig sin sine sit skal skulle som sådan thi til ud under var vi vil ville vor være været".split(" ")),e.Pipeline.registerFunction(e.da.stopWordFilter,"stopWordFilter-da")}});
|
||||
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hi=function(){this.pipeline.reset(),this.pipeline.add(e.hi.trimmer,e.hi.stopWordFilter,e.hi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hi.stemmer))},e.hi.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿa-zA-Za-zA-Z0-90-9",e.hi.trimmer=e.trimmerSupport.generateTrimmer(e.hi.wordCharacters),e.Pipeline.registerFunction(e.hi.trimmer,"trimmer-hi"),e.hi.stopWordFilter=e.generateStopWordFilter("अत अपना अपनी अपने अभी अंदर आदि आप इत्यादि इन इनका इन्हीं इन्हें इन्हों इस इसका इसकी इसके इसमें इसी इसे उन उनका उनकी उनके उनको उन्हीं उन्हें उन्हों उस उसके उसी उसे एक एवं एस ऐसे और कई कर करता करते करना करने करें कहते कहा का काफ़ी कि कितना किन्हें किन्हों किया किर किस किसी किसे की कुछ कुल के को कोई कौन कौनसा गया घर जब जहाँ जा जितना जिन जिन्हें जिन्हों जिस जिसे जीधर जैसा जैसे जो तक तब तरह तिन तिन्हें तिन्हों तिस तिसे तो था थी थे दबारा दिया दुसरा दूसरे दो द्वारा न नके नहीं ना निहायत नीचे ने पर पहले पूरा पे फिर बनी बही बहुत बाद बाला बिलकुल भी भीतर मगर मानो मे में यदि यह यहाँ यही या यिह ये रखें रहा रहे ऱ्वासा लिए लिये लेकिन व वग़ैरह वर्ग वह वहाँ वहीं वाले वुह वे वो सकता सकते सबसे सभी साथ साबुत साभ सारा से सो संग ही हुआ हुई हुए है हैं हो होता होती होते होना होने".split(" ")),e.hi.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.hi.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var t=i.toString().toLowerCase().replace(/^\s+/,"");return r.cut(t).split("|")},e.Pipeline.registerFunction(e.hi.stemmer,"stemmer-hi"),e.Pipeline.registerFunction(e.hi.stopWordFilter,"stopWordFilter-hi")}});
|
||||
+18
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hy=function(){this.pipeline.reset(),this.pipeline.add(e.hy.trimmer,e.hy.stopWordFilter)},e.hy.wordCharacters="[A-Za-z-֏ff-ﭏ]",e.hy.trimmer=e.trimmerSupport.generateTrimmer(e.hy.wordCharacters),e.Pipeline.registerFunction(e.hy.trimmer,"trimmer-hy"),e.hy.stopWordFilter=e.generateStopWordFilter("դու և եք էիր էիք հետո նաև նրանք որը վրա է որ պիտի են այս մեջ ն իր ու ի այդ որոնք այն կամ էր մի ես համար այլ իսկ էին ենք հետ ին թ էինք մենք նրա նա դուք եմ էի ըստ որպես ում".split(" ")),e.Pipeline.registerFunction(e.hy.stopWordFilter,"stopWordFilter-hy"),e.hy.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}(),e.Pipeline.registerFunction(e.hy.stemmer,"stemmer-hy")}});
|
||||
+18
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.ja=function(){this.pipeline.reset(),this.pipeline.add(e.ja.trimmer,e.ja.stopWordFilter,e.ja.stemmer),r?this.tokenizer=e.ja.tokenizer:(e.tokenizer&&(e.tokenizer=e.ja.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.ja.tokenizer))};var t=new e.TinySegmenter;e.ja.tokenizer=function(i){var n,o,s,p,a,u,m,l,c,f;if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t.toLowerCase()):t.toLowerCase()});for(o=i.toString().toLowerCase().replace(/^\s+/,""),n=o.length-1;n>=0;n--)if(/\S/.test(o.charAt(n))){o=o.substring(0,n+1);break}for(a=[],s=o.length,c=0,l=0;c<=s;c++)if(u=o.charAt(c),m=c-l,u.match(/\s/)||c==s){if(m>0)for(p=t.segment(o.slice(l,c)).filter(function(e){return!!e}),f=l,n=0;n<p.length;n++)r?a.push(new e.Token(p[n],{position:[f,p[n].length],index:a.length})):a.push(p[n]),f+=p[n].length;l=c+1}return a},e.ja.stemmer=function(){return function(e){return e}}(),e.Pipeline.registerFunction(e.ja.stemmer,"stemmer-ja"),e.ja.wordCharacters="一二三四五六七八九十百千万億兆一-龠々〆ヵヶぁ-んァ-ヴーア-ン゙a-zA-Za-zA-Z0-90-9",e.ja.trimmer=e.trimmerSupport.generateTrimmer(e.ja.wordCharacters),e.Pipeline.registerFunction(e.ja.trimmer,"trimmer-ja"),e.ja.stopWordFilter=e.generateStopWordFilter("これ それ あれ この その あの ここ そこ あそこ こちら どこ だれ なに なん 何 私 貴方 貴方方 我々 私達 あの人 あのかた 彼女 彼 です あります おります います は が の に を で え から まで より も どの と し それで しかし".split(" ")),e.Pipeline.registerFunction(e.ja.stopWordFilter,"stopWordFilter-ja"),e.jp=e.ja,e.Pipeline.registerFunction(e.jp.stemmer,"stemmer-jp"),e.Pipeline.registerFunction(e.jp.trimmer,"trimmer-jp"),e.Pipeline.registerFunction(e.jp.stopWordFilter,"stopWordFilter-jp")}});
|
||||
@@ -0,0 +1 @@
|
||||
module.exports=require("./lunr.ja");
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.kn=function(){this.pipeline.reset(),this.pipeline.add(e.kn.trimmer,e.kn.stopWordFilter,e.kn.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.kn.stemmer))},e.kn.wordCharacters="ಀ-಄ಅ-ಔಕ-ಹಾ-ೌ಼-ಽೕ-ೖೝ-ೞೠ-ೡೢ-ೣ೦-೯ೱ-ೳ",e.kn.trimmer=e.trimmerSupport.generateTrimmer(e.kn.wordCharacters),e.Pipeline.registerFunction(e.kn.trimmer,"trimmer-kn"),e.kn.stopWordFilter=e.generateStopWordFilter("ಮತ್ತು ಈ ಒಂದು ರಲ್ಲಿ ಹಾಗೂ ಎಂದು ಅಥವಾ ಇದು ರ ಅವರು ಎಂಬ ಮೇಲೆ ಅವರ ತನ್ನ ಆದರೆ ತಮ್ಮ ನಂತರ ಮೂಲಕ ಹೆಚ್ಚು ನ ಆ ಕೆಲವು ಅನೇಕ ಎರಡು ಹಾಗು ಪ್ರಮುಖ ಇದನ್ನು ಇದರ ಸುಮಾರು ಅದರ ಅದು ಮೊದಲ ಬಗ್ಗೆ ನಲ್ಲಿ ರಂದು ಇತರ ಅತ್ಯಂತ ಹೆಚ್ಚಿನ ಸಹ ಸಾಮಾನ್ಯವಾಗಿ ನೇ ಹಲವಾರು ಹೊಸ ದಿ ಕಡಿಮೆ ಯಾವುದೇ ಹೊಂದಿದೆ ದೊಡ್ಡ ಅನ್ನು ಇವರು ಪ್ರಕಾರ ಇದೆ ಮಾತ್ರ ಕೂಡ ಇಲ್ಲಿ ಎಲ್ಲಾ ವಿವಿಧ ಅದನ್ನು ಹಲವು ರಿಂದ ಕೇವಲ ದ ದಕ್ಷಿಣ ಗೆ ಅವನ ಅತಿ ನೆಯ ಬಹಳ ಕೆಲಸ ಎಲ್ಲ ಪ್ರತಿ ಇತ್ಯಾದಿ ಇವು ಬೇರೆ ಹೀಗೆ ನಡುವೆ ಇದಕ್ಕೆ ಎಸ್ ಇವರ ಮೊದಲು ಶ್ರೀ ಮಾಡುವ ಇದರಲ್ಲಿ ರೀತಿಯ ಮಾಡಿದ ಕಾಲ ಅಲ್ಲಿ ಮಾಡಲು ಅದೇ ಈಗ ಅವು ಗಳು ಎ ಎಂಬುದು ಅವನು ಅಂದರೆ ಅವರಿಗೆ ಇರುವ ವಿಶೇಷ ಮುಂದೆ ಅವುಗಳ ಮುಂತಾದ ಮೂಲ ಬಿ ಮೀ ಒಂದೇ ಇನ್ನೂ ಹೆಚ್ಚಾಗಿ ಮಾಡಿ ಅವರನ್ನು ಇದೇ ಯ ರೀತಿಯಲ್ಲಿ ಜೊತೆ ಅದರಲ್ಲಿ ಮಾಡಿದರು ನಡೆದ ಆಗ ಮತ್ತೆ ಪೂರ್ವ ಆತ ಬಂದ ಯಾವ ಒಟ್ಟು ಇತರೆ ಹಿಂದೆ ಪ್ರಮಾಣದ ಗಳನ್ನು ಕುರಿತು ಯು ಆದ್ದರಿಂದ ಅಲ್ಲದೆ ನಗರದ ಮೇಲಿನ ಏಕೆಂದರೆ ರಷ್ಟು ಎಂಬುದನ್ನು ಬಾರಿ ಎಂದರೆ ಹಿಂದಿನ ಆದರೂ ಆದ ಸಂಬಂಧಿಸಿದ ಮತ್ತೊಂದು ಸಿ ಆತನ ".split(" ")),e.kn.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.kn.tokenizer=function(t){if(!arguments.length||null==t||void 0==t)return[];if(Array.isArray(t))return t.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var n=t.toString().toLowerCase().replace(/^\s+/,"");return r.cut(n).split("|")},e.Pipeline.registerFunction(e.kn.stemmer,"stemmer-kn"),e.Pipeline.registerFunction(e.kn.stopWordFilter,"stopWordFilter-kn")}});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){e.multiLanguage=function(){for(var t=Array.prototype.slice.call(arguments),i=t.join("-"),r="",n=[],s=[],p=0;p<t.length;++p)"en"==t[p]?(r+="\\w",n.unshift(e.stopWordFilter),n.push(e.stemmer),s.push(e.stemmer)):(r+=e[t[p]].wordCharacters,e[t[p]].stopWordFilter&&n.unshift(e[t[p]].stopWordFilter),e[t[p]].stemmer&&(n.push(e[t[p]].stemmer),s.push(e[t[p]].stemmer)));var o=e.trimmerSupport.generateTrimmer(r);return e.Pipeline.registerFunction(o,"lunr-multi-trimmer-"+i),n.unshift(o),function(){this.pipeline.reset(),this.pipeline.add.apply(this.pipeline,n),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add.apply(this.searchPipeline,s))}}}});
|
||||
+18
File diff suppressed because one or more lines are too long
+18
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* Lunr languages, `Norwegian` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.no=function(){this.pipeline.reset(),this.pipeline.add(e.no.trimmer,e.no.stopWordFilter,e.no.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.no.stemmer))},e.no.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.no.trimmer=e.trimmerSupport.generateTrimmer(e.no.wordCharacters),e.Pipeline.registerFunction(e.no.trimmer,"trimmer-no"),e.no.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,r=w.cursor+3;if(a=w.limit,0<=r||r<=w.limit){for(s=r;;){if(e=w.cursor,w.in_grouping(d,97,248)){w.cursor=e;break}if(e>=w.limit)return;w.cursor=e+1}for(;!w.out_grouping(d,97,248);){if(w.cursor>=w.limit)return;w.cursor++}a=w.cursor,a<s&&(a=s)}}function i(){var e,r,n;if(w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(m,29),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:n=w.limit-w.cursor,w.in_grouping_b(c,98,122)?w.slice_del():(w.cursor=w.limit-n,w.eq_s_b(1,"k")&&w.out_grouping_b(d,97,248)&&w.slice_del());break;case 3:w.slice_from("er")}}function t(){var e,r=w.limit-w.cursor;w.cursor>=a&&(e=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,w.find_among_b(u,2)?(w.bra=w.cursor,w.limit_backward=e,w.cursor=w.limit-r,w.cursor>w.limit_backward&&(w.cursor--,w.bra=w.cursor,w.slice_del())):w.limit_backward=e)}function o(){var e,r;w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(l,11),e?(w.bra=w.cursor,w.limit_backward=r,1==e&&w.slice_del()):w.limit_backward=r)}var s,a,m=[new r("a",-1,1),new r("e",-1,1),new r("ede",1,1),new r("ande",1,1),new r("ende",1,1),new r("ane",1,1),new r("ene",1,1),new r("hetene",6,1),new r("erte",1,3),new r("en",-1,1),new r("heten",9,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",12,1),new r("s",-1,2),new r("as",14,1),new r("es",14,1),new r("edes",16,1),new r("endes",16,1),new r("enes",16,1),new r("hetenes",19,1),new r("ens",14,1),new r("hetens",21,1),new r("ers",14,1),new r("ets",14,1),new r("et",-1,1),new r("het",25,1),new r("ert",-1,3),new r("ast",-1,1)],u=[new r("dt",-1,-1),new r("vt",-1,-1)],l=[new r("leg",-1,1),new r("eleg",0,1),new r("ig",-1,1),new r("eig",2,1),new r("lig",2,1),new r("elig",4,1),new r("els",-1,1),new r("lov",-1,1),new r("elov",7,1),new r("slov",7,1),new r("hetslov",9,1)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],c=[119,125,149,1],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,i(),w.cursor=w.limit,t(),w.cursor=w.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.no.stemmer,"stemmer-no"),e.no.stopWordFilter=e.generateStopWordFilter("alle at av bare begge ble blei bli blir blitt både båe da de deg dei deim deira deires dem den denne der dere deres det dette di din disse ditt du dykk dykkar då eg ein eit eitt eller elles en enn er et ett etter for fordi fra før ha hadde han hans har hennar henne hennes her hjå ho hoe honom hoss hossen hun hva hvem hver hvilke hvilken hvis hvor hvordan hvorfor i ikke ikkje ikkje ingen ingi inkje inn inni ja jeg kan kom korleis korso kun kunne kva kvar kvarhelst kven kvi kvifor man mange me med medan meg meget mellom men mi min mine mitt mot mykje ned no noe noen noka noko nokon nokor nokre nå når og også om opp oss over på samme seg selv si si sia sidan siden sin sine sitt sjøl skal skulle slik so som som somme somt så sånn til um upp ut uten var vart varte ved vere verte vi vil ville vore vors vort vår være være vært å".split(" ")),e.Pipeline.registerFunction(e.no.stopWordFilter,"stopWordFilter-no")}});
|
||||
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
+18
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sa=function(){this.pipeline.reset(),this.pipeline.add(e.sa.trimmer,e.sa.stopWordFilter,e.sa.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sa.stemmer))},e.sa.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿ꣠-꣱ꣲ-ꣷ꣸-ꣻ꣼-ꣽꣾ-ꣿᆰ0-ᆰ9",e.sa.trimmer=e.trimmerSupport.generateTrimmer(e.sa.wordCharacters),e.Pipeline.registerFunction(e.sa.trimmer,"trimmer-sa"),e.sa.stopWordFilter=e.generateStopWordFilter('तथा अयम् एकम् इत्यस्मिन् तथा तत् वा अयम् इत्यस्य ते आहूत उपरि तेषाम् किन्तु तेषाम् तदा इत्यनेन अधिकः इत्यस्य तत् केचन बहवः द्वि तथा महत्वपूर्णः अयम् अस्य विषये अयं अस्ति तत् प्रथमः विषये इत्युपरि इत्युपरि इतर अधिकतमः अधिकः अपि सामान्यतया ठ इतरेतर नूतनम् द न्यूनम् कश्चित् वा विशालः द सः अस्ति तदनुसारम् तत्र अस्ति केवलम् अपि अत्र सर्वे विविधाः तत् बहवः यतः इदानीम् द दक्षिण इत्यस्मै तस्य उपरि नथ अतीव कार्यम् सर्वे एकैकम् इत्यादि। एते सन्ति उत इत्थम् मध्ये एतदर्थं . स कस्य प्रथमः श्री. करोति अस्मिन् प्रकारः निर्मिता कालः तत्र कर्तुं समान अधुना ते सन्ति स एकः अस्ति सः अर्थात् तेषां कृते . स्थितम् विशेषः अग्रिम तेषाम् समान स्रोतः ख म समान इदानीमपि अधिकतया करोतु ते समान इत्यस्य वीथी सह यस्मिन् कृतवान् धृतः तदा पुनः पूर्वं सः आगतः किम् कुल इतर पुरा मात्रा स विषये उ अतएव अपि नगरस्य उपरि यतः प्रतिशतं कतरः कालः साधनानि भूत तथापि जात सम्बन्धि अन्यत् ग अतः अस्माकं स्वकीयाः अस्माकं इदानीं अन्तः इत्यादयः भवन्तः इत्यादयः एते एताः तस्य अस्य इदम् एते तेषां तेषां तेषां तान् तेषां तेषां तेषां समानः सः एकः च तादृशाः बहवः अन्ये च वदन्ति यत् कियत् कस्मै कस्मै यस्मै यस्मै यस्मै यस्मै न अतिनीचः किन्तु प्रथमं सम्पूर्णतया ततः चिरकालानन्तरं पुस्तकं सम्पूर्णतया अन्तः किन्तु अत्र वा इह इव श्रद्धाय अवशिष्यते परन्तु अन्ये वर्गाः सन्ति ते सन्ति शक्नुवन्ति सर्वे मिलित्वा सर्वे एकत्र"'.split(" ")),e.sa.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.sa.tokenizer=function(t){if(!arguments.length||null==t||void 0==t)return[];if(Array.isArray(t))return t.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var i=t.toString().toLowerCase().replace(/^\s+/,"");return r.cut(i).split("|")},e.Pipeline.registerFunction(e.sa.stemmer,"stemmer-sa"),e.Pipeline.registerFunction(e.sa.stopWordFilter,"stopWordFilter-sa")}});
|
||||
@@ -0,0 +1 @@
|
||||
!function(r,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(r.lunr)}(this,function(){return function(r){r.stemmerSupport={Among:function(r,t,i,s){if(this.toCharArray=function(r){for(var t=r.length,i=new Array(t),s=0;s<t;s++)i[s]=r.charCodeAt(s);return i},!r&&""!=r||!t&&0!=t||!i)throw"Bad Among initialisation: s:"+r+", substring_i: "+t+", result: "+i;this.s_size=r.length,this.s=this.toCharArray(r),this.substring_i=t,this.result=i,this.method=s},SnowballProgram:function(){var r;return{bra:0,ket:0,limit:0,cursor:0,limit_backward:0,setCurrent:function(t){r=t,this.cursor=0,this.limit=t.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},getCurrent:function(){var t=r;return r=null,t},in_grouping:function(t,i,s){if(this.cursor<this.limit){var e=r.charCodeAt(this.cursor);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},in_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},out_grouping:function(t,i,s){if(this.cursor<this.limit){var e=r.charCodeAt(this.cursor);if(e>s||e<i)return this.cursor++,!0;if(e-=i,!(t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},out_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e>s||e<i)return this.cursor--,!0;if(e-=i,!(t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},eq_s:function(t,i){if(this.limit-this.cursor<t)return!1;for(var s=0;s<t;s++)if(r.charCodeAt(this.cursor+s)!=i.charCodeAt(s))return!1;return this.cursor+=t,!0},eq_s_b:function(t,i){if(this.cursor-this.limit_backward<t)return!1;for(var s=0;s<t;s++)if(r.charCodeAt(this.cursor-t+s)!=i.charCodeAt(s))return!1;return this.cursor-=t,!0},find_among:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o<h?o:h,_=t[a],m=l;m<_.s_size;m++){if(n+l==u){f=-1;break}if(f=r.charCodeAt(n+l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n+_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n+_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},find_among_b:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit_backward,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o<h?o:h,_=t[a],m=_.s_size-1-l;m>=0;m--){if(n-l==u){f=-1;break}if(f=r.charCodeAt(n-1-l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n-_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n-_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},replace_s:function(t,i,s){var e=s.length-(i-t),n=r.substring(0,t),u=r.substring(i);return r=n+s+u,this.limit+=e,this.cursor>=i?this.cursor+=e:this.cursor>t&&(this.cursor=t),e},slice_check:function(){if(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>r.length)throw"faulty slice operation"},slice_from:function(r){this.slice_check(),this.replace_s(this.bra,this.ket,r)},slice_del:function(){this.slice_from("")},insert:function(r,t,i){var s=this.replace_s(r,t,i);r<=this.bra&&(this.bra+=s),r<=this.ket&&(this.ket+=s)},slice_to:function(){return this.slice_check(),r.substring(this.bra,this.ket)},eq_v_b:function(r){return this.eq_s_b(r.length,r)}}}},r.trimmerSupport={generateTrimmer:function(r){var t=new RegExp("^[^"+r+"]+"),i=new RegExp("[^"+r+"]+$");return function(r){return"function"==typeof r.update?r.update(function(r){return r.replace(t,"").replace(i,"")}):r.replace(t,"").replace(i,"")}}}}});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* Lunr languages, `Swedish` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sv=function(){this.pipeline.reset(),this.pipeline.add(e.sv.trimmer,e.sv.stopWordFilter,e.sv.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sv.stemmer))},e.sv.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.sv.trimmer=e.trimmerSupport.generateTrimmer(e.sv.wordCharacters),e.Pipeline.registerFunction(e.sv.trimmer,"trimmer-sv"),e.sv.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,t=new function(){function e(){var e,r=w.cursor+3;if(o=w.limit,0<=r||r<=w.limit){for(a=r;;){if(e=w.cursor,w.in_grouping(l,97,246)){w.cursor=e;break}if(w.cursor=e,w.cursor>=w.limit)return;w.cursor++}for(;!w.out_grouping(l,97,246);){if(w.cursor>=w.limit)return;w.cursor++}o=w.cursor,o<a&&(o=a)}}function t(){var e,r=w.limit_backward;if(w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(u,37),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.in_grouping_b(d,98,121)&&w.slice_del()}}function i(){var e=w.limit_backward;w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.find_among_b(c,7)&&(w.cursor=w.limit,w.ket=w.cursor,w.cursor>w.limit_backward&&(w.bra=--w.cursor,w.slice_del())),w.limit_backward=e)}function s(){var e,r;if(w.cursor>=o){if(r=w.limit_backward,w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(m,5))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.slice_from("lös");break;case 3:w.slice_from("full")}w.limit_backward=r}}var a,o,u=[new r("a",-1,1),new r("arna",0,1),new r("erna",0,1),new r("heterna",2,1),new r("orna",0,1),new r("ad",-1,1),new r("e",-1,1),new r("ade",6,1),new r("ande",6,1),new r("arne",6,1),new r("are",6,1),new r("aste",6,1),new r("en",-1,1),new r("anden",12,1),new r("aren",12,1),new r("heten",12,1),new r("ern",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",18,1),new r("or",-1,1),new r("s",-1,2),new r("as",21,1),new r("arnas",22,1),new r("ernas",22,1),new r("ornas",22,1),new r("es",21,1),new r("ades",26,1),new r("andes",26,1),new r("ens",21,1),new r("arens",29,1),new r("hetens",29,1),new r("erns",21,1),new r("at",-1,1),new r("andet",-1,1),new r("het",-1,1),new r("ast",-1,1)],c=[new r("dd",-1,-1),new r("gd",-1,-1),new r("nn",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1),new r("tt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("els",-1,1),new r("fullt",-1,3),new r("löst",-1,2)],l=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,24,0,32],d=[119,127,149],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,t(),w.cursor=w.limit,i(),w.cursor=w.limit,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return t.setCurrent(e),t.stem(),t.getCurrent()}):(t.setCurrent(e),t.stem(),t.getCurrent())}}(),e.Pipeline.registerFunction(e.sv.stemmer,"stemmer-sv"),e.sv.stopWordFilter=e.generateStopWordFilter("alla allt att av blev bli blir blivit de dem den denna deras dess dessa det detta dig din dina ditt du där då efter ej eller en er era ert ett från för ha hade han hans har henne hennes hon honom hur här i icke ingen inom inte jag ju kan kunde man med mellan men mig min mina mitt mot mycket ni nu när någon något några och om oss på samma sedan sig sin sina sitta själv skulle som så sådan sådana sådant till under upp ut utan vad var vara varför varit varje vars vart vem vi vid vilka vilkas vilken vilket vår våra vårt än är åt över".split(" ")),e.Pipeline.registerFunction(e.sv.stopWordFilter,"stopWordFilter-sv")}});
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ta=function(){this.pipeline.reset(),this.pipeline.add(e.ta.trimmer,e.ta.stopWordFilter,e.ta.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ta.stemmer))},e.ta.wordCharacters="-உஊ-ஏஐ-ஙச-ட-னப-யர-ஹ-ிீ-ொ-ௐ---௩௪-௯௰-௹௺-a-zA-Za-zA-Z0-90-9",e.ta.trimmer=e.trimmerSupport.generateTrimmer(e.ta.wordCharacters),e.Pipeline.registerFunction(e.ta.trimmer,"trimmer-ta"),e.ta.stopWordFilter=e.generateStopWordFilter("அங்கு அங்கே அது அதை அந்த அவர் அவர்கள் அவள் அவன் அவை ஆக ஆகவே ஆகையால் ஆதலால் ஆதலினால் ஆனாலும் ஆனால் இங்கு இங்கே இது இதை இந்த இப்படி இவர் இவர்கள் இவள் இவன் இவை இவ்வளவு உனக்கு உனது உன் உன்னால் எங்கு எங்கே எது எதை எந்த எப்படி எவர் எவர்கள் எவள் எவன் எவை எவ்வளவு எனக்கு எனது எனவே என் என்ன என்னால் ஏது ஏன் தனது தன்னால் தானே தான் நாங்கள் நாம் நான் நீ நீங்கள்".split(" ")),e.ta.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var t=e.wordcut;t.init(),e.ta.tokenizer=function(r){if(!arguments.length||null==r||void 0==r)return[];if(Array.isArray(r))return r.map(function(t){return isLunr2?new e.Token(t.toLowerCase()):t.toLowerCase()});var i=r.toString().toLowerCase().replace(/^\s+/,"");return t.cut(i).split("|")},e.Pipeline.registerFunction(e.ta.stemmer,"stemmer-ta"),e.Pipeline.registerFunction(e.ta.stopWordFilter,"stopWordFilter-ta")}});
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.te=function(){this.pipeline.reset(),this.pipeline.add(e.te.trimmer,e.te.stopWordFilter,e.te.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.te.stemmer))},e.te.wordCharacters="ఀ-ఄఅ-ఔక-హా-ౌౕ-ౖౘ-ౚౠ-ౡౢ-ౣ౦-౯౸-౿఼ఽ్ౝ౷",e.te.trimmer=e.trimmerSupport.generateTrimmer(e.te.wordCharacters),e.Pipeline.registerFunction(e.te.trimmer,"trimmer-te"),e.te.stopWordFilter=e.generateStopWordFilter("అందరూ అందుబాటులో అడగండి అడగడం అడ్డంగా అనుగుణంగా అనుమతించు అనుమతిస్తుంది అయితే ఇప్పటికే ఉన్నారు ఎక్కడైనా ఎప్పుడు ఎవరైనా ఎవరో ఏ ఏదైనా ఏమైనప్పటికి ఒక ఒకరు కనిపిస్తాయి కాదు కూడా గా గురించి చుట్టూ చేయగలిగింది తగిన తర్వాత దాదాపు దూరంగా నిజంగా పై ప్రకారం ప్రక్కన మధ్య మరియు మరొక మళ్ళీ మాత్రమే మెచ్చుకో వద్ద వెంట వేరుగా వ్యతిరేకంగా సంబంధం".split(" ")),e.te.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var t=e.wordcut;t.init(),e.te.tokenizer=function(r){if(!arguments.length||null==r||void 0==r)return[];if(Array.isArray(r))return r.map(function(t){return isLunr2?new e.Token(t.toLowerCase()):t.toLowerCase()});var i=r.toString().toLowerCase().replace(/^\s+/,"");return t.cut(i).split("|")},e.Pipeline.registerFunction(e.te.stemmer,"stemmer-te"),e.Pipeline.registerFunction(e.te.stopWordFilter,"stopWordFilter-te")}});
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.th=function(){this.pipeline.reset(),this.pipeline.add(e.th.trimmer),r?this.tokenizer=e.th.tokenizer:(e.tokenizer&&(e.tokenizer=e.th.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.th.tokenizer))},e.th.wordCharacters="[-]",e.th.trimmer=e.trimmerSupport.generateTrimmer(e.th.wordCharacters),e.Pipeline.registerFunction(e.th.trimmer,"trimmer-th");var t=e.wordcut;t.init(),e.th.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t):t});var n=i.toString().replace(/^\s+/,"");return t.cut(n).split("|")}}});
|
||||
+18
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.vi=function(){this.pipeline.reset(),this.pipeline.add(e.vi.stopWordFilter,e.vi.trimmer)},e.vi.wordCharacters="[A-Za-ẓ̀͐́͑̉̃̓ÂâÊêÔôĂ-ăĐ-đƠ-ơƯ-ư]",e.vi.trimmer=e.trimmerSupport.generateTrimmer(e.vi.wordCharacters),e.Pipeline.registerFunction(e.vi.trimmer,"trimmer-vi"),e.vi.stopWordFilter=e.generateStopWordFilter("là cái nhưng mà".split(" "))}});
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r(require("@node-rs/jieba")):r()(e.lunr)}(this,function(e){return function(r,t){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var i="2"==r.version[0];r.zh=function(){this.pipeline.reset(),this.pipeline.add(r.zh.trimmer,r.zh.stopWordFilter,r.zh.stemmer),i?this.tokenizer=r.zh.tokenizer:(r.tokenizer&&(r.tokenizer=r.zh.tokenizer),this.tokenizerFn&&(this.tokenizerFn=r.zh.tokenizer))},r.zh.tokenizer=function(n){if(!arguments.length||null==n||void 0==n)return[];if(Array.isArray(n))return n.map(function(e){return i?new r.Token(e.toLowerCase()):e.toLowerCase()});t&&e.load(t);var o=n.toString().trim().toLowerCase(),s=[];e.cut(o,!0).forEach(function(e){s=s.concat(e.split(" "))}),s=s.filter(function(e){return!!e});var u=0;return s.map(function(e,t){if(i){var n=o.indexOf(e,u),s={};return s.position=[n,e.length],s.index=t,u=n,new r.Token(e,s)}return e})},r.zh.wordCharacters="\\w一-龥",r.zh.trimmer=r.trimmerSupport.generateTrimmer(r.zh.wordCharacters),r.Pipeline.registerFunction(r.zh.trimmer,"trimmer-zh"),r.zh.stemmer=function(){return function(e){return e}}(),r.Pipeline.registerFunction(r.zh.stemmer,"stemmer-zh"),r.zh.stopWordFilter=r.generateStopWordFilter("的 一 不 在 人 有 是 为 為 以 于 於 上 他 而 后 後 之 来 來 及 了 因 下 可 到 由 这 這 与 與 也 此 但 并 並 个 個 其 已 无 無 小 我 们 們 起 最 再 今 去 好 只 又 或 很 亦 某 把 那 你 乃 它 吧 被 比 别 趁 当 當 从 從 得 打 凡 儿 兒 尔 爾 该 該 各 给 給 跟 和 何 还 還 即 几 幾 既 看 据 據 距 靠 啦 另 么 麽 每 嘛 拿 哪 您 凭 憑 且 却 卻 让 讓 仍 啥 如 若 使 谁 誰 虽 雖 随 隨 同 所 她 哇 嗡 往 些 向 沿 哟 喲 用 咱 则 則 怎 曾 至 致 着 著 诸 諸 自".split(" ")),r.Pipeline.registerFunction(r.zh.stopWordFilter,"stopWordFilter-zh")}});
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* export the module via AMD, CommonJS or as a browser global
|
||||
* Export code from https://github.com/umdjs/umd/blob/master/returnExports.js
|
||||
*/
|
||||
;(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(factory)
|
||||
} else if (typeof exports === 'object') {
|
||||
/**
|
||||
* Node. Does not work with strict CommonJS, but
|
||||
* only CommonJS-like environments that support module.exports,
|
||||
* like Node.
|
||||
*/
|
||||
module.exports = factory()
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
factory()(root.lunr);
|
||||
}
|
||||
}(this, function () {
|
||||
/**
|
||||
* Just return a value to define the module export.
|
||||
* This example returns an object, but the module
|
||||
* can return a function as the exported value.
|
||||
*/
|
||||
|
||||
return function(lunr) {
|
||||
// TinySegmenter 0.1 -- Super compact Japanese tokenizer in Javascript
|
||||
// (c) 2008 Taku Kudo <taku@chasen.org>
|
||||
// TinySegmenter is freely distributable under the terms of a new BSD licence.
|
||||
// For details, see http://chasen.org/~taku/software/TinySegmenter/LICENCE.txt
|
||||
|
||||
function TinySegmenter() {
|
||||
var patterns = {
|
||||
"[一二三四五六七八九十百千万億兆]":"M",
|
||||
"[一-龠々〆ヵヶ]":"H",
|
||||
"[ぁ-ん]":"I",
|
||||
"[ァ-ヴーア-ン゙ー]":"K",
|
||||
"[a-zA-Za-zA-Z]":"A",
|
||||
"[0-90-9]":"N"
|
||||
}
|
||||
this.chartype_ = [];
|
||||
for (var i in patterns) {
|
||||
var regexp = new RegExp(i);
|
||||
this.chartype_.push([regexp, patterns[i]]);
|
||||
}
|
||||
|
||||
this.BIAS__ = -332
|
||||
this.BC1__ = {"HH":6,"II":2461,"KH":406,"OH":-1378};
|
||||
this.BC2__ = {"AA":-3267,"AI":2744,"AN":-878,"HH":-4070,"HM":-1711,"HN":4012,"HO":3761,"IA":1327,"IH":-1184,"II":-1332,"IK":1721,"IO":5492,"KI":3831,"KK":-8741,"MH":-3132,"MK":3334,"OO":-2920};
|
||||
this.BC3__ = {"HH":996,"HI":626,"HK":-721,"HN":-1307,"HO":-836,"IH":-301,"KK":2762,"MK":1079,"MM":4034,"OA":-1652,"OH":266};
|
||||
this.BP1__ = {"BB":295,"OB":304,"OO":-125,"UB":352};
|
||||
this.BP2__ = {"BO":60,"OO":-1762};
|
||||
this.BQ1__ = {"BHH":1150,"BHM":1521,"BII":-1158,"BIM":886,"BMH":1208,"BNH":449,"BOH":-91,"BOO":-2597,"OHI":451,"OIH":-296,"OKA":1851,"OKH":-1020,"OKK":904,"OOO":2965};
|
||||
this.BQ2__ = {"BHH":118,"BHI":-1159,"BHM":466,"BIH":-919,"BKK":-1720,"BKO":864,"OHH":-1139,"OHM":-181,"OIH":153,"UHI":-1146};
|
||||
this.BQ3__ = {"BHH":-792,"BHI":2664,"BII":-299,"BKI":419,"BMH":937,"BMM":8335,"BNN":998,"BOH":775,"OHH":2174,"OHM":439,"OII":280,"OKH":1798,"OKI":-793,"OKO":-2242,"OMH":-2402,"OOO":11699};
|
||||
this.BQ4__ = {"BHH":-3895,"BIH":3761,"BII":-4654,"BIK":1348,"BKK":-1806,"BMI":-3385,"BOO":-12396,"OAH":926,"OHH":266,"OHK":-2036,"ONN":-973};
|
||||
this.BW1__ = {",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682};
|
||||
this.BW2__ = {"..":-11822,"11":-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669};
|
||||
this.BW3__ = {"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1000,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990};
|
||||
this.TC1__ = {"AAA":1093,"HHH":1029,"HHM":580,"HII":998,"HOH":-390,"HOM":-331,"IHI":1169,"IOH":-142,"IOI":-1015,"IOM":467,"MMH":187,"OOI":-1832};
|
||||
this.TC2__ = {"HHO":2088,"HII":-1023,"HMM":-1154,"IHI":-1965,"KKH":703,"OII":-2649};
|
||||
this.TC3__ = {"AAA":-294,"HHH":346,"HHI":-341,"HII":-1088,"HIK":731,"HOH":-1486,"IHH":128,"IHI":-3041,"IHO":-1935,"IIH":-825,"IIM":-1035,"IOI":-542,"KHH":-1216,"KKA":491,"KKH":-1217,"KOK":-1009,"MHH":-2694,"MHM":-457,"MHO":123,"MMH":-471,"NNH":-1689,"NNO":662,"OHO":-3393};
|
||||
this.TC4__ = {"HHH":-203,"HHI":1344,"HHK":365,"HHM":-122,"HHN":182,"HHO":669,"HIH":804,"HII":679,"HOH":446,"IHH":695,"IHO":-2324,"IIH":321,"III":1497,"IIO":656,"IOO":54,"KAK":4845,"KKA":3386,"KKK":3065,"MHH":-405,"MHI":201,"MMH":-241,"MMM":661,"MOM":841};
|
||||
this.TQ1__ = {"BHHH":-227,"BHHI":316,"BHIH":-132,"BIHH":60,"BIII":1595,"BNHH":-744,"BOHH":225,"BOOO":-908,"OAKK":482,"OHHH":281,"OHIH":249,"OIHI":200,"OIIH":-68};
|
||||
this.TQ2__ = {"BIHH":-1401,"BIII":-1033,"BKAK":-543,"BOOO":-5591};
|
||||
this.TQ3__ = {"BHHH":478,"BHHM":-1073,"BHIH":222,"BHII":-504,"BIIH":-116,"BIII":-105,"BMHI":-863,"BMHM":-464,"BOMH":620,"OHHH":346,"OHHI":1729,"OHII":997,"OHMH":481,"OIHH":623,"OIIH":1344,"OKAK":2792,"OKHH":587,"OKKA":679,"OOHH":110,"OOII":-685};
|
||||
this.TQ4__ = {"BHHH":-721,"BHHM":-3604,"BHII":-966,"BIIH":-607,"BIII":-2181,"OAAA":-2763,"OAKK":180,"OHHH":-294,"OHHI":2446,"OHHO":480,"OHIH":-1573,"OIHH":1935,"OIHI":-493,"OIIH":626,"OIII":-4007,"OKAK":-8156};
|
||||
this.TW1__ = {"につい":-4681,"東京都":2026};
|
||||
this.TW2__ = {"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216};
|
||||
this.TW3__ = {"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287};
|
||||
this.TW4__ = {"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865};
|
||||
this.UC1__ = {"A":484,"K":93,"M":645,"O":-505};
|
||||
this.UC2__ = {"A":819,"H":1059,"I":409,"M":3987,"N":5775,"O":646};
|
||||
this.UC3__ = {"A":-1370,"I":2311};
|
||||
this.UC4__ = {"A":-2643,"H":1809,"I":-1032,"K":-3450,"M":3565,"N":3876,"O":6646};
|
||||
this.UC5__ = {"H":313,"I":-1238,"K":-799,"M":539,"O":-831};
|
||||
this.UC6__ = {"H":-506,"I":-253,"K":87,"M":247,"O":-387};
|
||||
this.UP1__ = {"O":-214};
|
||||
this.UP2__ = {"B":69,"O":935};
|
||||
this.UP3__ = {"B":189};
|
||||
this.UQ1__ = {"BH":21,"BI":-12,"BK":-99,"BN":142,"BO":-56,"OH":-95,"OI":477,"OK":410,"OO":-2422};
|
||||
this.UQ2__ = {"BH":216,"BI":113,"OK":1759};
|
||||
this.UQ3__ = {"BA":-479,"BH":42,"BI":1913,"BK":-7198,"BM":3160,"BN":6427,"BO":14761,"OI":-827,"ON":-3212};
|
||||
this.UW1__ = {",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135};
|
||||
this.UW2__ = {",":-829,"、":-829,"〇":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568};
|
||||
this.UW3__ = {",":4889,"1":-800,"−":-1723,"、":4889,"々":-2311,"〇":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"1":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278};
|
||||
this.UW4__ = {",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"〇":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1000,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637};
|
||||
this.UW5__ = {",":465,".":-299,"1":-514,"E2":-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"1":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343};
|
||||
this.UW6__ = {",":227,".":808,"1":-270,"E1":306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"1":-270,"E1":306,"ル":-673,"ン":-496};
|
||||
|
||||
return this;
|
||||
}
|
||||
TinySegmenter.prototype.ctype_ = function(str) {
|
||||
for (var i in this.chartype_) {
|
||||
if (str.match(this.chartype_[i][0])) {
|
||||
return this.chartype_[i][1];
|
||||
}
|
||||
}
|
||||
return "O";
|
||||
}
|
||||
|
||||
TinySegmenter.prototype.ts_ = function(v) {
|
||||
if (v) { return v; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
TinySegmenter.prototype.segment = function(input) {
|
||||
if (input == null || input == undefined || input == "") {
|
||||
return [];
|
||||
}
|
||||
var result = [];
|
||||
var seg = ["B3","B2","B1"];
|
||||
var ctype = ["O","O","O"];
|
||||
var o = input.split("");
|
||||
for (i = 0; i < o.length; ++i) {
|
||||
seg.push(o[i]);
|
||||
ctype.push(this.ctype_(o[i]))
|
||||
}
|
||||
seg.push("E1");
|
||||
seg.push("E2");
|
||||
seg.push("E3");
|
||||
ctype.push("O");
|
||||
ctype.push("O");
|
||||
ctype.push("O");
|
||||
var word = seg[3];
|
||||
var p1 = "U";
|
||||
var p2 = "U";
|
||||
var p3 = "U";
|
||||
for (var i = 4; i < seg.length - 3; ++i) {
|
||||
var score = this.BIAS__;
|
||||
var w1 = seg[i-3];
|
||||
var w2 = seg[i-2];
|
||||
var w3 = seg[i-1];
|
||||
var w4 = seg[i];
|
||||
var w5 = seg[i+1];
|
||||
var w6 = seg[i+2];
|
||||
var c1 = ctype[i-3];
|
||||
var c2 = ctype[i-2];
|
||||
var c3 = ctype[i-1];
|
||||
var c4 = ctype[i];
|
||||
var c5 = ctype[i+1];
|
||||
var c6 = ctype[i+2];
|
||||
score += this.ts_(this.UP1__[p1]);
|
||||
score += this.ts_(this.UP2__[p2]);
|
||||
score += this.ts_(this.UP3__[p3]);
|
||||
score += this.ts_(this.BP1__[p1 + p2]);
|
||||
score += this.ts_(this.BP2__[p2 + p3]);
|
||||
score += this.ts_(this.UW1__[w1]);
|
||||
score += this.ts_(this.UW2__[w2]);
|
||||
score += this.ts_(this.UW3__[w3]);
|
||||
score += this.ts_(this.UW4__[w4]);
|
||||
score += this.ts_(this.UW5__[w5]);
|
||||
score += this.ts_(this.UW6__[w6]);
|
||||
score += this.ts_(this.BW1__[w2 + w3]);
|
||||
score += this.ts_(this.BW2__[w3 + w4]);
|
||||
score += this.ts_(this.BW3__[w4 + w5]);
|
||||
score += this.ts_(this.TW1__[w1 + w2 + w3]);
|
||||
score += this.ts_(this.TW2__[w2 + w3 + w4]);
|
||||
score += this.ts_(this.TW3__[w3 + w4 + w5]);
|
||||
score += this.ts_(this.TW4__[w4 + w5 + w6]);
|
||||
score += this.ts_(this.UC1__[c1]);
|
||||
score += this.ts_(this.UC2__[c2]);
|
||||
score += this.ts_(this.UC3__[c3]);
|
||||
score += this.ts_(this.UC4__[c4]);
|
||||
score += this.ts_(this.UC5__[c5]);
|
||||
score += this.ts_(this.UC6__[c6]);
|
||||
score += this.ts_(this.BC1__[c2 + c3]);
|
||||
score += this.ts_(this.BC2__[c3 + c4]);
|
||||
score += this.ts_(this.BC3__[c4 + c5]);
|
||||
score += this.ts_(this.TC1__[c1 + c2 + c3]);
|
||||
score += this.ts_(this.TC2__[c2 + c3 + c4]);
|
||||
score += this.ts_(this.TC3__[c3 + c4 + c5]);
|
||||
score += this.ts_(this.TC4__[c4 + c5 + c6]);
|
||||
// score += this.ts_(this.TC5__[c4 + c5 + c6]);
|
||||
score += this.ts_(this.UQ1__[p1 + c1]);
|
||||
score += this.ts_(this.UQ2__[p2 + c2]);
|
||||
score += this.ts_(this.UQ3__[p3 + c3]);
|
||||
score += this.ts_(this.BQ1__[p2 + c2 + c3]);
|
||||
score += this.ts_(this.BQ2__[p2 + c3 + c4]);
|
||||
score += this.ts_(this.BQ3__[p3 + c2 + c3]);
|
||||
score += this.ts_(this.BQ4__[p3 + c3 + c4]);
|
||||
score += this.ts_(this.TQ1__[p2 + c1 + c2 + c3]);
|
||||
score += this.ts_(this.TQ2__[p2 + c2 + c3 + c4]);
|
||||
score += this.ts_(this.TQ3__[p3 + c1 + c2 + c3]);
|
||||
score += this.ts_(this.TQ4__[p3 + c2 + c3 + c4]);
|
||||
var p = "O";
|
||||
if (score > 0) {
|
||||
result.push(word);
|
||||
word = "";
|
||||
p = "B";
|
||||
}
|
||||
p1 = p2;
|
||||
p2 = p3;
|
||||
p3 = p;
|
||||
word += seg[i];
|
||||
}
|
||||
result.push(word);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
lunr.TinySegmenter = TinySegmenter;
|
||||
};
|
||||
|
||||
}));
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["src/templates/assets/stylesheets/palette/_scheme.scss","../../../../src/templates/assets/stylesheets/palette.scss","src/templates/assets/stylesheets/palette/_accent.scss","src/templates/assets/stylesheets/palette/_primary.scss","src/templates/assets/stylesheets/utilities/_break.scss"],"names":[],"mappings":"AA2BA,cAGE,6BAME,sDAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CACA,mDAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CAGA,mDAAA,CACA,gDAAA,CACA,yDAAA,CACA,4DAAA,CAGA,0BAAA,CACA,mCAAA,CAGA,iCAAA,CACA,kCAAA,CACA,mCAAA,CACA,mCAAA,CACA,kCAAA,CACA,iCAAA,CACA,+CAAA,CACA,6DAAA,CACA,gEAAA,CACA,4DAAA,CACA,4DAAA,CACA,6DAAA,CAGA,6CAAA,CAGA,+CAAA,CAGA,uDAAA,CACA,6DAAA,CACA,2DAAA,CAGA,iCAAA,CAGA,yDAAA,CACA,iEAAA,CAGA,mDAAA,CACA,mDAAA,CAGA,qDAAA,CACA,uDAAA,CAGA,8DAAA,CAKA,8DAAA,CAKA,0DAAA,CAzEA,iBCiBF,CD6DE,kHAEE,YC3DJ,CDkFE,yDACE,4BChFJ,CD+EE,2DACE,4BC7EJ,CD4EE,gEACE,4BC1EJ,CDyEE,2DACE,4BCvEJ,CDsEE,yDACE,4BCpEJ,CDmEE,0DACE,4BCjEJ,CDgEE,gEACE,4BC9DJ,CD6DE,0DACE,4BC3DJ,CD0DE,2OACE,4BC/CJ,CDsDA,+FAGE,iCCpDF,CACF,CCjDE,2BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD6CN,CCvDE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDoDN,CC9DE,8BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD2DN,CCrEE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDkEN,CC5EE,8BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDyEN,CCnFE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDgFN,CC1FE,kCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDuFN,CCjGE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD8FN,CCxGE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDqGN,CC/GE,6BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD4GN,CCtHE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDmHN,CC7HE,4BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCD6HN,CCpIE,8BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCDoIN,CC3IE,6BACE,yBAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCD2IN,CClJE,8BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCDkJN,CCzJE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDsJN,CE3JE,4BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwJN,CEnKE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgKN,CE3KE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwKN,CEnLE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgLN,CE3LE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwLN,CEnME,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgMN,CE3ME,mCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwMN,CEnNE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgNN,CE3NE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwNN,CEnOE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgON,CE3OE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwON,CEnPE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCFmPN,CE3PE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCF2PN,CEnQE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCFmQN,CE3QE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCF2QN,CEnRE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgRN,CE3RE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwRN,CEnSE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCAAA,CAKA,4BF4RN,CE5SE,kCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCAAA,CAKA,4BFqSN,CEtRE,sEACE,4BFyRJ,CE1RE,+DACE,4BF6RJ,CE9RE,iEACE,4BFiSJ,CElSE,gEACE,4BFqSJ,CEtSE,iEACE,4BFySJ,CEhSA,8BACE,mDAAA,CACA,4DAAA,CACA,0DAAA,CACA,oDAAA,CACA,2DAAA,CAGA,4BFiSF,CE9RE,yCACE,+BFgSJ,CE7RI,kDAEE,0CAAA,CACA,sCAAA,CAFA,mCFiSN,CG7MI,mCD1EA,+CACE,8CF0RJ,CEvRI,qDACE,8CFyRN,CEpRE,iEACE,mCFsRJ,CACF,CGxNI,sCDvDA,uCACE,oCFkRJ,CACF,CEzQA,8BACE,kDAAA,CACA,4DAAA,CACA,wDAAA,CACA,oDAAA,CACA,6DAAA,CAGA,4BF0QF,CEvQE,yCACE,+BFyQJ,CEtQI,kDAEE,0CAAA,CACA,sCAAA,CAFA,mCF0QN,CEnQE,yCACE,6CFqQJ,CG9NI,0CDhCA,8CACE,gDFiQJ,CACF,CGnOI,0CDvBA,iFACE,6CF6PJ,CACF,CG3PI,sCDKA,uCACE,6CFyPJ,CACF","file":"palette.css"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user