From 6fbbc65edf429e5d4f6ef3d05c8b31a4ea2bf94b Mon Sep 17 00:00:00 2001 From: mikiher Date: Sat, 29 Jun 2024 20:04:23 +0300 Subject: [PATCH 1/7] Replace tone with ffmpeg for metadata and cover embedding --- server/managers/AudioMetadataManager.js | 66 ++++++------- server/utils/ffmpegHelpers.js | 123 ++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 39 deletions(-) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 11c82822..8b068223 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -5,7 +5,7 @@ const Logger = require('../Logger') const fs = require('../libs/fsExtra') -const toneHelpers = require('../utils/toneHelpers') +const ffmpegHelpers = require('../utils/ffmpegHelpers') const TaskManager = require('./TaskManager') @@ -21,22 +21,19 @@ class AudioMetadataMangaer { } /** - * Get queued task data - * @return {Array} - */ + * Get queued task data + * @return {Array} + */ getQueuedTaskData() { - return this.tasksQueued.map(t => t.data) + return this.tasksQueued.map((t) => t.data) } getIsLibraryItemQueuedOrProcessing(libraryItemId) { - return this.tasksQueued.some(t => t.data.libraryItemId === libraryItemId) || this.tasksRunning.some(t => t.data.libraryItemId === libraryItemId) + return this.tasksQueued.some((t) => t.data.libraryItemId === libraryItemId) || this.tasksRunning.some((t) => t.data.libraryItemId === libraryItemId) } getToneMetadataObjectForApi(libraryItem) { - const audioFiles = libraryItem.media.includedAudioFiles - let mimeType = audioFiles[0].mimeType - if (audioFiles.some(a => a.mimeType !== mimeType)) mimeType = null - return toneHelpers.getToneMetadataObject(libraryItem, libraryItem.media.chapters, libraryItem.media.tracks.length, mimeType) + return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length) } handleBatchEmbed(user, libraryItems, options = {}) { @@ -56,29 +53,28 @@ class AudioMetadataMangaer { const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id) // Only writing chapters for single file audiobooks - const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters.map(c => ({ ...c })) : null + const chapters = audioFiles.length == 1 || forceEmbedChapters ? libraryItem.media.chapters.map((c) => ({ ...c })) : null let mimeType = audioFiles[0].mimeType - if (audioFiles.some(a => a.mimeType !== mimeType)) mimeType = null + if (audioFiles.some((a) => a.mimeType !== mimeType)) mimeType = null // Create task const taskData = { libraryItemId: libraryItem.id, libraryItemPath: libraryItem.path, userId: user.id, - audioFiles: audioFiles.map(af => ( - { - index: af.index, - ino: af.ino, - filename: af.metadata.filename, - path: af.metadata.path, - cachePath: Path.join(itemCachePath, af.metadata.filename) - } - )), + audioFiles: audioFiles.map((af) => ({ + index: af.index, + ino: af.ino, + filename: af.metadata.filename, + path: af.metadata.path, + cachePath: Path.join(itemCachePath, af.metadata.filename) + })), coverPath: libraryItem.media.coverPath, - metadataObject: toneHelpers.getToneMetadataObject(libraryItem, chapters, audioFiles.length, mimeType), + metadataObject: ffmpegHelpers.getFFMetadataObject(libraryItem, audioFiles.length), itemCachePath, chapters, + mimeType, options: { forceEmbedChapters, backupFiles @@ -107,18 +103,19 @@ class AudioMetadataMangaer { // Ensure item cache dir exists let cacheDirCreated = false - if (!await fs.pathExists(task.data.itemCachePath)) { + if (!(await fs.pathExists(task.data.itemCachePath))) { await fs.mkdir(task.data.itemCachePath) cacheDirCreated = true } // Create metadata json file - const toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json') + const ffmetadataPath = Path.join(task.data.itemCachePath, 'ffmetadata.txt') try { - await fs.writeFile(toneJsonPath, JSON.stringify({ meta: task.data.metadataObject }, null, 2)) + await fs.writeFile(ffmetadataPath, ffmpegHelpers.generateFFMetadata(task.data.metadataObject, task.data.chapters)) + Logger.debug(`[AudioMetadataManager] Wrote ${ffmetadataPath}`) } catch (error) { - Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error) - task.setFailed('Failed to write metadata.json') + Logger.error(`[AudioMetadataManager] Write ${ffmetadataPath} failed`, error) + task.setFailed('Failed to write file ffmetadata.txt') this.handleTaskFinished(task) return } @@ -141,16 +138,7 @@ class AudioMetadataMangaer { } } - const _toneMetadataObject = { - 'ToneJsonFile': toneJsonPath, - 'TrackNumber': af.index, - } - - if (task.data.coverPath) { - _toneMetadataObject['CoverFile'] = task.data.coverPath - } - - const success = await toneHelpers.tagAudioFile(af.path, _toneMetadataObject) + const success = await ffmpegHelpers.addCoverAndMetadataToFile(af.path, task.data.coverPath, ffmetadataPath, af.path) if (success) { Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${af.path}"`) } @@ -167,7 +155,7 @@ class AudioMetadataMangaer { if (cacheDirCreated) { await fs.remove(task.data.itemCachePath) } else { - await fs.remove(toneJsonPath) + await fs.remove(ffmetadataPath) } } @@ -177,7 +165,7 @@ class AudioMetadataMangaer { handleTaskFinished(task) { TaskManager.taskFinished(task) - this.tasksRunning = this.tasksRunning.filter(t => t.id !== task.id) + this.tasksRunning = this.tasksRunning.filter((t) => t.id !== task.id) if (this.tasksRunning.length < this.MAX_CONCURRENT_TASKS && this.tasksQueued.length) { Logger.info(`[AudioMetadataManager] Task finished and dequeueing next task. ${this.tasksQueued} tasks queued.`) diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 491f59dc..c4cf43b9 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -1,6 +1,7 @@ const axios = require('axios') const Ffmpeg = require('../libs/fluentFfmpeg') const fs = require('../libs/fsExtra') +const os = require('os') const Path = require('path') const Logger = require('../Logger') const { filePathToPOSIX } = require('./fileUtils') @@ -184,3 +185,125 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { ffmpeg.run() }) } + +/** + * Generates ffmetadata file content from the provided metadata object and chapters array. + * @param {Object} metadata - The input metadata object. + * @param {Array} chapters - An array of chapter objects. + * @returns {string} - The ffmetadata file content. + */ +function generateFFMetadata(metadata, chapters) { + let ffmetadataContent = ';FFMETADATA1\n' + + // Add global metadata + for (const key in metadata) { + if (metadata[key]) { + ffmetadataContent += `${key}=${escapeFFMetadataValue(metadata[key])}\n` + } + } + + // Add chapters + chapters.forEach((chapter) => { + ffmetadataContent += '\n[CHAPTER]\n' + ffmetadataContent += `TIMEBASE=1/1000\n` + ffmetadataContent += `START=${Math.floor(chapter.start * 1000)}\n` + ffmetadataContent += `END=${Math.floor(chapter.end * 1000)}\n` + if (chapter.title) { + ffmetadataContent += `title=${escapeFFMetadataValue(chapter.title)}\n` + } + }) + + return ffmetadataContent +} + +module.exports.generateFFMetadata = generateFFMetadata + +/** + * Adds an ffmetadata and optionally a cover image to an audio file using fluent-ffmpeg. + * @param {string} audioFilePath - Path to the input audio file. + * @param {string|null} coverFilePath - Path to the cover image file. + * @param {string} metadataFilePath - Path to the ffmetadata file. + */ +async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath) { + return new Promise((resolve) => { + const tempFilePath = Path.join(os.tmpdir(), 'temp_output.m4b') + let ffmpeg = Ffmpeg() + ffmpeg.input(audioFilePath).input(metadataFilePath).outputOptions([ + '-map 0:a', // map audio stream from input file + '-map_metadata 1', // map metadata from metadata file + '-map_chapters 1', // map chapters from metadata file + '-c copy', // copy streams + '-f mp4' // force mp4 format + ]) + + if (coverFilePath) { + ffmpeg.input(coverFilePath).outputOptions([ + '-map 2:v', // map video stream from cover image file + '-disposition:v:0 attached_pic', // set cover image as attached picture + '-metadata:s:v', + 'title=Cover', // add title metadata to cover image stream + '-metadata:s:v', + 'comment=Cover' // add comment metadata to cover image stream + ]) + } else { + ffmpeg.outputOptions([ + '-map 0:v?' // retain video stream from input file if exists + ]) + } + + ffmpeg + .output(tempFilePath) + .on('start', function (commandLine) { + Logger.debug('[ffmpegHelpers] Spawned Ffmpeg with command: ' + commandLine) + }) + .on('end', (stdout, stderr) => { + Logger.debug('[ffmpegHelpers] ffmpeg stdout:', stdout) + Logger.debug('[ffmpegHelpers] ffmpeg stderr:', stderr) + fs.copyFileSync(tempFilePath, audioFilePath) + fs.unlinkSync(tempFilePath) + resolve(true) + }) + .on('error', (err, stdout, stderr) => { + Logger.error('Error adding cover image and metadata:', err) + Logger.error('ffmpeg stdout:', stdout) + Logger.error('ffmpeg stderr:', stderr) + resolve(false) + }) + + ffmpeg.run() + }) +} + +module.exports.addCoverAndMetadataToFile = addCoverAndMetadataToFile + +function escapeFFMetadataValue(value) { + return value.replace(/([;=\n\\#])/g, '\\$1') +} + +function getFFMetadataObject(libraryItem, audioFilesLength) { + const metadata = libraryItem.media.metadata + + const ffmetadata = { + title: metadata.title, + artist: metadata.authors?.map((a) => a.name).join(', '), + album_artist: metadata.authors?.map((a) => a.name).join(', '), + album: (metadata.title || '') + (metadata.subtitle ? `: ${metadata.subtitle}` : ''), + genre: metadata.genres?.join('; '), + date: metadata.publishedYear, + comment: metadata.description, + description: metadata.description, + composer: metadata.narratorName, + copyright: metadata.publisher, + grouping: metadata.series?.map((s) => s.name + (s.sequence ? ` #${s.sequence}` : '')).join(', ') + } + + Object.keys(ffmetadata).forEach((key) => { + if (!ffmetadata[key]) { + delete ffmetadata[key] + } + }) + + return ffmetadata +} + +module.exports.getFFMetadataObject = getFFMetadataObject From a21b1f3b168208977fb566a84b76a9f05f8375c1 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 30 Jun 2024 15:45:25 +0300 Subject: [PATCH 2/7] Make required changes for mp3 embedding --- server/managers/AudioMetadataManager.js | 2 +- server/utils/ffmpegHelpers.js | 57 +++++++++++++++++-------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 8b068223..b9133de8 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -138,7 +138,7 @@ class AudioMetadataMangaer { } } - const success = await ffmpegHelpers.addCoverAndMetadataToFile(af.path, task.data.coverPath, ffmetadataPath, af.path) + const success = await ffmpegHelpers.addCoverAndMetadataToFile(af.path, task.data.coverPath, ffmetadataPath, task.data.mimeType) if (success) { Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${af.path}"`) } diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index c4cf43b9..c9cddbf4 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -189,7 +189,7 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { /** * Generates ffmetadata file content from the provided metadata object and chapters array. * @param {Object} metadata - The input metadata object. - * @param {Array} chapters - An array of chapter objects. + * @param {Array|null} chapters - An array of chapter objects. * @returns {string} - The ffmetadata file content. */ function generateFFMetadata(metadata, chapters) { @@ -203,15 +203,17 @@ function generateFFMetadata(metadata, chapters) { } // Add chapters - chapters.forEach((chapter) => { - ffmetadataContent += '\n[CHAPTER]\n' - ffmetadataContent += `TIMEBASE=1/1000\n` - ffmetadataContent += `START=${Math.floor(chapter.start * 1000)}\n` - ffmetadataContent += `END=${Math.floor(chapter.end * 1000)}\n` - if (chapter.title) { - ffmetadataContent += `title=${escapeFFMetadataValue(chapter.title)}\n` - } - }) + if (chapters) { + chapters.forEach((chapter) => { + ffmetadataContent += '\n[CHAPTER]\n' + ffmetadataContent += `TIMEBASE=1/1000\n` + ffmetadataContent += `START=${Math.floor(chapter.start * 1000)}\n` + ffmetadataContent += `END=${Math.floor(chapter.end * 1000)}\n` + if (chapter.title) { + ffmetadataContent += `title=${escapeFFMetadataValue(chapter.title)}\n` + } + }) + } return ffmetadataContent } @@ -223,19 +225,37 @@ module.exports.generateFFMetadata = generateFFMetadata * @param {string} audioFilePath - Path to the input audio file. * @param {string|null} coverFilePath - Path to the cover image file. * @param {string} metadataFilePath - Path to the ffmetadata file. + * @param {string} mimeType - The MIME type of the audio file. */ -async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath) { +async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, mimeType) { + const isMp4 = mimeType === 'audio/mp4' + const isMp3 = mimeType === 'audio/mpeg' + + const audioFileDir = Path.dirname(audioFilePath) + const audioFileExt = Path.extname(audioFilePath) + const audioFileBaseName = Path.basename(audioFilePath, audioFileExt) + const tempFilePath = Path.join(audioFileDir, `${audioFileBaseName}.tmp${audioFileExt}`) + return new Promise((resolve) => { - const tempFilePath = Path.join(os.tmpdir(), 'temp_output.m4b') let ffmpeg = Ffmpeg() ffmpeg.input(audioFilePath).input(metadataFilePath).outputOptions([ '-map 0:a', // map audio stream from input file - '-map_metadata 1', // map metadata from metadata file + '-map_metadata 1', // map metadata tags from metadata file first + '-map_metadata 0', // add additional metadata tags from input file '-map_chapters 1', // map chapters from metadata file - '-c copy', // copy streams - '-f mp4' // force mp4 format + '-c copy' // copy streams ]) + if (isMp4) { + ffmpeg.outputOptions([ + '-f mp4' // force output format to mp4 + ]) + } else if (isMp3) { + ffmpeg.outputOptions([ + '-id3v2_version 3' // set ID3v2 version to 3 + ]) + } + if (coverFilePath) { ffmpeg.input(coverFilePath).outputOptions([ '-map 2:v', // map video stream from cover image file @@ -285,15 +305,18 @@ function getFFMetadataObject(libraryItem, audioFilesLength) { const ffmetadata = { title: metadata.title, - artist: metadata.authors?.map((a) => a.name).join(', '), - album_artist: metadata.authors?.map((a) => a.name).join(', '), + artist: metadata.authorName, + album_artist: metadata.authorName, album: (metadata.title || '') + (metadata.subtitle ? `: ${metadata.subtitle}` : ''), + TIT3: metadata.subtitle, // mp3 only genre: metadata.genres?.join('; '), date: metadata.publishedYear, comment: metadata.description, description: metadata.description, composer: metadata.narratorName, copyright: metadata.publisher, + publisher: metadata.publisher, // mp3 only + TRACKTOTAL: `${audioFilesLength}`, // mp3 only grouping: metadata.series?.map((s) => s.name + (s.sequence ? ` #${s.sequence}` : '')).join(', ') } From 4732ca811942be63b7dd2e6bafcc45291f5fb75a Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 1 Jul 2024 16:57:14 -0500 Subject: [PATCH 3/7] Embed track number --- server/managers/AudioMetadataManager.js | 2 +- server/utils/ffmpegHelpers.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index b9133de8..4d90b5df 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -138,7 +138,7 @@ class AudioMetadataMangaer { } } - const success = await ffmpegHelpers.addCoverAndMetadataToFile(af.path, task.data.coverPath, ffmetadataPath, task.data.mimeType) + const success = await ffmpegHelpers.addCoverAndMetadataToFile(af.path, task.data.coverPath, ffmetadataPath, af.index, task.data.mimeType) if (success) { Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${af.path}"`) } diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index c9cddbf4..ef41b46d 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -225,9 +225,10 @@ module.exports.generateFFMetadata = generateFFMetadata * @param {string} audioFilePath - Path to the input audio file. * @param {string|null} coverFilePath - Path to the cover image file. * @param {string} metadataFilePath - Path to the ffmetadata file. + * @param {number} track - The track number to embed in the audio file. * @param {string} mimeType - The MIME type of the audio file. */ -async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, mimeType) { +async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, track, mimeType) { const isMp4 = mimeType === 'audio/mp4' const isMp3 = mimeType === 'audio/mpeg' @@ -246,6 +247,10 @@ async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataF '-c copy' // copy streams ]) + if (track && !isNaN(track)) { + ffmpeg.outputOptions(['-metadata track=' + track]) + } + if (isMp4) { ffmpeg.outputOptions([ '-f mp4' // force output format to mp4 From ebaec2364898c3579f79d5d915cdeafbd0709bcb Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 2 Jul 2024 18:25:04 +0300 Subject: [PATCH 4/7] Replace tone with ffmpeg in AbMergeManager --- server/managers/AbMergeManager.js | 86 +++++++++++++------------ server/managers/AudioMetadataManager.js | 12 ++-- server/utils/ffmpegHelpers.js | 13 ++++ 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index 8a87df2e..711e8892 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -1,4 +1,3 @@ - const Path = require('path') const fs = require('../libs/fsExtra') @@ -7,7 +6,7 @@ const Logger = require('../Logger') const TaskManager = require('./TaskManager') const Task = require('../objects/Task') const { writeConcatFile } = require('../utils/ffmpegHelpers') -const toneHelpers = require('../utils/toneHelpers') +const ffmpegHelpers = require('../utils/ffmpegHelpers') class AbMergeManager { constructor() { @@ -17,7 +16,7 @@ class AbMergeManager { } getPendingTaskByLibraryItemId(libraryItemId) { - return this.pendingTasks.find(t => t.task.data.libraryItemId === libraryItemId) + return this.pendingTasks.find((t) => t.task.data.libraryItemId === libraryItemId) } cancelEncode(task) { @@ -31,23 +30,27 @@ class AbMergeManager { const targetFilename = audiobookDirname + '.m4b' const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id) const tempFilepath = Path.join(itemCachePath, targetFilename) + const ffmetadataPath = Path.join(itemCachePath, 'ffmetadata.txt') const taskData = { libraryItemId: libraryItem.id, libraryItemPath: libraryItem.path, userId: user.id, - originalTrackPaths: libraryItem.media.tracks.map(t => t.metadata.path), + originalTrackPaths: libraryItem.media.tracks.map((t) => t.metadata.path), tempFilepath, targetFilename, targetFilepath: Path.join(libraryItem.path, targetFilename), itemCachePath, - toneJsonObject: null + ffmetadataObject: ffmpegHelpers.getFFMetadataObject(libraryItem, 1), + chapters: libraryItem.media.chapters?.map((c) => ({ ...c })), + coverPath: libraryItem.media.coverPath, + ffmetadataPath } const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.` task.setData('encode-m4b', 'Encoding M4b', taskDescription, false, taskData) TaskManager.addTask(task) Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`) - if (!await fs.pathExists(taskData.itemCachePath)) { + if (!(await fs.pathExists(taskData.itemCachePath))) { await fs.mkdir(taskData.itemCachePath) } @@ -55,6 +58,15 @@ class AbMergeManager { } async runAudiobookMerge(libraryItem, task, encodingOptions) { + // Create ffmetadata file + const success = await ffmpegHelpers.writeFFMetadataFile(task.data.metadataObject, task.data.chapters, task.data.ffmetadataPath) + if (!success) { + Logger.error(`[AudioMetadataManager] Failed to write ffmetadata file for audiobook "${task.data.libraryItemId}"`) + task.setFailed('Failed to write metadata file.') + this.removeTask(task, true) + return + } + const audioBitrate = encodingOptions.bitrate || '128k' const audioCodec = encodingOptions.codec || 'aac' const audioChannels = encodingOptions.channels || 2 @@ -90,12 +102,7 @@ class AbMergeManager { const ffmpegOutputOptions = ['-f mp4'] if (audioRequiresEncode) { - ffmpegOptions = ffmpegOptions.concat([ - '-map 0:a', - `-acodec ${audioCodec}`, - `-ac ${audioChannels}`, - `-b:a ${audioBitrate}` - ]) + ffmpegOptions = ffmpegOptions.concat(['-map 0:a', `-acodec ${audioCodec}`, `-ac ${audioChannels}`, `-b:a ${audioBitrate}`]) } else { ffmpegOptions.push('-max_muxing_queue_size 1000') @@ -106,24 +113,6 @@ class AbMergeManager { } } - let toneJsonPath = null - try { - toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json') - await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath, 1, 'audio/mp4') - } catch (error) { - Logger.error(`[AbMergeManager] Write metadata.json failed`, error) - toneJsonPath = null - } - - task.data.toneJsonObject = { - 'ToneJsonFile': toneJsonPath, - 'TrackNumber': 1, - } - - if (libraryItem.media.coverPath) { - task.data.toneJsonObject['CoverFile'] = libraryItem.media.coverPath - } - const workerData = { inputs: ffmpegInputs, options: ffmpegOptions, @@ -162,7 +151,7 @@ class AbMergeManager { async sendResult(task, result) { // Remove pending task - this.pendingTasks = this.pendingTasks.filter(d => d.id !== task.id) + this.pendingTasks = this.pendingTasks.filter((d) => d.id !== task.id) if (result.isKilled) { task.setFailed('Ffmpeg task killed') @@ -177,7 +166,7 @@ class AbMergeManager { } // Write metadata to merged file - const success = await toneHelpers.tagAudioFile(task.data.tempFilepath, task.data.toneJsonObject) + const success = await ffmpegHelpers.addCoverAndMetadataToFile(task.data.tempFilepath, task.data.coverPath, task.data.ffmetadataPath, 1, 'audio/mp4') if (!success) { Logger.error(`[AbMergeManager] Failed to write metadata to file "${task.data.tempFilepath}"`) task.setFailed('Failed to write metadata to m4b file') @@ -199,6 +188,9 @@ class AbMergeManager { Logger.debug(`[AbMergeManager] Moving m4b from ${task.data.tempFilepath} to ${task.data.targetFilepath}`) await fs.move(task.data.tempFilepath, task.data.targetFilepath) + // Remove ffmetadata file + await fs.remove(task.data.ffmetadataPath) + task.setFinished() await this.removeTask(task, false) Logger.info(`[AbMergeManager] Ab task finished ${task.id}`) @@ -207,9 +199,9 @@ class AbMergeManager { async removeTask(task, removeTempFilepath = false) { Logger.info('[AbMergeManager] Removing task ' + task.id) - const pendingDl = this.pendingTasks.find(d => d.id === task.id) + const pendingDl = this.pendingTasks.find((d) => d.id === task.id) if (pendingDl) { - this.pendingTasks = this.pendingTasks.filter(d => d.id !== task.id) + this.pendingTasks = this.pendingTasks.filter((d) => d.id !== task.id) if (pendingDl.worker) { Logger.warn(`[AbMergeManager] Removing download in progress - stopping worker`) try { @@ -223,13 +215,27 @@ class AbMergeManager { } } - if (removeTempFilepath) { // On failed tasks remove the bad file if it exists + if (removeTempFilepath) { + // On failed tasks remove the bad file if it exists if (await fs.pathExists(task.data.tempFilepath)) { - await fs.remove(task.data.tempFilepath).then(() => { - Logger.info('[AbMergeManager] Deleted target file', task.data.tempFilepath) - }).catch((err) => { - Logger.error('[AbMergeManager] Failed to delete target file', err) - }) + await fs + .remove(task.data.tempFilepath) + .then(() => { + Logger.info('[AbMergeManager] Deleted target file', task.data.tempFilepath) + }) + .catch((err) => { + Logger.error('[AbMergeManager] Failed to delete target file', err) + }) + } + if (await fs.pathExists(task.data.ffmetadataPath)) { + await fs + .remove(task.data.ffmetadataPath) + .then(() => { + Logger.info('[AbMergeManager] Deleted ffmetadata file', task.data.ffmetadataPath) + }) + .catch((err) => { + Logger.error('[AbMergeManager] Failed to delete ffmetadata file', err) + }) } } diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 4d90b5df..44fbdb57 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -108,14 +108,12 @@ class AudioMetadataMangaer { cacheDirCreated = true } - // Create metadata json file + // Create ffmetadata file const ffmetadataPath = Path.join(task.data.itemCachePath, 'ffmetadata.txt') - try { - await fs.writeFile(ffmetadataPath, ffmpegHelpers.generateFFMetadata(task.data.metadataObject, task.data.chapters)) - Logger.debug(`[AudioMetadataManager] Wrote ${ffmetadataPath}`) - } catch (error) { - Logger.error(`[AudioMetadataManager] Write ${ffmetadataPath} failed`, error) - task.setFailed('Failed to write file ffmetadata.txt') + const success = await ffmpegHelpers.writeFFMetadataFile(task.data.metadataObject, task.data.chapters, ffmetadataPath) + if (!success) { + Logger.error(`[AudioMetadataManager] Failed to write ffmetadata file for audiobook "${task.data.libraryItemId}"`) + task.setFailed('Failed to write metadata file.') this.handleTaskFinished(task) return } diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index ef41b46d..28c6e618 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -220,6 +220,19 @@ function generateFFMetadata(metadata, chapters) { module.exports.generateFFMetadata = generateFFMetadata +async function writeFFMetadataFile(metadata, chapters, ffmetadataPath) { + try { + await fs.writeFile(ffmetadataPath, generateFFMetadata(metadata, chapters)) + Logger.debug(`[ffmpegHelpers] Wrote ${ffmetadataPath}`) + return true + } catch (error) { + Logger.error(`[ffmpegHelpers] Write ${ffmetadataPath} failed`, error) + return false + } +} + +module.exports.writeFFMetadataFile = writeFFMetadataFile + /** * Adds an ffmetadata and optionally a cover image to an audio file using fluent-ffmpeg. * @param {string} audioFilePath - Path to the input audio file. From 1b015beba403621e618b28721a7ccf44334cad61 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 2 Jul 2024 19:00:03 +0300 Subject: [PATCH 5/7] Remove windows restrictions from Tools.vuw --- client/components/modals/item/tabs/Tools.vue | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue index de19e04c..b05b2187 100644 --- a/client/components/modals/item/tabs/Tools.vue +++ b/client/components/modals/item/tabs/Tools.vue @@ -2,11 +2,8 @@

{{ $strings.HeaderAudiobookTools }}

- - Not supported for the Windows install yet - -
+

{{ $strings.LabelToolsMakeM4b }}

@@ -23,7 +20,7 @@
-
+

{{ $strings.LabelToolsEmbedMetadata }}

@@ -111,12 +108,6 @@ export default { }, isEncodeTaskRunning() { return this.encodeTask && !this.encodeTask?.isFinished - }, - isWindowsInstall() { - return this.Source == 'windows' - }, - Source() { - return this.$store.state.Source } }, methods: { @@ -141,4 +132,4 @@ export default { } } } - \ No newline at end of file + From 8bdee51798731b136a46f98a183860f1622781e2 Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 3 Jul 2024 23:50:42 +0300 Subject: [PATCH 6/7] Add unit tests for new ffmpegHelpers functions --- server/utils/ffmpegHelpers.js | 24 ++- test/server/utils/ffmpegHelpers.test.js | 249 ++++++++++++++++++++++++ 2 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 test/server/utils/ffmpegHelpers.test.js diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index 28c6e618..2a242504 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -5,6 +5,7 @@ const os = require('os') const Path = require('path') const Logger = require('../Logger') const { filePathToPOSIX } = require('./fileUtils') +const LibraryItem = require('../objects/LibraryItem') function escapeSingleQuotes(path) { // return path.replace(/'/g, '\'\\\'\'') @@ -220,6 +221,14 @@ function generateFFMetadata(metadata, chapters) { module.exports.generateFFMetadata = generateFFMetadata +/** + * Writes FFmpeg metadata file with the given metadata and chapters. + * + * @param {Object} metadata - The metadata object. + * @param {Array} chapters - The array of chapter objects. + * @param {string} ffmetadataPath - The path to the FFmpeg metadata file. + * @returns {Promise} - A promise that resolves to true if the file was written successfully, false otherwise. + */ async function writeFFMetadataFile(metadata, chapters, ffmetadataPath) { try { await fs.writeFile(ffmetadataPath, generateFFMetadata(metadata, chapters)) @@ -235,23 +244,25 @@ module.exports.writeFFMetadataFile = writeFFMetadataFile /** * Adds an ffmetadata and optionally a cover image to an audio file using fluent-ffmpeg. + * * @param {string} audioFilePath - Path to the input audio file. * @param {string|null} coverFilePath - Path to the cover image file. * @param {string} metadataFilePath - Path to the ffmetadata file. * @param {number} track - The track number to embed in the audio file. * @param {string} mimeType - The MIME type of the audio file. + * @param {Ffmpeg} ffmpeg - The Ffmpeg instance to use (optional). Used for dependency injection in tests. + * @returns {Promise} A promise that resolves to true if the operation is successful, false otherwise. */ -async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, track, mimeType) { +async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, track, mimeType, ffmpeg = Ffmpeg()) { const isMp4 = mimeType === 'audio/mp4' const isMp3 = mimeType === 'audio/mpeg' const audioFileDir = Path.dirname(audioFilePath) const audioFileExt = Path.extname(audioFilePath) const audioFileBaseName = Path.basename(audioFilePath, audioFileExt) - const tempFilePath = Path.join(audioFileDir, `${audioFileBaseName}.tmp${audioFileExt}`) + const tempFilePath = filePathToPOSIX(Path.join(audioFileDir, `${audioFileBaseName}.tmp${audioFileExt}`)) return new Promise((resolve) => { - let ffmpeg = Ffmpeg() ffmpeg.input(audioFilePath).input(metadataFilePath).outputOptions([ '-map 0:a', // map audio stream from input file '-map_metadata 1', // map metadata tags from metadata file first @@ -318,6 +329,13 @@ function escapeFFMetadataValue(value) { return value.replace(/([;=\n\\#])/g, '\\$1') } +/** + * Retrieves the FFmpeg metadata object for a given library item. + * + * @param {LibraryItem} libraryItem - The library item containing the media metadata. + * @param {number} audioFilesLength - The length of the audio files. + * @returns {Object} - The FFmpeg metadata object. + */ function getFFMetadataObject(libraryItem, audioFilesLength) { const metadata = libraryItem.media.metadata diff --git a/test/server/utils/ffmpegHelpers.test.js b/test/server/utils/ffmpegHelpers.test.js new file mode 100644 index 00000000..82daf1ba --- /dev/null +++ b/test/server/utils/ffmpegHelpers.test.js @@ -0,0 +1,249 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const { generateFFMetadata, addCoverAndMetadataToFile } = require('../../../server/utils/ffmpegHelpers') +const fs = require('../../../server/libs/fsExtra') +const EventEmitter = require('events') + +global.isWin = process.platform === 'win32' + +describe('generateFFMetadata', () => { + function createTestSetup() { + const metadata = { + title: 'My Audiobook', + artist: 'John Doe', + album: 'Best Audiobooks' + } + + const chapters = [ + { start: 0, end: 1000, title: 'Chapter 1' }, + { start: 1000, end: 2000, title: 'Chapter 2' } + ] + + return { metadata, chapters } + } + + let metadata = null + let chapters = null + beforeEach(() => { + const input = createTestSetup() + metadata = input.metadata + chapters = input.chapters + }) + + it('should generate ffmetadata content with chapters', () => { + const result = generateFFMetadata(metadata, chapters) + + expect(result).to.equal(';FFMETADATA1\ntitle=My Audiobook\nartist=John Doe\nalbum=Best Audiobooks\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=0\nEND=1000000\ntitle=Chapter 1\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=1000000\nEND=2000000\ntitle=Chapter 2\n') + }) + + it('should generate ffmetadata content without chapters', () => { + chapters = null + + const result = generateFFMetadata(metadata, chapters) + + expect(result).to.equal(';FFMETADATA1\ntitle=My Audiobook\nartist=John Doe\nalbum=Best Audiobooks\n') + }) + + it('should handle chapters with no title', () => { + chapters = [ + { start: 0, end: 1000 }, + { start: 1000, end: 2000 } + ] + + const result = generateFFMetadata(metadata, chapters) + + expect(result).to.equal(';FFMETADATA1\ntitle=My Audiobook\nartist=John Doe\nalbum=Best Audiobooks\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=0\nEND=1000000\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=1000000\nEND=2000000\n') + }) + + it('should handle metadata escaping special characters (=, ;, #, and a newline)', () => { + metadata.title = 'My Audiobook; with = special # characters\n' + chapters[0].title = 'Chapter #1' + + const result = generateFFMetadata(metadata, chapters) + + expect(result).to.equal(';FFMETADATA1\ntitle=My Audiobook\\; with \\= special \\# characters\\\n\nartist=John Doe\nalbum=Best Audiobooks\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=0\nEND=1000000\ntitle=Chapter \\#1\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=1000000\nEND=2000000\ntitle=Chapter 2\n') + }) +}) + +describe('addCoverAndMetadataToFile', () => { + function createTestSetup() { + const audioFilePath = '/path/to/audio/file.mp3' + const coverFilePath = '/path/to/cover/image.jpg' + const metadataFilePath = '/path/to/metadata/file.txt' + const track = 1 + const mimeType = 'audio/mpeg' + + const ffmpegStub = new EventEmitter() + ffmpegStub.input = sinon.stub().returnsThis() + ffmpegStub.outputOptions = sinon.stub().returnsThis() + ffmpegStub.output = sinon.stub().returnsThis() + ffmpegStub.input = sinon.stub().returnsThis() + ffmpegStub.run = sinon.stub().callsFake(() => { + ffmpegStub.emit('end') + }) + const fsCopyFileSyncStub = sinon.stub(fs, 'copyFileSync') + const fsUnlinkSyncStub = sinon.stub(fs, 'unlinkSync') + + return { audioFilePath, coverFilePath, metadataFilePath, track, mimeType, ffmpegStub, fsCopyFileSyncStub, fsUnlinkSyncStub } + } + + let audioFilePath = null + let coverFilePath = null + let metadataFilePath = null + let track = null + let mimeType = null + let ffmpegStub = null + let fsCopyFileSyncStub = null + let fsUnlinkSyncStub = null + beforeEach(() => { + const input = createTestSetup() + audioFilePath = input.audioFilePath + coverFilePath = input.coverFilePath + metadataFilePath = input.metadataFilePath + track = input.track + mimeType = input.mimeType + ffmpegStub = input.ffmpegStub + fsCopyFileSyncStub = input.fsCopyFileSyncStub + fsUnlinkSyncStub = input.fsUnlinkSyncStub + }) + + it('should add cover image and metadata to audio file', async () => { + // Act + const result = await addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, track, mimeType, ffmpegStub) + + // Assert + expect(result).to.be.true + expect(ffmpegStub.input.calledThrice).to.be.true + expect(ffmpegStub.input.getCall(0).args[0]).to.equal(audioFilePath) + expect(ffmpegStub.input.getCall(1).args[0]).to.equal(metadataFilePath) + expect(ffmpegStub.input.getCall(2).args[0]).to.equal(coverFilePath) + + expect(ffmpegStub.outputOptions.callCount).to.equal(4) + expect(ffmpegStub.outputOptions.getCall(0).args[0]).to.deep.equal(['-map 0:a', '-map_metadata 1', '-map_metadata 0', '-map_chapters 1', '-c copy']) + expect(ffmpegStub.outputOptions.getCall(1).args[0]).to.deep.equal(['-metadata track=1']) + expect(ffmpegStub.outputOptions.getCall(2).args[0]).to.deep.equal(['-id3v2_version 3']) + expect(ffmpegStub.outputOptions.getCall(3).args[0]).to.deep.equal(['-map 2:v', '-disposition:v:0 attached_pic', '-metadata:s:v', 'title=Cover', '-metadata:s:v', 'comment=Cover']) + + expect(ffmpegStub.output.calledOnce).to.be.true + expect(ffmpegStub.output.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.mp3') + + expect(ffmpegStub.run.calledOnce).to.be.true + + expect(fsCopyFileSyncStub.calledOnce).to.be.true + expect(fsCopyFileSyncStub.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.mp3') + expect(fsCopyFileSyncStub.firstCall.args[1]).to.equal('/path/to/audio/file.mp3') + + expect(fsUnlinkSyncStub.calledOnce).to.be.true + expect(fsUnlinkSyncStub.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.mp3') + + // Restore the stub + sinon.restore() + }) + + it('should handle missing cover image', async () => { + // Arrange + coverFilePath = null + + // Act + const result = await addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, track, mimeType, ffmpegStub) + + // Assert + expect(result).to.be.true + expect(ffmpegStub.input.calledTwice).to.be.true + expect(ffmpegStub.input.getCall(0).args[0]).to.equal(audioFilePath) + expect(ffmpegStub.input.getCall(1).args[0]).to.equal(metadataFilePath) + + expect(ffmpegStub.outputOptions.callCount).to.equal(4) + expect(ffmpegStub.outputOptions.getCall(0).args[0]).to.deep.equal(['-map 0:a', '-map_metadata 1', '-map_metadata 0', '-map_chapters 1', '-c copy']) + expect(ffmpegStub.outputOptions.getCall(1).args[0]).to.deep.equal(['-metadata track=1']) + expect(ffmpegStub.outputOptions.getCall(2).args[0]).to.deep.equal(['-id3v2_version 3']) + expect(ffmpegStub.outputOptions.getCall(3).args[0]).to.deep.equal(['-map 0:v?']) + + expect(ffmpegStub.output.calledOnce).to.be.true + expect(ffmpegStub.output.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.mp3') + + expect(ffmpegStub.run.calledOnce).to.be.true + + expect(fsCopyFileSyncStub.calledOnce).to.be.true + expect(fsCopyFileSyncStub.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.mp3') + expect(fsCopyFileSyncStub.firstCall.args[1]).to.equal('/path/to/audio/file.mp3') + + expect(fsUnlinkSyncStub.calledOnce).to.be.true + expect(fsUnlinkSyncStub.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.mp3') + + // Restore the stub + sinon.restore() + }) + + it('should handle error during ffmpeg execution', async () => { + // Arrange + ffmpegStub.run = sinon.stub().callsFake(() => { + ffmpegStub.emit('error', new Error('FFmpeg error')) + }) + + // Act + const result = await addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, track, mimeType, ffmpegStub) + + // Assert + expect(result).to.be.false + expect(ffmpegStub.input.calledThrice).to.be.true + expect(ffmpegStub.input.getCall(0).args[0]).to.equal(audioFilePath) + expect(ffmpegStub.input.getCall(1).args[0]).to.equal(metadataFilePath) + expect(ffmpegStub.input.getCall(2).args[0]).to.equal(coverFilePath) + + expect(ffmpegStub.outputOptions.callCount).to.equal(4) + expect(ffmpegStub.outputOptions.getCall(0).args[0]).to.deep.equal(['-map 0:a', '-map_metadata 1', '-map_metadata 0', '-map_chapters 1', '-c copy']) + expect(ffmpegStub.outputOptions.getCall(1).args[0]).to.deep.equal(['-metadata track=1']) + expect(ffmpegStub.outputOptions.getCall(2).args[0]).to.deep.equal(['-id3v2_version 3']) + expect(ffmpegStub.outputOptions.getCall(3).args[0]).to.deep.equal(['-map 2:v', '-disposition:v:0 attached_pic', '-metadata:s:v', 'title=Cover', '-metadata:s:v', 'comment=Cover']) + + expect(ffmpegStub.output.calledOnce).to.be.true + expect(ffmpegStub.output.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.mp3') + + expect(ffmpegStub.run.calledOnce).to.be.true + + expect(fsCopyFileSyncStub.called).to.be.false + + expect(fsUnlinkSyncStub.called).to.be.false + + // Restore the stub + sinon.restore() + }) + + it('should handle m4b embedding', async () => { + // Arrange + mimeType = 'audio/mp4' + audioFilePath = '/path/to/audio/file.m4b' + + // Act + const result = await addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataFilePath, track, mimeType, ffmpegStub) + + // Assert + expect(result).to.be.true + expect(ffmpegStub.input.calledThrice).to.be.true + expect(ffmpegStub.input.getCall(0).args[0]).to.equal(audioFilePath) + expect(ffmpegStub.input.getCall(1).args[0]).to.equal(metadataFilePath) + expect(ffmpegStub.input.getCall(2).args[0]).to.equal(coverFilePath) + + expect(ffmpegStub.outputOptions.callCount).to.equal(4) + expect(ffmpegStub.outputOptions.getCall(0).args[0]).to.deep.equal(['-map 0:a', '-map_metadata 1', '-map_metadata 0', '-map_chapters 1', '-c copy']) + expect(ffmpegStub.outputOptions.getCall(1).args[0]).to.deep.equal(['-metadata track=1']) + expect(ffmpegStub.outputOptions.getCall(2).args[0]).to.deep.equal(['-f mp4']) + expect(ffmpegStub.outputOptions.getCall(3).args[0]).to.deep.equal(['-map 2:v', '-disposition:v:0 attached_pic', '-metadata:s:v', 'title=Cover', '-metadata:s:v', 'comment=Cover']) + + expect(ffmpegStub.output.calledOnce).to.be.true + expect(ffmpegStub.output.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.m4b') + + expect(ffmpegStub.run.calledOnce).to.be.true + + expect(fsCopyFileSyncStub.calledOnce).to.be.true + expect(fsCopyFileSyncStub.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.m4b') + expect(fsCopyFileSyncStub.firstCall.args[1]).to.equal('/path/to/audio/file.m4b') + + expect(fsUnlinkSyncStub.calledOnce).to.be.true + expect(fsUnlinkSyncStub.firstCall.args[0]).to.equal('/path/to/audio/file.tmp.m4b') + + // Restore the stub + sinon.restore() + }) +}) From e6b1acfb449012fea2af57576332910154778a14 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 6 Jul 2024 16:00:48 -0500 Subject: [PATCH 7/7] Remove tone scripts & references, rename tone-object endpoint, remove node-tone dependency, remove TONE_PATH env --- .devcontainer/Dockerfile | 3 - Dockerfile | 2 - build/debian/DEBIAN/preinst | 16 +- client/pages/audiobook/_id/manage.vue | 24 ++- package-lock.json | 6 - package.json | 1 - readme.md | 2 +- server/controllers/LibraryItemController.js | 6 +- server/managers/AudioMetadataManager.js | 2 +- server/objects/metadata/AudioMetaTags.js | 26 +-- server/routers/ApiRouter.js | 2 +- server/scanner/MediaProbeData.js | 12 +- server/utils/toneHelpers.js | 113 ------------- server/utils/toneProber.js | 173 -------------------- 14 files changed, 34 insertions(+), 354 deletions(-) delete mode 100644 server/utils/toneHelpers.js delete mode 100644 server/utils/toneProber.js diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ddd7c5f3..16ae302c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -10,6 +10,3 @@ RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ curl tzdata ffmpeg && \ rm -rf /var/lib/apt/lists/* - -# Move tone executable to appropriate directory -COPY --from=sandreas/tone:v0.1.5 /usr/local/bin/tone /usr/local/bin/ diff --git a/Dockerfile b/Dockerfile index 97bb4732..fe68b304 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,6 @@ RUN npm ci && npm cache clean --force RUN npm run generate ### STAGE 1: Build server ### -FROM sandreas/tone:v0.1.5 AS tone FROM node:20-alpine ENV NODE_ENV=production @@ -21,7 +20,6 @@ RUN apk update && \ g++ \ tini -COPY --from=tone /usr/local/bin/tone /usr/local/bin/ COPY --from=build /client/dist /client/dist COPY index.js package* / COPY server server diff --git a/build/debian/DEBIAN/preinst b/build/debian/DEBIAN/preinst index f43f2683..c4692ed3 100644 --- a/build/debian/DEBIAN/preinst +++ b/build/debian/DEBIAN/preinst @@ -50,7 +50,6 @@ install_ffmpeg() { echo "Starting FFMPEG Install" WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz" - WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.5/tone-0.1.5-linux-x64.tar.gz --output-document=tone-0.1.5-linux-x64.tar.gz" if ! cd "$FFMPEG_INSTALL_DIR"; then echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR" @@ -63,13 +62,7 @@ install_ffmpeg() { tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1 --no-same-owner rm ffmpeg-git-amd64-static.tar.xz - # Temp downloading tone library to the ffmpeg dir - echo "Getting tone.." - $WGET_TONE - tar xvf tone-0.1.5-linux-x64.tar.gz --strip-components=1 --no-same-owner - rm tone-0.1.5-linux-x64.tar.gz - - echo "Good to go on Ffmpeg (& tone)... hopefully" + echo "Good to go on Ffmpeg... hopefully" } setup_config() { @@ -77,12 +70,6 @@ setup_config() { echo "Existing config found." cat $CONFIG_PATH - # TONE_PATH variable added in 2.1.6, if it doesnt exist then add it - if ! grep -q "TONE_PATH" "$CONFIG_PATH"; then - echo "Adding TONE_PATH to existing config" - echo "TONE_PATH=$FFMPEG_INSTALL_DIR/tone" >> "$CONFIG_PATH" - fi - else if [ ! -d "$DEFAULT_DATA_DIR" ]; then @@ -98,7 +85,6 @@ setup_config() { CONFIG_PATH=$DEFAULT_DATA_DIR/config FFMPEG_PATH=$FFMPEG_INSTALL_DIR/ffmpeg FFPROBE_PATH=$FFMPEG_INSTALL_DIR/ffprobe -TONE_PATH=$FFMPEG_INSTALL_DIR/tone PORT=$DEFAULT_PORT HOST=$DEFAULT_HOST" diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 03c214b4..8863fd5b 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -11,10 +11,9 @@
-
+
-

{{ $strings.HeaderMetadataToEmbed }}

-

audiobookshelf uses tone to write metadata.

+

{{ $strings.HeaderMetadataToEmbed }}

@@ -26,7 +25,7 @@
{{ $strings.LabelValue }}
-