From cbd63de17d5be105b1be5934af7707fa0d174057 Mon Sep 17 00:00:00 2001 From: Quentin King Date: Sun, 4 Jan 2026 10:11:10 -0600 Subject: [PATCH] Add series metadata tags to FFmpeg encoding --- server/utils/ffmpegHelpers.js | 45 ++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index ef0447347..47b740883 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -392,6 +392,34 @@ function escapeFFMetadataValue(value) { return value.replace(/([;=\n\\#])/g, '\\$1') } +/** + * Computes the ALBUMSORT tag for proper series ordering. + * Format: + * - No series: "Title" or "Title - Subtitle" + * - In series: "Series Name 001 - Title" (zero-padded sequence) + * + * @param {import('../models/LibraryItem')} libraryItem + * @returns {string} + */ +function computeAlbumSort(libraryItem) { + const title = libraryItem.media.title || '' + const subtitle = libraryItem.media.subtitle + const series = libraryItem.media.series?.[0] + + if (series?.name) { + // In series: "Series Name 001 - Title" + const sequence = series.bookSeries?.sequence || '' + const paddedSequence = sequence ? String(sequence).padStart(3, '0') : '' + return `${series.name} ${paddedSequence} - ${title}`.trim() + } + + // Not in series: "Title" or "Title - Subtitle" + if (subtitle) { + return `${title} - ${subtitle}` + } + return title +} + /** * Retrieves the FFmpeg metadata object for a given library item. * @@ -408,6 +436,11 @@ function getFFMetadataObject(libraryItem, audioFilesLength) { format = 'unabridged' } + // Get first series for series tags (multiple series not widely supported) + const primarySeries = libraryItem.media.series?.[0] + const seriesName = primarySeries?.name + const seriesSequence = primarySeries?.bookSeries?.sequence + const ffmetadata = { title: libraryItem.media.title, artist: libraryItem.media.authorName, @@ -431,7 +464,17 @@ function getFFMetadataObject(libraryItem, audioFilesLength) { LANGUAGE: libraryItem.media.language, EXPLICIT: libraryItem.media.explicit ? '1' : null, ITUNESADVISORY: libraryItem.media.explicit ? '1' : '2', // 1 = explicit, 2 = clean - FORMAT: format + FORMAT: format, + // Series tags (Mp3tag standard) + SERIES: seriesName, + 'SERIES-PART': seriesSequence, + PART: seriesSequence, // Libation uses PART + // M4B-specific series tags + MOVEMENTNAME: seriesName, + MOVEMENT: seriesSequence, + SHOWMOVEMENT: seriesName ? '1' : null, + // Album sort for proper series ordering + ALBUMSORT: computeAlbumSort(libraryItem) } Object.keys(ffmetadata).forEach((key) => { if (!ffmetadata[key]) {