mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Add:Option to disable backup of audio files in embed metadata tool #1370
This commit is contained in:
parent
7ccf36a896
commit
5a26704c32
@ -63,6 +63,10 @@
|
|||||||
|
|
||||||
<div class="w-full max-w-4xl mx-auto">
|
<div class="w-full max-w-4xl mx-auto">
|
||||||
<div v-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
<div v-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
||||||
|
<ui-checkbox v-if="!isFinished" v-model="shouldBackupAudioFiles" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -104,7 +108,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-start mb-2">
|
<div v-if="shouldBackupAudioFiles || isM4BTool" class="flex items-start mb-2">
|
||||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
<p class="text-gray-200 ml-2">
|
<p class="text-gray-200 ml-2">
|
||||||
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
||||||
@ -171,7 +175,7 @@ export default {
|
|||||||
if (!store.getters['user/getIsAdminOrUp']) {
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
return redirect('/?error=unauthorized')
|
return redirect('/?error=unauthorized')
|
||||||
}
|
}
|
||||||
var libraryItem = await app.$axios.$get(`/api/items/${params.id}?expanded=1`).catch((error) => {
|
const libraryItem = await app.$axios.$get(`/api/items/${params.id}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@ -201,6 +205,7 @@ export default {
|
|||||||
selectedTool: 'embed',
|
selectedTool: 'embed',
|
||||||
isCancelingEncode: false,
|
isCancelingEncode: false,
|
||||||
showEncodeOptions: false,
|
showEncodeOptions: false,
|
||||||
|
shouldBackupAudioFiles: true,
|
||||||
encodingOptions: {
|
encodingOptions: {
|
||||||
bitrate: '64k',
|
bitrate: '64k',
|
||||||
channels: '2',
|
channels: '2',
|
||||||
@ -275,6 +280,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleBackupAudioFiles(val) {
|
||||||
|
localStorage.setItem('embedMetadataShouldBackup', val ? 1 : 0)
|
||||||
|
},
|
||||||
cancelEncodeClick() {
|
cancelEncodeClick() {
|
||||||
this.isCancelingEncode = true
|
this.isCancelingEncode = true
|
||||||
this.$axios
|
this.$axios
|
||||||
@ -332,7 +340,7 @@ export default {
|
|||||||
updateAudioFileMetadata() {
|
updateAudioFileMetadata() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata?tone=1`)
|
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata?backup=${this.shouldBackupAudioFiles ? 1 : 0}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Audio metadata encode started')
|
console.log('Audio metadata encode started')
|
||||||
})
|
})
|
||||||
@ -350,9 +358,14 @@ export default {
|
|||||||
console.log('audio metadata finished', data)
|
console.log('audio metadata finished', data)
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
if (data.libraryItemId !== this.libraryItemId) return
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.isFinished = true
|
|
||||||
this.audiofilesEncoding = {}
|
this.audiofilesEncoding = {}
|
||||||
|
|
||||||
|
if (data.failed) {
|
||||||
|
this.$toast.error(data.error)
|
||||||
|
} else {
|
||||||
|
this.isFinished = true
|
||||||
this.$toast.success('Audio file metadata updated')
|
this.$toast.success('Audio file metadata updated')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
audiofileMetadataStarted(data) {
|
audiofileMetadataStarted(data) {
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
if (data.libraryItemId !== this.libraryItemId) return
|
||||||
@ -378,6 +391,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.task) this.taskUpdated(this.task)
|
if (this.task) this.taskUpdated(this.task)
|
||||||
|
|
||||||
|
const shouldBackupAudioFiles = localStorage.getItem('embedMetadataShouldBackup')
|
||||||
|
this.shouldBackupAudioFiles = shouldBackupAudioFiles != 0
|
||||||
},
|
},
|
||||||
fetchToneObject() {
|
fetchToneObject() {
|
||||||
this.$axios
|
this.$axios
|
||||||
|
@ -47,7 +47,6 @@ class ToolsController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// POST: api/tools/item/:id/embed-metadata
|
// POST: api/tools/item/:id/embed-metadata
|
||||||
async embedAudioFileMetadata(req, res) {
|
async embedAudioFileMetadata(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
@ -60,9 +59,11 @@ class ToolsController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
const useTone = req.query.tone === '1'
|
const options = {
|
||||||
const forceEmbedChapters = req.query.forceEmbedChapters === '1'
|
forceEmbedChapters: req.query.forceEmbedChapters === '1',
|
||||||
this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, useTone, forceEmbedChapters)
|
backup: req.query.backup === '1'
|
||||||
|
}
|
||||||
|
this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, options)
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const workerThreads = require('worker_threads')
|
|
||||||
|
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
const filePerms = require('../utils/filePerms')
|
|
||||||
const { secondsToTimestamp } = require('../utils/index')
|
const { secondsToTimestamp } = require('../utils/index')
|
||||||
const { filePathToPOSIX } = require('../utils/fileUtils')
|
|
||||||
const { writeMetadataFile } = require('../utils/ffmpegHelpers')
|
|
||||||
const toneHelpers = require('../utils/toneHelpers')
|
const toneHelpers = require('../utils/toneHelpers')
|
||||||
|
const filePerms = require('../utils/filePerms')
|
||||||
|
|
||||||
class AudioMetadataMangaer {
|
class AudioMetadataMangaer {
|
||||||
constructor(db, taskManager) {
|
constructor(db, taskManager) {
|
||||||
@ -18,23 +15,15 @@ class AudioMetadataMangaer {
|
|||||||
this.taskManager = taskManager
|
this.taskManager = taskManager
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMetadataForItem(user, libraryItem, useTone, forceEmbedChapters) {
|
|
||||||
if (useTone) {
|
|
||||||
this.updateMetadataForItemWithTone(user, libraryItem, forceEmbedChapters)
|
|
||||||
} else {
|
|
||||||
this.updateMetadataForItemWithFfmpeg(user, libraryItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// TONE
|
|
||||||
//
|
|
||||||
getToneMetadataObjectForApi(libraryItem) {
|
getToneMetadataObjectForApi(libraryItem) {
|
||||||
return toneHelpers.getToneMetadataObject(libraryItem)
|
return toneHelpers.getToneMetadataObject(libraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMetadataForItemWithTone(user, libraryItem, forceEmbedChapters) {
|
async updateMetadataForItem(user, libraryItem, options = {}) {
|
||||||
var audioFiles = libraryItem.media.includedAudioFiles
|
const forceEmbedChapters = !!options.forceEmbedChapters
|
||||||
|
const backupFiles = !!options.backup
|
||||||
|
|
||||||
|
const audioFiles = libraryItem.media.includedAudioFiles
|
||||||
|
|
||||||
const itemAudioMetadataPayload = {
|
const itemAudioMetadataPayload = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -45,35 +34,55 @@ class AudioMetadataMangaer {
|
|||||||
|
|
||||||
SocketAuthority.emitter('audio_metadata_started', itemAudioMetadataPayload)
|
SocketAuthority.emitter('audio_metadata_started', itemAudioMetadataPayload)
|
||||||
|
|
||||||
// Write chapters file
|
// Ensure folder for backup files
|
||||||
var toneJsonPath = null
|
|
||||||
const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${libraryItem.id}`)
|
const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${libraryItem.id}`)
|
||||||
await fs.ensureDir(itemCacheDir)
|
let cacheDirCreated = false
|
||||||
|
if (!await fs.pathExists(itemCacheDir)) {
|
||||||
|
await fs.mkdir(itemCacheDir)
|
||||||
|
await filePerms.setDefault(itemCacheDir, true)
|
||||||
|
cacheDirCreated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write chapters file
|
||||||
|
const toneJsonPath = Path.join(itemCacheDir, 'metadata.json')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toneJsonPath = Path.join(itemCacheDir, 'metadata.json')
|
|
||||||
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters : null
|
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters : null
|
||||||
await toneHelpers.writeToneMetadataJsonFile(libraryItem, chapters, toneJsonPath, audioFiles.length)
|
await toneHelpers.writeToneMetadataJsonFile(libraryItem, chapters, toneJsonPath, audioFiles.length)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error)
|
Logger.error(`[AudioMetadataManager] Write metadata.json failed`, error)
|
||||||
toneJsonPath = null
|
|
||||||
|
itemAudioMetadataPayload.failed = true
|
||||||
|
itemAudioMetadataPayload.error = 'Failed to write metadata.json'
|
||||||
|
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = []
|
const results = []
|
||||||
for (const af of audioFiles) {
|
for (const af of audioFiles) {
|
||||||
const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneJsonPath, itemCacheDir)
|
const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneJsonPath, itemCacheDir, backupFiles)
|
||||||
results.push(result)
|
results.push(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove temp cache file/folder if not backing up
|
||||||
|
if (!backupFiles) {
|
||||||
|
// If cache dir was created from this then remove it
|
||||||
|
if (cacheDirCreated) {
|
||||||
|
await fs.remove(itemCacheDir)
|
||||||
|
} else {
|
||||||
|
await fs.remove(toneJsonPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const elapsed = Date.now() - itemAudioMetadataPayload.startedAt
|
const elapsed = Date.now() - itemAudioMetadataPayload.startedAt
|
||||||
Logger.debug(`[AudioMetadataManager] Elapsed ${secondsToTimestamp(elapsed)}`)
|
Logger.debug(`[AudioMetadataManager] Elapsed ${secondsToTimestamp(elapsed / 1000, true)}`)
|
||||||
itemAudioMetadataPayload.results = results
|
itemAudioMetadataPayload.results = results
|
||||||
itemAudioMetadataPayload.elapsed = elapsed
|
itemAudioMetadataPayload.elapsed = elapsed
|
||||||
itemAudioMetadataPayload.finishedAt = Date.now()
|
itemAudioMetadataPayload.finishedAt = Date.now()
|
||||||
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneJsonPath, itemCacheDir) {
|
async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneJsonPath, itemCacheDir, backupFiles) {
|
||||||
const resultPayload = {
|
const resultPayload = {
|
||||||
libraryItemId,
|
libraryItemId,
|
||||||
index: audioFile.index,
|
index: audioFile.index,
|
||||||
@ -83,6 +92,7 @@ class AudioMetadataMangaer {
|
|||||||
SocketAuthority.emitter('audiofile_metadata_started', resultPayload)
|
SocketAuthority.emitter('audiofile_metadata_started', resultPayload)
|
||||||
|
|
||||||
// Backup audio file
|
// Backup audio file
|
||||||
|
if (backupFiles) {
|
||||||
try {
|
try {
|
||||||
const backupFilePath = Path.join(itemCacheDir, audioFile.metadata.filename)
|
const backupFilePath = Path.join(itemCacheDir, audioFile.metadata.filename)
|
||||||
await fs.copy(audioFile.metadata.path, backupFilePath)
|
await fs.copy(audioFile.metadata.path, backupFilePath)
|
||||||
@ -90,6 +100,7 @@ class AudioMetadataMangaer {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(`[AudioMetadataManager] Failed to backup audio file "${audioFile.metadata.path}"`, err)
|
Logger.error(`[AudioMetadataManager] Failed to backup audio file "${audioFile.metadata.path}"`, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const _toneMetadataObject = {
|
const _toneMetadataObject = {
|
||||||
'ToneJsonFile': toneJsonPath,
|
'ToneJsonFile': toneJsonPath,
|
||||||
@ -104,161 +115,5 @@ class AudioMetadataMangaer {
|
|||||||
SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
|
SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
|
||||||
return resultPayload
|
return resultPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// FFMPEG
|
|
||||||
//
|
|
||||||
async updateMetadataForItemWithFfmpeg(user, libraryItem) {
|
|
||||||
var audioFiles = libraryItem.media.audioFiles
|
|
||||||
|
|
||||||
const itemAudioMetadataPayload = {
|
|
||||||
userId: user.id,
|
|
||||||
libraryItemId: libraryItem.id,
|
|
||||||
startedAt: Date.now(),
|
|
||||||
audioFiles: audioFiles.map(af => ({ index: af.index, ino: af.ino, filename: af.metadata.filename }))
|
|
||||||
}
|
|
||||||
|
|
||||||
SocketAuthority.emitter('audio_metadata_started', itemAudioMetadataPayload)
|
|
||||||
|
|
||||||
var downloadsPath = Path.join(global.MetadataPath, 'downloads')
|
|
||||||
var outputDir = Path.join(downloadsPath, libraryItem.id)
|
|
||||||
await fs.ensureDir(outputDir)
|
|
||||||
|
|
||||||
var metadataFilePath = Path.join(outputDir, 'metadata.txt')
|
|
||||||
await writeMetadataFile(libraryItem, metadataFilePath)
|
|
||||||
|
|
||||||
if (libraryItem.media.coverPath != null) {
|
|
||||||
var coverPath = filePathToPOSIX(libraryItem.media.coverPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const proms = audioFiles.map(af => {
|
|
||||||
return this.updateAudioFileMetadataWithFfmpeg(libraryItem.id, af, outputDir, metadataFilePath, coverPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
const results = await Promise.all(proms)
|
|
||||||
|
|
||||||
Logger.debug(`[AudioMetadataManager] Finished`)
|
|
||||||
|
|
||||||
await fs.remove(outputDir)
|
|
||||||
|
|
||||||
const elapsed = Date.now() - itemAudioMetadataPayload.startedAt
|
|
||||||
Logger.debug(`[AudioMetadataManager] Elapsed ${secondsToTimestamp(elapsed)}`)
|
|
||||||
itemAudioMetadataPayload.results = results
|
|
||||||
itemAudioMetadataPayload.elapsed = elapsed
|
|
||||||
itemAudioMetadataPayload.finishedAt = Date.now()
|
|
||||||
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAudioFileMetadataWithFfmpeg(libraryItemId, audioFile, outputDir, metadataFilePath, coverPath = '') {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const resultPayload = {
|
|
||||||
libraryItemId,
|
|
||||||
index: audioFile.index,
|
|
||||||
ino: audioFile.ino,
|
|
||||||
filename: audioFile.metadata.filename
|
|
||||||
}
|
|
||||||
SocketAuthority.emitter('audiofile_metadata_started', resultPayload)
|
|
||||||
|
|
||||||
Logger.debug(`[AudioFileMetadataManager] Starting audio file metadata encode for "${audioFile.metadata.filename}"`)
|
|
||||||
|
|
||||||
var outputPath = Path.join(outputDir, audioFile.metadata.filename)
|
|
||||||
var inputPath = audioFile.metadata.path
|
|
||||||
const isM4b = audioFile.metadata.format === 'm4b'
|
|
||||||
const ffmpegInputs = [
|
|
||||||
{
|
|
||||||
input: inputPath,
|
|
||||||
options: isM4b ? ['-f mp4'] : []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: metadataFilePath
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
/*
|
|
||||||
Mp4 doesnt support writing custom tags by default. Supported tags are itunes tags: https://git.videolan.org/?p=ffmpeg.git;a=blob;f=libavformat/movenc.c;h=b6821d447c92183101086cb67099b2f4804293de;hb=HEAD#l2905
|
|
||||||
|
|
||||||
Workaround -movflags use_metadata_tags found here: https://superuser.com/a/1208277
|
|
||||||
|
|
||||||
Ffmpeg premapped id3 tags: https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
|
|
||||||
*/
|
|
||||||
|
|
||||||
const ffmpegOptions = ['-c copy', '-map_chapters 1', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']
|
|
||||||
|
|
||||||
if (coverPath != '') {
|
|
||||||
var ffmpegCoverPathInput = {
|
|
||||||
input: coverPath,
|
|
||||||
options: ['-f image2pipe']
|
|
||||||
}
|
|
||||||
var ffmpegCoverPathOptions = [
|
|
||||||
'-c:v copy',
|
|
||||||
'-map 2:v',
|
|
||||||
'-map 0:a'
|
|
||||||
]
|
|
||||||
|
|
||||||
ffmpegInputs.push(ffmpegCoverPathInput)
|
|
||||||
Logger.debug(`[AudioFileMetaDataManager] Cover found for "${audioFile.metadata.filename}". Cover will be merged to metadata`)
|
|
||||||
} else {
|
|
||||||
// remove the video stream to account for the user getting rid an existing cover in abs
|
|
||||||
var ffmpegCoverPathOptions = [
|
|
||||||
'-map 0',
|
|
||||||
'-map -0:v'
|
|
||||||
]
|
|
||||||
|
|
||||||
Logger.debug(`[AudioFileMetaDataManager] No cover found for "${audioFile.metadata.filename}". Cover will be skipped or removed from metadata`)
|
|
||||||
}
|
|
||||||
|
|
||||||
ffmpegOptions.push(...ffmpegCoverPathOptions)
|
|
||||||
|
|
||||||
var workerData = {
|
|
||||||
inputs: ffmpegInputs,
|
|
||||||
options: ffmpegOptions,
|
|
||||||
outputOptions: isM4b ? ['-f mp4'] : [],
|
|
||||||
output: outputPath,
|
|
||||||
}
|
|
||||||
var workerPath = Path.join(global.appRoot, 'server/utils/downloadWorker.js')
|
|
||||||
var worker = new workerThreads.Worker(workerPath, { workerData })
|
|
||||||
|
|
||||||
worker.on('message', async (message) => {
|
|
||||||
if (message != null && typeof message === 'object') {
|
|
||||||
if (message.type === 'RESULT') {
|
|
||||||
Logger.debug(message)
|
|
||||||
|
|
||||||
if (message.success) {
|
|
||||||
Logger.debug(`[AudioFileMetadataManager] Metadata encode SUCCESS for "${audioFile.metadata.filename}"`)
|
|
||||||
|
|
||||||
await filePerms.setDefault(outputPath, true)
|
|
||||||
|
|
||||||
fs.move(outputPath, inputPath, { overwrite: true }).then(() => {
|
|
||||||
Logger.debug(`[AudioFileMetadataManager] Audio file replaced successfully "${inputPath}"`)
|
|
||||||
|
|
||||||
resultPayload.success = true
|
|
||||||
SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
|
|
||||||
resolve(resultPayload)
|
|
||||||
}).catch((error) => {
|
|
||||||
Logger.error(`[AudioFileMetadataManager] Audio file failed to move "${inputPath}"`, error)
|
|
||||||
resultPayload.success = false
|
|
||||||
SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
|
|
||||||
resolve(resultPayload)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Logger.debug(`[AudioFileMetadataManager] Metadata encode FAILED for "${audioFile.metadata.filename}"`)
|
|
||||||
|
|
||||||
resultPayload.success = false
|
|
||||||
SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
|
|
||||||
resolve(resultPayload)
|
|
||||||
}
|
|
||||||
} else if (message.type === 'FFMPEG') {
|
|
||||||
if (message.level === 'debug' && process.env.NODE_ENV === 'production') {
|
|
||||||
// stderr is not necessary in production
|
|
||||||
} else if (Logger[message.level]) {
|
|
||||||
Logger[message.level](message.log)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logger.error('Invalid worker message', message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = AudioMetadataMangaer
|
module.exports = AudioMetadataMangaer
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
const Ffmpeg = require('../libs/fluentFfmpeg')
|
const Ffmpeg = require('../libs/fluentFfmpeg')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const package = require('../../package.json')
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { filePathToPOSIX } = require('./fileUtils')
|
const { filePathToPOSIX } = require('./fileUtils')
|
||||||
|
|
||||||
@ -41,59 +40,6 @@ async function writeConcatFile(tracks, outputPath, startTime = 0) {
|
|||||||
}
|
}
|
||||||
module.exports.writeConcatFile = writeConcatFile
|
module.exports.writeConcatFile = writeConcatFile
|
||||||
|
|
||||||
|
|
||||||
async function writeMetadataFile(libraryItem, outputPath) {
|
|
||||||
var inputstrs = [
|
|
||||||
';FFMETADATA1',
|
|
||||||
`title=${libraryItem.media.metadata.title}`,
|
|
||||||
`artist=${libraryItem.media.metadata.authorName}`,
|
|
||||||
`album_artist=${libraryItem.media.metadata.authorName}`,
|
|
||||||
`date=${libraryItem.media.metadata.publishedYear || ''}`,
|
|
||||||
`description=${libraryItem.media.metadata.description || ''}`,
|
|
||||||
`genre=${libraryItem.media.metadata.genres.join(';')}`,
|
|
||||||
`performer=${libraryItem.media.metadata.narratorName || ''}`,
|
|
||||||
`encoded_by=audiobookshelf:${package.version}`
|
|
||||||
]
|
|
||||||
|
|
||||||
if (libraryItem.media.metadata.asin) {
|
|
||||||
inputstrs.push(`ASIN=${libraryItem.media.metadata.asin}`)
|
|
||||||
}
|
|
||||||
if (libraryItem.media.metadata.isbn) {
|
|
||||||
inputstrs.push(`ISBN=${libraryItem.media.metadata.isbn}`)
|
|
||||||
}
|
|
||||||
if (libraryItem.media.metadata.language) {
|
|
||||||
inputstrs.push(`language=${libraryItem.media.metadata.language}`)
|
|
||||||
}
|
|
||||||
if (libraryItem.media.metadata.series.length) {
|
|
||||||
// Only uses first series
|
|
||||||
var firstSeries = libraryItem.media.metadata.series[0]
|
|
||||||
inputstrs.push(`series=${firstSeries.name}`)
|
|
||||||
if (firstSeries.sequence) {
|
|
||||||
inputstrs.push(`series-part=${firstSeries.sequence}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (libraryItem.media.metadata.subtitle) {
|
|
||||||
inputstrs.push(`subtitle=${libraryItem.media.metadata.subtitle}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (libraryItem.media.chapters) {
|
|
||||||
libraryItem.media.chapters.forEach((chap) => {
|
|
||||||
const chapterstrs = [
|
|
||||||
'[CHAPTER]',
|
|
||||||
'TIMEBASE=1/1000',
|
|
||||||
`START=${Math.round(chap.start * 1000)}`,
|
|
||||||
`END=${Math.round(chap.end * 1000)}`,
|
|
||||||
`title=${chap.title}`
|
|
||||||
]
|
|
||||||
inputstrs = inputstrs.concat(chapterstrs)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(outputPath, inputstrs.join('\n'))
|
|
||||||
return inputstrs
|
|
||||||
}
|
|
||||||
module.exports.writeMetadataFile = writeMetadataFile
|
|
||||||
|
|
||||||
async function extractCoverArt(filepath, outputpath) {
|
async function extractCoverArt(filepath, outputpath) {
|
||||||
var dirname = Path.dirname(outputpath)
|
var dirname = Path.dirname(outputpath)
|
||||||
await fs.ensureDir(dirname)
|
await fs.ensureDir(dirname)
|
||||||
|
@ -100,8 +100,6 @@ function secondsToTimestamp(seconds, includeMs = false, alwaysIncludeHours = fal
|
|||||||
}
|
}
|
||||||
module.exports.secondsToTimestamp = secondsToTimestamp
|
module.exports.secondsToTimestamp = secondsToTimestamp
|
||||||
|
|
||||||
module.exports.msToTimestamp = (ms, includeMs) => secondsToTimestamp(ms / 1000, includeMs)
|
|
||||||
|
|
||||||
module.exports.reqSupportsWebp = (req) => {
|
module.exports.reqSupportsWebp = (req) => {
|
||||||
if (!req || !req.headers || !req.headers.accept) return false
|
if (!req || !req.headers || !req.headers.accept) return false
|
||||||
return req.headers.accept.includes('image/webp') || req.headers.accept === '*/*'
|
return req.headers.accept.includes('image/webp') || req.headers.accept === '*/*'
|
||||||
|
Loading…
Reference in New Issue
Block a user