From c1938f78c24feff8d81cf7bb4b7c17a714d8ad79 Mon Sep 17 00:00:00 2001 From: Keagan Hilliard Date: Wed, 2 Nov 2022 19:40:50 -0600 Subject: [PATCH 1/5] Added json file support --- server/managers/AbMergeManager.js | 22 +++++---- server/managers/AudioMetadataManager.js | 25 ++++------ server/utils/toneHelpers.js | 65 +++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 25 deletions(-) diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index 1b3801c0..25e571e2 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -120,20 +120,22 @@ class AbMergeManager { } } - var chaptersFilePath = null - if (libraryItem.media.chapters.length) { - chaptersFilePath = Path.join(task.data.itemCachePath, 'chapters.txt') - try { - await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath) - } catch (error) { - Logger.error(`[AbMergeManager] Write chapters.txt failed`, error) - chaptersFilePath = null - } + var toneJsonPath = null + try { + toneJsonPath = Path.join(itemCacheDir, 'metadata.json') + await toneHelpers.writeToneMetadataJsonFile(libraryItem, toneJsonPath) + } catch (error) { + Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error) + toneJsonPath = null } const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath) toneMetadataObject.TrackNumber = 1 task.data.toneMetadataObject = toneMetadataObject + task.data.toneJsonObject = { + 'ToneJsonFile': toneJsonPath, + 'TrackNumber': 1, + } Logger.debug(`[AbMergeManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject) @@ -190,7 +192,7 @@ class AbMergeManager { } // Write metadata to merged file - const success = await toneHelpers.tagAudioFile(task.data.tempFilepath, task.data.toneMetadataObject) + const success = await toneHelpers.tagAudioFile(task.data.tempFilepath, task.data.toneJsonObject) if (!success) { Logger.error(`[AbMergeManager] Failed to write metadata to file "${task.data.tempFilepath}"`) task.setFailed('Failed to write metadata to m4b file') diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 02a44ce1..bcf51aed 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -43,23 +43,18 @@ class AudioMetadataMangaer { this.emitter('audio_metadata_started', itemAudioMetadataPayload) // Write chapters file - var chaptersFilePath = null + var toneJsonPath = null const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${libraryItem.id}`) await fs.ensureDir(itemCacheDir) - if (libraryItem.media.chapters.length) { - chaptersFilePath = Path.join(itemCacheDir, 'chapters.txt') - try { - await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath) - } catch (error) { - Logger.error(`[AudioMetadataManager] Write chapters.txt failed`, error) - chaptersFilePath = null - } + try { + toneJsonPath = Path.join(itemCacheDir, 'metadata.json') + await toneHelpers.writeToneMetadataJsonFile(libraryItem, toneJsonPath) + } catch (error) { + Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error) + toneJsonPath = null } - const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath) - Logger.debug(`[AudioMetadataManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject) - const results = [] for (const af of audioFiles) { const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneMetadataObject, itemCacheDir) @@ -74,7 +69,7 @@ class AudioMetadataMangaer { this.emitter('audio_metadata_finished', itemAudioMetadataPayload) } - async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneMetadataObject, itemCacheDir) { + async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneJsonPath, itemCacheDir) { const resultPayload = { libraryItemId, index: audioFile.index, @@ -93,8 +88,8 @@ class AudioMetadataMangaer { } const _toneMetadataObject = { - ...toneMetadataObject, - 'TrackNumber': audioFile.index + 'ToneJsonFile': toneJsonPath, + 'TrackNumber': audioFile.index, } resultPayload.success = await toneHelpers.tagAudioFile(audioFile.metadata.path, _toneMetadataObject) diff --git a/server/utils/toneHelpers.js b/server/utils/toneHelpers.js index fc88c169..54add6bd 100644 --- a/server/utils/toneHelpers.js +++ b/server/utils/toneHelpers.js @@ -72,6 +72,71 @@ module.exports.getToneMetadataObject = (libraryItem, chaptersFile) => { return metadataObject } +module.exports.writeToneMetadataJsonFile = (libraryItem, filePath) => { + const bookMetadata = libraryItem.media.metadata + const coverPath = libraryItem.media.coverPath + const chapters = libraryItem.media.chapters + + const metadataObject = { + 'album': bookMetadata.title || '', + 'title': bookMetadata.title || '', + 'trackTotal': libraryItem.media.tracks.length, + 'additionalFields': {} + } + if (bookMetadata.subtitle) { + metadataObject['subtitle'] = bookMetadata.subtitle + } + if (bookMetadata.authorName) { + metadataObject['artist'] = bookMetadata.authorName + metadataObject['albumArtist'] = bookMetadata.authorName + } + if (bookMetadata.description) { + metadataObject['comment'] = bookMetadata.description + metadataObject['description'] = bookMetadata.description + } + if (bookMetadata.narratorName) { + metadataObject['narrator'] = bookMetadata.narratorName + metadataObject['composer'] = bookMetadata.narratorName + } + if (bookMetadata.firstSeriesName) { + metadataObject['movementName'] = bookMetadata.firstSeriesName + } + if (bookMetadata.firstSeriesSequence) { + metadataObject['movement'] = bookMetadata.firstSeriesSequence + } + if (bookMetadata.genres.length) { + metadataObject['genre'] = bookMetadata.genres.join('/') + } + if (bookMetadata.publisher) { + metadataObject['publisher'] = bookMetadata.publisher + } + if (bookMetadata.asin) { + metadataObject.additionalFields['asin'] = bookMetadata.asin + } + if (bookMetadata.isbn) { + metadataObject.additionalFields['isbn'] = bookMetadata.isbn + } + if (coverPath) { + metadataObject['coverFile'] = coverPath + } + if (parsePublishedYear(bookMetadata.publishedYear)) { + metadataObject['publishingDate'] = parsePublishedYear(bookMetadata.publishedYear) + } + if (chapters && chapters.length > 0) { + let metadataChapters = [] + for (const chapter of chapters) { + metadataChapters.push({ + start: chapter.start, + length: chapter.end - chapter.start, + title: chapter.title, + }) + } + metadataObject['chapters'] = chaptersFile + } + + return fs.writeFile(filePath, JSON.stringify({ metadata: metadataObject })) +} + module.exports.tagAudioFile = (filePath, payload) => { return tone.tag(filePath, payload).then((data) => { return true From d57effe97cb6dbee7ad7a28a4688bc85aa4e5554 Mon Sep 17 00:00:00 2001 From: Keagan Hilliard Date: Thu, 3 Nov 2022 09:32:50 -0600 Subject: [PATCH 2/5] Fixed a couple of issues, should be working well now --- server/managers/AudioMetadataManager.js | 2 +- server/utils/toneHelpers.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index bcf51aed..88bb83e9 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -57,7 +57,7 @@ class AudioMetadataMangaer { const results = [] for (const af of audioFiles) { - const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneMetadataObject, itemCacheDir) + const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneJsonPath, itemCacheDir) results.push(result) } diff --git a/server/utils/toneHelpers.js b/server/utils/toneHelpers.js index 54add6bd..ff57bf1e 100644 --- a/server/utils/toneHelpers.js +++ b/server/utils/toneHelpers.js @@ -126,19 +126,20 @@ module.exports.writeToneMetadataJsonFile = (libraryItem, filePath) => { let metadataChapters = [] for (const chapter of chapters) { metadataChapters.push({ - start: chapter.start, - length: chapter.end - chapter.start, + start: Math.round(chapter.start * 1000), + length: Math.round((chapter.end - chapter.start) * 1000), title: chapter.title, }) } - metadataObject['chapters'] = chaptersFile + metadataObject['chapters'] = metadataChapters } - return fs.writeFile(filePath, JSON.stringify({ metadata: metadataObject })) + return fs.writeFile(filePath, JSON.stringify({ meta: metadataObject })) } module.exports.tagAudioFile = (filePath, payload) => { return tone.tag(filePath, payload).then((data) => { + Logger.info('Tone results: ', data) return true }).catch((error) => { Logger.error(`[toneHelpers] tagAudioFile: Failed for "${filePath}"`, error) From 586c8a550a9b53487aabd13fa47d44626cf1da09 Mon Sep 17 00:00:00 2001 From: Keagan Hilliard Date: Thu, 3 Nov 2022 10:09:49 -0600 Subject: [PATCH 3/5] Removed a noisy log and limit chapter embedding to items with only 1 audiofile --- server/managers/AudioMetadataManager.js | 2 +- server/utils/toneHelpers.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 88bb83e9..c76e3fe2 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -49,7 +49,7 @@ class AudioMetadataMangaer { try { toneJsonPath = Path.join(itemCacheDir, 'metadata.json') - await toneHelpers.writeToneMetadataJsonFile(libraryItem, toneJsonPath) + await toneHelpers.writeToneMetadataJsonFile(libraryItem, audioFiles.length == 1 && libraryItem.media.chapters, toneJsonPath) } catch (error) { Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error) toneJsonPath = null diff --git a/server/utils/toneHelpers.js b/server/utils/toneHelpers.js index ff57bf1e..fcdd1440 100644 --- a/server/utils/toneHelpers.js +++ b/server/utils/toneHelpers.js @@ -72,10 +72,9 @@ module.exports.getToneMetadataObject = (libraryItem, chaptersFile) => { return metadataObject } -module.exports.writeToneMetadataJsonFile = (libraryItem, filePath) => { +module.exports.writeToneMetadataJsonFile = (libraryItem, chapters, filePath) => { const bookMetadata = libraryItem.media.metadata const coverPath = libraryItem.media.coverPath - const chapters = libraryItem.media.chapters const metadataObject = { 'album': bookMetadata.title || '', @@ -139,7 +138,6 @@ module.exports.writeToneMetadataJsonFile = (libraryItem, filePath) => { module.exports.tagAudioFile = (filePath, payload) => { return tone.tag(filePath, payload).then((data) => { - Logger.info('Tone results: ', data) return true }).catch((error) => { Logger.error(`[toneHelpers] tagAudioFile: Failed for "${filePath}"`, error) From 3824154c15fa6f86d121ef8db5da77c2c3154079 Mon Sep 17 00:00:00 2001 From: Keagan Hilliard Date: Thu, 3 Nov 2022 10:20:32 -0600 Subject: [PATCH 4/5] Forgot to update the merge --- server/managers/AbMergeManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index 25e571e2..0bc89be3 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -123,7 +123,7 @@ class AbMergeManager { var toneJsonPath = null try { toneJsonPath = Path.join(itemCacheDir, 'metadata.json') - await toneHelpers.writeToneMetadataJsonFile(libraryItem, toneJsonPath) + await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath) } catch (error) { Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error) toneJsonPath = null From 1118b8b78200704d2c16c5c36f7fccc70e5d0f4f Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 5 Nov 2022 13:13:52 -0500 Subject: [PATCH 5/5] Metadata embed and m4b merge fixes and cleanup --- client/pages/audiobook/_id/manage.vue | 4 ++++ server/controllers/LibraryItemController.js | 3 ++- server/managers/AbMergeManager.js | 13 ++++--------- server/managers/AudioMetadataManager.js | 9 +++++---- server/utils/toneHelpers.js | 6 +++--- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 9f858a81..a45979dc 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -91,6 +91,10 @@ A backup of your original audio files will be stored in /metadata/cache/items/{{ libraryItemId }}/. Make sure to periodically purge items cache.

+
+ star +

Chapters are not embedded in multi-track audiobooks.

+
star

Encoding can take up to 30 minutes.

diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index b1f3f77d..56b181c2 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -398,7 +398,8 @@ class LibraryItemController { } const useTone = req.query.tone === '1' - this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, useTone) + const forceEmbedChapters = req.query.forceEmbedChapters === '1' + this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, useTone, forceEmbedChapters) res.sendStatus(200) } diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index 0bc89be3..72343d50 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -62,7 +62,7 @@ class AbMergeManager { targetFilename, targetFilepath: Path.join(libraryItem.path, targetFilename), itemCachePath, - toneMetadataObject: null + toneJsonObject: null } const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.` task.setData('encode-m4b', 'Encoding M4b', taskDescription, taskData) @@ -122,23 +122,18 @@ class AbMergeManager { var toneJsonPath = null try { - toneJsonPath = Path.join(itemCacheDir, 'metadata.json') - await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath) + toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json') + await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath, 1) } catch (error) { - Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error) + Logger.error(`[AbMergeManager] Write metadata.json failed`, error) toneJsonPath = null } - const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath) - toneMetadataObject.TrackNumber = 1 - task.data.toneMetadataObject = toneMetadataObject task.data.toneJsonObject = { 'ToneJsonFile': toneJsonPath, 'TrackNumber': 1, } - Logger.debug(`[AbMergeManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject) - var workerData = { inputs: ffmpegInputs, options: ffmpegOptions, diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index c76e3fe2..25a93583 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -15,9 +15,9 @@ class AudioMetadataMangaer { this.clientEmitter = clientEmitter } - updateMetadataForItem(user, libraryItem, useTone = true) { + updateMetadataForItem(user, libraryItem, useTone, forceEmbedChapters) { if (useTone) { - this.updateMetadataForItemWithTone(user, libraryItem) + this.updateMetadataForItemWithTone(user, libraryItem, forceEmbedChapters) } else { this.updateMetadataForItemWithFfmpeg(user, libraryItem) } @@ -30,7 +30,7 @@ class AudioMetadataMangaer { return toneHelpers.getToneMetadataObject(libraryItem) } - async updateMetadataForItemWithTone(user, libraryItem) { + async updateMetadataForItemWithTone(user, libraryItem, forceEmbedChapters) { var audioFiles = libraryItem.media.includedAudioFiles const itemAudioMetadataPayload = { @@ -49,7 +49,8 @@ class AudioMetadataMangaer { try { toneJsonPath = Path.join(itemCacheDir, 'metadata.json') - await toneHelpers.writeToneMetadataJsonFile(libraryItem, audioFiles.length == 1 && libraryItem.media.chapters, toneJsonPath) + const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters : null + await toneHelpers.writeToneMetadataJsonFile(libraryItem, chapters, toneJsonPath, audioFiles.length) } catch (error) { Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error) toneJsonPath = null diff --git a/server/utils/toneHelpers.js b/server/utils/toneHelpers.js index fcdd1440..dac6fa40 100644 --- a/server/utils/toneHelpers.js +++ b/server/utils/toneHelpers.js @@ -72,14 +72,14 @@ module.exports.getToneMetadataObject = (libraryItem, chaptersFile) => { return metadataObject } -module.exports.writeToneMetadataJsonFile = (libraryItem, chapters, filePath) => { +module.exports.writeToneMetadataJsonFile = (libraryItem, chapters, filePath, trackTotal) => { const bookMetadata = libraryItem.media.metadata const coverPath = libraryItem.media.coverPath const metadataObject = { 'album': bookMetadata.title || '', 'title': bookMetadata.title || '', - 'trackTotal': libraryItem.media.tracks.length, + 'trackTotal': trackTotal, 'additionalFields': {} } if (bookMetadata.subtitle) { @@ -133,7 +133,7 @@ module.exports.writeToneMetadataJsonFile = (libraryItem, chapters, filePath) => metadataObject['chapters'] = metadataChapters } - return fs.writeFile(filePath, JSON.stringify({ meta: metadataObject })) + return fs.writeFile(filePath, JSON.stringify({ meta: metadataObject }, null, 2)) } module.exports.tagAudioFile = (filePath, payload) => {