0.12.0-16 misc fixes

This commit is contained in:
2026-03-30 14:37:23 +00:00
parent 76b020d5c0
commit 5284dd2a43
30 changed files with 6955 additions and 896 deletions
+129 -7
View File
@@ -5,7 +5,7 @@ const path = require('path');
const { SourcePlugin } = require('./PluginBase');
const { spawnTrackedProcess } = require('../services/processRunner');
const { parseHandBrakeProgress, parseMakeMkvProgress } = require('../utils/progressParsers');
const { ensureDir, sanitizeFileName } = require('../utils/files');
const { ensureDir, sanitizeFileName, renderTemplate } = require('../utils/files');
const VIDEO_EXTENSIONS = new Set(['mkv', 'mp4', 'm2ts', 'avi', 'mov', 'm4v', 'wmv', 'ts']);
const AUDIO_EXTENSIONS = new Set(['flac', 'mp3', 'aac', 'wav', 'm4a', 'ogg', 'opus', 'wma', 'ape']);
@@ -74,6 +74,20 @@ function buildAudioFfmpegArgs(ffmpegCmd, inputFile, outputFile, format, opts = {
args.push('-codec:a', 'copy');
}
// Metadata tags
const trackTitle = opts.trackTitle ? String(opts.trackTitle).trim() : null;
const trackArtist = (opts.trackArtist || opts.albumArtist)
? String(opts.trackArtist || opts.albumArtist).trim() : null;
const albumTitle = opts.albumTitle ? String(opts.albumTitle).trim() : null;
const albumYear = opts.albumYear ? String(opts.albumYear).trim() : null;
const trackNumber = opts.trackNumber != null ? String(opts.trackNumber) : null;
if (trackTitle) args.push('-metadata', `title=${trackTitle}`);
if (trackArtist) args.push('-metadata', `artist=${trackArtist}`);
if (albumTitle) args.push('-metadata', `album=${albumTitle}`);
if (albumYear) args.push('-metadata', `date=${albumYear}`);
if (trackNumber) args.push('-metadata', `track=${trackNumber}`);
args.push(outputFile);
return { cmd: ffmpegCmd, args };
}
@@ -345,12 +359,110 @@ class ConverterPlugin extends SourcePlugin {
const outputFormat = String(encodePlan.outputFormat || encodePlan.audioFormat || 'flac').trim().toLowerCase();
const formatOptions = encodePlan.audioFormatOptions || {};
const isFolder = Boolean(encodePlan.isFolder);
const isSharedAudio = Boolean(encodePlan.isSharedAudio)
|| (Array.isArray(encodePlan.inputPaths) && encodePlan.inputPaths.length > 0 && !isFolder);
// Metadata from user config
const planMetadata = encodePlan.metadata || {};
const planTracks = Array.isArray(encodePlan.tracks) ? encodePlan.tracks : [];
if (!AUDIO_OUTPUT_FORMATS.has(outputFormat)) {
throw new Error(`Unbekanntes Audio-Ausgabeformat: ${outputFormat}`);
}
if (isFolder) {
/** Build per-track metadata opts merged with album-level metadata */
function trackMetaOpts(position, defaultBaseName) {
const trackMeta = planTracks.find((t) => t.position === position) || {};
return {
...formatOptions,
trackTitle: trackMeta.title || defaultBaseName || null,
trackArtist: trackMeta.artist || planMetadata.albumArtist || null,
albumTitle: planMetadata.albumTitle || null,
albumArtist: planMetadata.albumArtist || null,
albumYear: planMetadata.albumYear ? String(planMetadata.albumYear) : null,
trackNumber: position
};
}
/** Build output filename with optional track number + title */
function buildOutputFileName(position, trackMeta, defaultBaseName) {
const numStr = String(position).padStart(2, '0');
const title = trackMeta?.title ? sanitizeFileName(trackMeta.title) : null;
const templateRaw = String(encodePlan.audioTrackTemplate || '').trim();
if (templateRaw) {
const rendered = renderTemplate(templateRaw, {
trackNr: numStr,
trackNumber: String(position),
title: title || sanitizeFileName(defaultBaseName) || `Track ${numStr}`,
artist: sanitizeFileName(trackMeta?.artist || planMetadata.albumArtist || '') || 'unknown',
album: sanitizeFileName(planMetadata.albumTitle || '') || 'unknown',
year: planMetadata.albumYear || 'unknown'
});
const sanitized = sanitizeFileName(rendered);
if (sanitized) {
return `${sanitized}.${outputFormat}`;
}
}
if (title) return `${numStr} - ${title}.${outputFormat}`;
return `${sanitizeFileName(defaultBaseName) || numStr}.${outputFormat}`;
}
if (isSharedAudio) {
// Freigegebene Audio-Dateien (mehrere Einzeldateien in einem Job)
const inputPaths = encodePlan.inputPaths || [];
if (inputPaths.length === 0) {
throw new Error('ConverterPlugin._encodeAudio(): inputPaths leer für shared-audio-Job');
}
if (!outputDir) {
throw new Error('ConverterPlugin._encodeAudio(): outputDir fehlt für shared-audio-Job');
}
ensureDir(outputDir);
ctx.logger.info('converter:encode:audio:shared:start', {
jobId: job?.id, fileCount: inputPaths.length, outputDir, format: outputFormat
});
for (let i = 0; i < inputPaths.length; i++) {
if (typeof isCancelled === 'function' && isCancelled()) {
const err = new Error('Job wurde vom Benutzer abgebrochen.');
err.statusCode = 409;
throw err;
}
const inputFile = inputPaths[i];
const fileName = path.basename(inputFile);
const baseName = path.basename(fileName, path.extname(fileName));
const position = i + 1;
const trackMeta = planTracks.find((t) => t.position === position) || {};
const outputFileName = buildOutputFileName(position, trackMeta, baseName);
const outputFile = path.join(outputDir, outputFileName);
ctx.logger.info(`converter:encode:audio:track:${position}`, { inputFile, outputFile });
ctx.emitProgress(
Math.round((i / inputPaths.length) * 100),
`Audio: ${position}/${inputPaths.length}${fileName}`
);
const encodeConfig = buildAudioFfmpegArgs(
ffmpegCmd, inputFile, outputFile, outputFormat,
trackMetaOpts(position, baseName)
);
await _spawnAndWait({
cmd: encodeConfig.cmd, args: encodeConfig.args,
jobId: job?.id,
isCancelled,
onProcessHandle,
context: { jobId: job?.id, source: 'ConverterPlugin.encode', stage: 'ENCODING' }
});
}
ctx.logger.info('converter:encode:audio:shared:done', {
jobId: job?.id, fileCount: inputPaths.length, outputDir
});
ctx.extra.encodeResult = { outputDir, format: outputFormat, fileCount: inputPaths.length };
} else if (isFolder) {
// Ordner-Modus: alle Audio-Dateien im Ordner konvertieren
const audioFiles = encodePlan.audioFiles || listAudioFilesInDir(inputPath);
@@ -373,15 +485,21 @@ class ConverterPlugin extends SourcePlugin {
const fileName = audioFiles[i];
const inputFile = path.join(inputPath, fileName);
const baseName = path.basename(fileName, path.extname(fileName));
const outputFile = path.join(outputDir, `${sanitizeFileName(baseName)}.${outputFormat}`);
const position = i + 1;
const trackMeta = planTracks.find((t) => t.position === position) || {};
const outputFileName = buildOutputFileName(position, trackMeta, baseName);
const outputFile = path.join(outputDir, outputFileName);
ctx.logger.info(`converter:encode:audio:track:${i + 1}`, { inputFile, outputFile });
ctx.logger.info(`converter:encode:audio:track:${position}`, { inputFile, outputFile });
ctx.emitProgress(
Math.round((i / audioFiles.length) * 100),
`Audio: ${i + 1}/${audioFiles.length}${fileName}`
`Audio: ${position}/${audioFiles.length}${fileName}`
);
const encodeConfig = buildAudioFfmpegArgs(ffmpegCmd, inputFile, outputFile, outputFormat, formatOptions);
const encodeConfig = buildAudioFfmpegArgs(
ffmpegCmd, inputFile, outputFile, outputFormat,
trackMetaOpts(position, baseName)
);
await _spawnAndWait({
cmd: encodeConfig.cmd, args: encodeConfig.args,
@@ -409,7 +527,11 @@ class ConverterPlugin extends SourcePlugin {
});
ctx.emitProgress(0, `Audio: Encoding läuft …`);
const encodeConfig = buildAudioFfmpegArgs(ffmpegCmd, inputPath, outputPath, outputFormat, formatOptions);
const baseName = path.basename(inputPath, path.extname(inputPath));
const encodeConfig = buildAudioFfmpegArgs(
ffmpegCmd, inputPath, outputPath, outputFormat,
trackMetaOpts(1, baseName)
);
await _spawnAndWait({
cmd: encodeConfig.cmd, args: encodeConfig.args,