0.12.0-5 Pre-Plugin
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-backend",
|
"name": "ripster-backend",
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1048,20 +1048,20 @@ async function migrateSettingsSchemaMetadata(db) {
|
|||||||
{
|
{
|
||||||
key: 'use_plugin_architecture_bluray_review',
|
key: 'use_plugin_architecture_bluray_review',
|
||||||
label: 'Review / Mediainfo-Prüfung (Blu-ray)',
|
label: 'Review / Mediainfo-Prüfung (Blu-ray)',
|
||||||
description: 'Noch nicht implementiert — kommt in einem späteren Sprint.',
|
description: 'HandBrake-Scan läuft über das Blu-ray-Plugin.',
|
||||||
defaultValue: 'false',
|
defaultValue: 'true',
|
||||||
orderIndex: 10003,
|
orderIndex: 10003,
|
||||||
dependsOn: 'use_plugin_architecture_bluray',
|
dependsOn: 'use_plugin_architecture_bluray',
|
||||||
validationJson: '{"readonly":true}'
|
validationJson: '{}'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'use_plugin_architecture_bluray_encode',
|
key: 'use_plugin_architecture_bluray_encode',
|
||||||
label: 'Encoding (Blu-ray)',
|
label: 'Encoding (Blu-ray)',
|
||||||
description: 'Noch nicht implementiert — kommt in einem späteren Sprint.',
|
description: 'Encoding läuft über das Blu-ray-Plugin.',
|
||||||
defaultValue: 'false',
|
defaultValue: 'true',
|
||||||
orderIndex: 10004,
|
orderIndex: 10004,
|
||||||
dependsOn: 'use_plugin_architecture_bluray',
|
dependsOn: 'use_plugin_architecture_bluray',
|
||||||
validationJson: '{"readonly":true}'
|
validationJson: '{}'
|
||||||
},
|
},
|
||||||
// ── DVD ──────────────────────────────────────────────────────────────────
|
// ── DVD ──────────────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
@@ -1159,11 +1159,11 @@ async function migrateSettingsSchemaMetadata(db) {
|
|||||||
{
|
{
|
||||||
key: 'use_plugin_architecture_audiobook_encode',
|
key: 'use_plugin_architecture_audiobook_encode',
|
||||||
label: 'Encoding (Audiobook)',
|
label: 'Encoding (Audiobook)',
|
||||||
description: 'Noch nicht implementiert — Encoding läuft noch über Legacy.',
|
description: 'Encoding läuft über das Audiobook-Plugin.',
|
||||||
defaultValue: 'false',
|
defaultValue: 'true',
|
||||||
orderIndex: 10032,
|
orderIndex: 10032,
|
||||||
dependsOn: 'use_plugin_architecture_audiobook',
|
dependsOn: 'use_plugin_architecture_audiobook',
|
||||||
validationJson: '{"readonly":true}'
|
validationJson: '{}'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
for (const setting of pluginArchitectureSettings) {
|
for (const setting of pluginArchitectureSettings) {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ const { ensureDir } = require('../utils/files');
|
|||||||
*
|
*
|
||||||
* Deshalb:
|
* Deshalb:
|
||||||
* rip() → No-Op (Datei liegt bereits im rawDir)
|
* rip() → No-Op (Datei liegt bereits im rawDir)
|
||||||
* encode() → ffmpeg-Encoding (Single-File oder per Chapter)
|
|
||||||
* review() → null (kein HandBrake-Preview)
|
* review() → null (kein HandBrake-Preview)
|
||||||
|
* encode() → ffmpeg Single-File oder kapitelweise
|
||||||
*
|
*
|
||||||
* detect() prüft das mediaProfile-Feld (gesetzt vom Orchestrator für
|
* detect() prüft das mediaProfile-Feld (gesetzt vom Orchestrator für
|
||||||
* Datei-Uploads) oder eine Dateiendung im discInfo.
|
* Datei-Uploads) oder eine Dateiendung im discInfo.
|
||||||
@@ -132,8 +132,7 @@ class AudiobookPlugin extends SourcePlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodiert die .aax-Datei mit ffmpeg.
|
* Encodiert die .aax-Datei mit ffmpeg — als Einzel-Datei oder kapitelweise.
|
||||||
* Unterstützt Single-File (M4B) und kapitelweise Ausgabe (MP3, FLAC).
|
|
||||||
*
|
*
|
||||||
* @param {object} job
|
* @param {object} job
|
||||||
* @param {PluginContext} ctx
|
* @param {PluginContext} ctx
|
||||||
@@ -146,6 +145,12 @@ class AudiobookPlugin extends SourcePlugin {
|
|||||||
pluginFile: 'AudiobookPlugin.js'
|
pluginFile: 'AudiobookPlugin.js'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const settings = await ctx.settings.getSettingsMap();
|
||||||
|
if (!settings.audiobook_encoding_enabled) {
|
||||||
|
ctx.logger.info('audiobook:encode:disabled', { jobId: job?.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
inputPath,
|
inputPath,
|
||||||
outputPath,
|
outputPath,
|
||||||
@@ -171,7 +176,6 @@ class AudiobookPlugin extends SourcePlugin {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = await ctx.settings.getSettingsMap();
|
|
||||||
const ffmpegCommand = String(settings.ffmpeg_command || 'ffmpeg').trim() || 'ffmpeg';
|
const ffmpegCommand = String(settings.ffmpeg_command || 'ffmpeg').trim() || 'ffmpeg';
|
||||||
const normalizedFormat = audiobookService.normalizeOutputFormat(outputFormat);
|
const normalizedFormat = audiobookService.normalizeOutputFormat(outputFormat);
|
||||||
const normalizedOptions = audiobookService.normalizeFormatOptions(normalizedFormat, formatOptions);
|
const normalizedOptions = audiobookService.normalizeFormatOptions(normalizedFormat, formatOptions);
|
||||||
@@ -203,7 +207,6 @@ class AudiobookPlugin extends SourcePlugin {
|
|||||||
outputPath
|
outputPath
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ergebnis für den Orchestrator ablegen
|
|
||||||
ctx.extra.encodeResult = {
|
ctx.extra.encodeResult = {
|
||||||
outputPath,
|
outputPath,
|
||||||
format: normalizedFormat,
|
format: normalizedFormat,
|
||||||
@@ -233,6 +236,18 @@ class AudiobookPlugin extends SourcePlugin {
|
|||||||
*/
|
*/
|
||||||
getSettingsSchema() {
|
getSettingsSchema() {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
key: 'audiobook_encoding_enabled',
|
||||||
|
category: 'Features',
|
||||||
|
label: 'Audiobook Encoding',
|
||||||
|
type: 'boolean',
|
||||||
|
required: 1,
|
||||||
|
description: 'Audiobook-Encoding über das Plugin aktivieren.',
|
||||||
|
default_value: 'true',
|
||||||
|
options_json: '[]',
|
||||||
|
validation_json: '{}',
|
||||||
|
order_index: 100
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'ffmpeg_command',
|
key: 'ffmpeg_command',
|
||||||
category: 'Tools',
|
category: 'Tools',
|
||||||
@@ -283,10 +298,7 @@ class AudiobookPlugin extends SourcePlugin {
|
|||||||
outputPath,
|
outputPath,
|
||||||
normalizedFormat,
|
normalizedFormat,
|
||||||
normalizedOptions,
|
normalizedOptions,
|
||||||
{
|
{ activationBytes, metadata }
|
||||||
activationBytes,
|
|
||||||
metadata
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.logger.info('audiobook:encode:single-file', {
|
ctx.logger.info('audiobook:encode:single-file', {
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ class VideoDiscPlugin extends SourcePlugin {
|
|||||||
const reviewSilent = Boolean(ctx.extra?.reviewSilent);
|
const reviewSilent = Boolean(ctx.extra?.reviewSilent);
|
||||||
let runInfo;
|
let runInfo;
|
||||||
if (runCommandFn) {
|
if (runCommandFn) {
|
||||||
|
try {
|
||||||
runInfo = await runCommandFn({
|
runInfo = await runCommandFn({
|
||||||
jobId: job?.id,
|
jobId: job?.id,
|
||||||
stage: reviewStage,
|
stage: reviewStage,
|
||||||
@@ -163,6 +164,16 @@ class VideoDiscPlugin extends SourcePlugin {
|
|||||||
collectStderrLines: false,
|
collectStderrLines: false,
|
||||||
silent: reviewSilent
|
silent: reviewSilent
|
||||||
});
|
});
|
||||||
|
} catch (cmdError) {
|
||||||
|
// runCommand collected stdout lines via callbacks before throwing.
|
||||||
|
// Recover the runInfo from the error so the caller can still parse
|
||||||
|
// whatever output HandBrakeCLI produced (e.g. exit code 2 with valid JSON).
|
||||||
|
runInfo = cmdError?.runInfo || {
|
||||||
|
status: 'ERROR',
|
||||||
|
exitCode: typeof cmdError?.code === 'number' ? cmdError.code : 1,
|
||||||
|
errorMessage: cmdError?.message || String(cmdError)
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
runInfo = await _runCommandCaptured({
|
runInfo = await _runCommandCaptured({
|
||||||
cmd: scanConfig.cmd,
|
cmd: scanConfig.cmd,
|
||||||
|
|||||||
@@ -8933,6 +8933,15 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An Incomplete_ raw folder means the rip never finished — skip it and re-rip.
|
||||||
|
if (existingRawInput && resolveRawFolderStateFromPath(job.raw_path) === RAW_FOLDER_STATES.INCOMPLETE) {
|
||||||
|
logger.info('startPreparedJob:raw-incomplete-skip', {
|
||||||
|
jobId,
|
||||||
|
rawPath: job.raw_path
|
||||||
|
});
|
||||||
|
existingRawInput = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (existingRawInput) {
|
if (existingRawInput) {
|
||||||
await historyService.updateJob(jobId, {
|
await historyService.updateJob(jobId, {
|
||||||
status: 'MEDIAINFO_CHECK',
|
status: 'MEDIAINFO_CHECK',
|
||||||
@@ -9258,6 +9267,17 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mkInfo = this.safeParseJson(sourceJob.makemkv_info_json);
|
const mkInfo = this.safeParseJson(sourceJob.makemkv_info_json);
|
||||||
|
|
||||||
|
// Importierte Jobs (RAW-Ordner-Import) müssen zuerst vollständig analysiert und
|
||||||
|
// encodiert werden, bevor ein Re-Encode möglich ist. Nur "Neu einlesen" ist erlaubt.
|
||||||
|
if (mkInfo?.source === 'orphan_raw_import' && !sourceJob.handbrake_info_json) {
|
||||||
|
const error = new Error(
|
||||||
|
'Re-Encode nicht möglich: Job wurde via RAW-Import erstellt und wurde noch nicht vollständig encodiert. Bitte zuerst "Neu einlesen" ausführen.'
|
||||||
|
);
|
||||||
|
error.statusCode = 409;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const sourceEncodePlan = this.safeParseJson(sourceJob.encode_plan_json);
|
const sourceEncodePlan = this.safeParseJson(sourceJob.encode_plan_json);
|
||||||
const reencodeMediaProfile = this.resolveMediaProfileForJob(sourceJob, {
|
const reencodeMediaProfile = this.resolveMediaProfileForJob(sourceJob, {
|
||||||
makemkvInfo: mkInfo,
|
makemkvInfo: mkInfo,
|
||||||
@@ -11824,7 +11844,8 @@ class PipelineService extends EventEmitter {
|
|||||||
await historyService.updateJob(jobId, {
|
await historyService.updateJob(jobId, {
|
||||||
makemkv_info_json: JSON.stringify(this.withAnalyzeContextMediaProfile({
|
makemkv_info_json: JSON.stringify(this.withAnalyzeContextMediaProfile({
|
||||||
...error.runInfo,
|
...error.runInfo,
|
||||||
analyzeContext: mkInfoBeforeRip?.analyzeContext || null
|
analyzeContext: mkInfoBeforeRip?.analyzeContext || null,
|
||||||
|
pluginExecution: mkInfoBeforeRip?.pluginExecution || null
|
||||||
}, mediaProfile))
|
}, mediaProfile))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -11887,10 +11908,17 @@ class PipelineService extends EventEmitter {
|
|||||||
const isCdRetry = mediaProfile === 'cd';
|
const isCdRetry = mediaProfile === 'cd';
|
||||||
const isAudiobookRetry = mediaProfile === 'audiobook';
|
const isAudiobookRetry = mediaProfile === 'audiobook';
|
||||||
|
|
||||||
// CD-Jobs dürfen auch im FINISHED-Status neu gerippt werden (z.B. importierte Jobs)
|
// CD-Jobs dürfen auch im FINISHED-Status neu gerippt werden (z.B. importierte Jobs).
|
||||||
|
// Importierte Audiobooks (orphan_raw_import ohne bisherigen Encode) dürfen ebenfalls
|
||||||
|
// neu gestartet werden, da erst der vollständige Flow durchlaufen werden muss.
|
||||||
|
const isImportedAudiobookWithoutEncode = isAudiobookRetry
|
||||||
|
&& sourceStatus === 'FINISHED'
|
||||||
|
&& sourceMakemkvInfo?.source === 'orphan_raw_import'
|
||||||
|
&& !sourceJob.handbrake_info_json;
|
||||||
const retryable = ['ERROR', 'CANCELLED'].includes(sourceStatus)
|
const retryable = ['ERROR', 'CANCELLED'].includes(sourceStatus)
|
||||||
|| ['ERROR', 'CANCELLED'].includes(sourceLastState)
|
|| ['ERROR', 'CANCELLED'].includes(sourceLastState)
|
||||||
|| (isCdRetry && ['FINISHED', 'ERROR', 'CANCELLED'].includes(sourceStatus));
|
|| (isCdRetry && ['FINISHED', 'ERROR', 'CANCELLED'].includes(sourceStatus))
|
||||||
|
|| isImportedAudiobookWithoutEncode;
|
||||||
if (!retryable) {
|
if (!retryable) {
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
`Retry nicht möglich: Job ${jobId} ist nicht im Status ERROR/CANCELLED (aktuell ${sourceStatus || sourceLastState || '-'}).`
|
`Retry nicht möglich: Job ${jobId} ist nicht im Status ERROR/CANCELLED (aktuell ${sourceStatus || sourceLastState || '-'}).`
|
||||||
@@ -12293,6 +12321,15 @@ class PipelineService extends EventEmitter {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mkInfoForRestart = this.safeParseJson(job.makemkv_info_json);
|
||||||
|
if (mkInfoForRestart?.source === 'orphan_raw_import' && !job.handbrake_info_json) {
|
||||||
|
const error = new Error(
|
||||||
|
'Encode-Neustart nicht möglich: Job wurde via RAW-Import erstellt und wurde noch nicht vollständig encodiert. Bitte zuerst "Neu einlesen" ausführen.'
|
||||||
|
);
|
||||||
|
error.statusCode = 409;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const encodePlan = this.safeParseJson(job.encode_plan_json);
|
const encodePlan = this.safeParseJson(job.encode_plan_json);
|
||||||
if (!encodePlan || !Array.isArray(encodePlan.titles) || encodePlan.titles.length === 0) {
|
if (!encodePlan || !Array.isArray(encodePlan.titles) || encodePlan.titles.length === 0) {
|
||||||
const error = new Error('Encode-Neustart nicht möglich: encode_plan fehlt.');
|
const error = new Error('Encode-Neustart nicht möglich: encode_plan fehlt.');
|
||||||
@@ -14045,9 +14082,8 @@ class PipelineService extends EventEmitter {
|
|||||||
message: `${metadata.title || job.title || job.detected_title || `Job #${jobId}`} -> ${preferredFinalOutputPath}`
|
message: `${metadata.title || job.title || job.detected_title || `Job #${jobId}`} -> ${preferredFinalOutputPath}`
|
||||||
});
|
});
|
||||||
|
|
||||||
let temporaryChapterMetadataPath = null;
|
// Activation Bytes für AAX-Dateien aus Cache lesen (vor dem Background-Start,
|
||||||
|
// damit ein Fehler hier den HTTP-Request direkt abbricht).
|
||||||
// Activation Bytes für AAX-Dateien aus Cache lesen
|
|
||||||
let encodeActivationBytes = null;
|
let encodeActivationBytes = null;
|
||||||
if (path.extname(inputPath).toLowerCase() === '.aax') {
|
if (path.extname(inputPath).toLowerCase() === '.aax') {
|
||||||
try {
|
try {
|
||||||
@@ -14063,6 +14099,10 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Den eigentlichen ffmpeg-Encode im Hintergrund ausführen, damit der HTTP-Request
|
||||||
|
// sofort nach dem Setzen des ENCODING-Status zurückkehrt (kein Blockieren bis FINISHED).
|
||||||
|
void (async () => {
|
||||||
|
let temporaryChapterMetadataPath = null;
|
||||||
try {
|
try {
|
||||||
let ffmpegRunInfo = null;
|
let ffmpegRunInfo = null;
|
||||||
if (isSplitOutput) {
|
if (isSplitOutput) {
|
||||||
@@ -14346,8 +14386,6 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
logger.error('audiobook:encode:failed', { jobId, error: errorToMeta(error) });
|
logger.error('audiobook:encode:failed', { jobId, error: errorToMeta(error) });
|
||||||
await this.failJob(jobId, 'ENCODING', error);
|
await this.failJob(jobId, 'ENCODING', error);
|
||||||
error.jobAlreadyFailed = true;
|
|
||||||
throw error;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (temporaryChapterMetadataPath) {
|
if (temporaryChapterMetadataPath) {
|
||||||
try {
|
try {
|
||||||
@@ -14357,6 +14395,9 @@ class PipelineService extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return { started: true, stage: 'ENCODING', outputPath: incompleteOutputPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CD Pipeline ─────────────────────────────────────────────────────────────
|
// ── CD Pipeline ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.2",
|
"primereact": "^10.9.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster-frontend",
|
"name": "ripster-frontend",
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ripster",
|
"name": "ripster",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.12.0-4",
|
"version": "0.12.0-5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
||||||
"dev:backend": "npm run dev --prefix backend",
|
"dev:backend": "npm run dev --prefix backend",
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
"build:frontend": "npm run build --prefix frontend",
|
"build:frontend": "npm run build --prefix frontend",
|
||||||
"qa:cd:analysis": "node ./scripts/smoketest/qa-cd-analysis.js",
|
"qa:cd:analysis": "node ./scripts/smoketest/qa-cd-analysis.js",
|
||||||
"qa:dvd:analysis": "node ./scripts/smoketest/qa-dvd-analysis.js",
|
"qa:dvd:analysis": "node ./scripts/smoketest/qa-dvd-analysis.js",
|
||||||
|
"qa:bluray:analysis": "node ./scripts/smoketest/qa-bluray-analysis.js",
|
||||||
"qa:audiobook:analysis": "node ./scripts/smoketest/qa-audiobook-analysis.js",
|
"qa:audiobook:analysis": "node ./scripts/smoketest/qa-audiobook-analysis.js",
|
||||||
"qa:cd:check": "bash ./scripts/smoketest/qa-cd-check.sh",
|
"qa:cd:check": "bash ./scripts/smoketest/qa-cd-check.sh",
|
||||||
"qa:audiobook:check": "bash ./scripts/smoketest/qa-audiobook-check.sh",
|
"qa:audiobook:check": "bash ./scripts/smoketest/qa-audiobook-check.sh",
|
||||||
|
|||||||
Reference in New Issue
Block a user