mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-31 12:15:49 -04:00
Update:Audiobook merge to set metadata with tone and replace m4b in library item #594
This commit is contained in:
parent
b7bdaac163
commit
f36a5eae6d
@ -16,8 +16,8 @@
|
|||||||
<ui-btn v-if="abmergeStatus !== $constants.DownloadStatus.READY" :loading="abmergeStatus === $constants.DownloadStatus.PENDING" :disabled="tempDisable" @click="startAudiobookMerge">Start Merge</ui-btn>
|
<ui-btn v-if="abmergeStatus !== $constants.DownloadStatus.READY" :loading="abmergeStatus === $constants.DownloadStatus.PENDING" :disabled="tempDisable" @click="startAudiobookMerge">Start Merge</ui-btn>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ui-btn @click="downloadWithProgress(abmergeDownload)">Download</ui-btn>
|
<!-- <ui-btn @click="downloadWithProgress(abmergeDownload)">Download</ui-btn>
|
||||||
<ui-icon-btn small icon="delete" bg-color="error" class="ml-2" @click="removeDownload" />
|
<ui-icon-btn small icon="delete" bg-color="error" class="ml-2" @click="removeDownload" /> -->
|
||||||
</div>
|
</div>
|
||||||
<p class="px-0.5 py-1 text-sm font-mono text-center">Size: {{ $bytesPretty(abmergeDownload.size) }}</p>
|
<p class="px-0.5 py-1 text-sm font-mono text-center">Size: {{ $bytesPretty(abmergeDownload.size) }}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -34,18 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div>
|
<div>
|
||||||
<p v-if="abmergeStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p>
|
<ui-btn :loading="abmergeStatus === $constants.DownloadStatus.PENDING" :disabled="true" @click="startAudiobookMerge">Not yet implemented</ui-btn>
|
||||||
<p v-if="abmergeStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p>
|
|
||||||
<p v-if="abmergeStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p>
|
|
||||||
|
|
||||||
<ui-btn v-if="abmergeStatus !== $constants.DownloadStatus.READY" :loading="abmergeStatus === $constants.DownloadStatus.PENDING" :disabled="true" @click="startAudiobookMerge">Not yet implemented</ui-btn>
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex">
|
|
||||||
<ui-btn @click="downloadWithProgress(abmergeDownload)">Download</ui-btn>
|
|
||||||
<ui-icon-btn small icon="delete" bg-color="error" class="ml-2" @click="removeDownload" />
|
|
||||||
</div>
|
|
||||||
<p class="px-0.5 py-1 text-sm font-mono text-center">Size: {{ $bytesPretty(abmergeDownload.size) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -164,7 +153,7 @@ export default {
|
|||||||
|
|
||||||
this.tempDisable = true
|
this.tempDisable = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/download/${downloadId}`)
|
.$delete(`/api/ab-manager-tasks/${downloadId}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.tempDisable = false
|
this.tempDisable = false
|
||||||
this.$toast.success('Merge download deleted')
|
this.$toast.success('Merge download deleted')
|
||||||
@ -192,7 +181,7 @@ export default {
|
|||||||
},
|
},
|
||||||
downloadWithProgress(download) {
|
downloadWithProgress(download) {
|
||||||
var downloadId = download.id
|
var downloadId = download.id
|
||||||
var downloadUrl = `${process.env.serverUrl}/api/download/${downloadId}`
|
var downloadUrl = `${process.env.serverUrl}/api/ab-manager-tasks/${downloadId}`
|
||||||
var filename = download.filename
|
var filename = download.filename
|
||||||
|
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
@ -242,18 +231,18 @@ export default {
|
|||||||
},
|
},
|
||||||
loadDownloads() {
|
loadDownloads() {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/downloads`)
|
.$get(`/api/ab-manager-tasks`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
var pendingDownloads = data.pendingDownloads.map((pd) => {
|
var pendingTasks = data.pendingTasks.map((pd) => {
|
||||||
pd.download.status = this.$constants.DownloadStatus.PENDING
|
pd.download.status = this.$constants.DownloadStatus.PENDING
|
||||||
return pd.download
|
return pd.download
|
||||||
})
|
})
|
||||||
var downloads = data.downloads.map((d) => {
|
var tasks = data.tasks.map((d) => {
|
||||||
d.status = this.$constants.DownloadStatus.READY
|
d.status = this.$constants.DownloadStatus.READY
|
||||||
return d
|
return d
|
||||||
})
|
})
|
||||||
var allDownloads = downloads.concat(pendingDownloads)
|
var allTasks = tasks.concat(pendingTasks)
|
||||||
this.$store.commit('downloads/setDownloads', allDownloads)
|
this.$store.commit('downloads/setDownloads', allTasks)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to load downloads', error)
|
console.error('Failed to load downloads', error)
|
||||||
|
@ -110,53 +110,53 @@ class MiscController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/download/:id
|
// GET: api/ab-manager-tasks/:id
|
||||||
async getDownload(req, res) {
|
async getAbManagerTask(req, res) {
|
||||||
if (!req.user.canDownload) {
|
if (!req.user.canDownload) {
|
||||||
Logger.error('User attempting to download without permission', req.user)
|
Logger.error('User attempting to download without permission', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var downloadId = req.params.id
|
var taskId = req.params.id
|
||||||
Logger.info('Download Request', downloadId)
|
Logger.info('Download Request', taskId)
|
||||||
var download = this.abMergeManager.getDownload(downloadId)
|
var task = this.abMergeManager.getTask(taskId)
|
||||||
if (!download) {
|
if (!task) {
|
||||||
Logger.error('Download request not found', downloadId)
|
Logger.error('Ab manager task request not found', taskId)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': download.mimeType
|
'Content-Type': task.mimeType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.download(download.path, download.filename, options, (err) => {
|
res.download(task.path, task.filename, options, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
Logger.error('Download Error', err)
|
Logger.error('Download Error', err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE: api/download/:id
|
// DELETE: api/ab-manager-tasks/:id
|
||||||
async removeDownload(req, res) {
|
async removeAbManagerTask(req, res) {
|
||||||
if (!req.user.canDownload || !req.user.canDelete) {
|
if (!req.user.canDownload || !req.user.canDelete) {
|
||||||
Logger.error('User attempting to remove download without permission', req.user.username)
|
Logger.error('User attempting to remove ab manager task without permission', req.user.username)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
this.abMergeManager.removeDownloadById(req.params.id)
|
this.abMergeManager.removeTaskById(req.params.id)
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/downloads
|
// GET: api/ab-manager-tasks
|
||||||
async getDownloads(req, res) {
|
async getAbManagerTasks(req, res) {
|
||||||
if (!req.user.canDownload) {
|
if (!req.user.canDownload) {
|
||||||
Logger.error('User attempting to get downloads without permission', req.user.username)
|
Logger.error('User attempting to get ab manager tasks without permission', req.user.username)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var downloads = {
|
var taskData = {
|
||||||
downloads: this.abMergeManager.downloads,
|
tasks: this.abMergeManager.tasks,
|
||||||
pendingDownloads: this.abMergeManager.pendingDownloads
|
pendingTasks: this.abMergeManager.pendingTasks
|
||||||
}
|
}
|
||||||
res.json(downloads)
|
res.json(taskData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: api/settings (admin)
|
// PATCH: api/settings (admin)
|
||||||
|
@ -4,10 +4,11 @@ const fs = require('../libs/fsExtra')
|
|||||||
|
|
||||||
const workerThreads = require('worker_threads')
|
const workerThreads = require('worker_threads')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Download = require('../objects/Download')
|
const AbManagerTask = require('../objects/AbManagerTask')
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
const { getId } = require('../utils/index')
|
const { getId } = require('../utils/index')
|
||||||
const { writeConcatFile, writeMetadataFile } = require('../utils/ffmpegHelpers')
|
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
||||||
|
const toneHelpers = require('../utils/toneHelpers')
|
||||||
const { getFileSize } = require('../utils/fileUtils')
|
const { getFileSize } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class AbMergeManager {
|
class AbMergeManager {
|
||||||
@ -18,8 +19,8 @@ class AbMergeManager {
|
|||||||
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
||||||
this.downloadDirPathExist = false
|
this.downloadDirPathExist = false
|
||||||
|
|
||||||
this.pendingDownloads = []
|
this.pendingTasks = []
|
||||||
this.downloads = []
|
this.tasks = []
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureDownloadDirPath() { // Creates download path if necessary and sets owner and permissions
|
async ensureDownloadDirPath() { // Creates download path if necessary and sets owner and permissions
|
||||||
@ -38,14 +39,14 @@ class AbMergeManager {
|
|||||||
this.downloadDirPathExist = true
|
this.downloadDirPathExist = true
|
||||||
}
|
}
|
||||||
|
|
||||||
getDownload(downloadId) {
|
getTask(taskId) {
|
||||||
return this.downloads.find(d => d.id === downloadId)
|
return this.tasks.find(d => d.id === taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeDownloadById(downloadId) {
|
removeTaskById(taskId) {
|
||||||
var download = this.getDownload(downloadId)
|
var task = this.getTask(taskId)
|
||||||
if (download) {
|
if (task) {
|
||||||
this.removeDownload(download)
|
this.removeTask(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,46 +69,46 @@ class AbMergeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async startAudiobookMerge(user, libraryItem) {
|
async startAudiobookMerge(user, libraryItem) {
|
||||||
var downloadId = getId('abmerge')
|
var taskId = getId('abmerge')
|
||||||
var dlpath = Path.join(this.downloadDirPath, downloadId)
|
var dlpath = Path.join(this.downloadDirPath, taskId)
|
||||||
Logger.info(`Start audiobook merge for ${libraryItem.id} - DownloadId: ${downloadId} - ${dlpath}`)
|
Logger.info(`Start audiobook merge for ${libraryItem.id} - TaskId: ${taskId} - ${dlpath}`)
|
||||||
|
|
||||||
var audiobookDirname = Path.basename(libraryItem.path)
|
var audiobookDirname = Path.basename(libraryItem.path)
|
||||||
var filename = audiobookDirname + '.m4b'
|
var filename = audiobookDirname + '.m4b'
|
||||||
var downloadData = {
|
var taskData = {
|
||||||
id: downloadId,
|
id: taskId,
|
||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
type: 'abmerge',
|
type: 'abmerge',
|
||||||
dirpath: dlpath,
|
dirpath: dlpath,
|
||||||
path: Path.join(dlpath, filename),
|
path: Path.join(dlpath, filename),
|
||||||
filename,
|
filename,
|
||||||
ext: '.m4b',
|
ext: '.m4b',
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
|
libraryItemPath: libraryItem.path,
|
||||||
|
originalTrackPaths: libraryItem.media.tracks.map(t => t.metadata.path)
|
||||||
}
|
}
|
||||||
var download = new Download()
|
var abManagerTask = new AbManagerTask()
|
||||||
download.setData(downloadData)
|
abManagerTask.setData(taskData)
|
||||||
download.setTimeoutTimer(this.downloadTimedOut.bind(this))
|
abManagerTask.setTimeoutTimer(this.downloadTimedOut.bind(this))
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(download.dirpath)
|
await fs.mkdir(abManagerTask.dirpath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[AbMergeManager] Failed to make directory ${download.dirpath}`)
|
Logger.error(`[AbMergeManager] Failed to make directory ${abManagerTask.dirpath}`)
|
||||||
Logger.debug(`[AbMergeManager] Make directory error: ${error}`)
|
Logger.debug(`[AbMergeManager] Make directory error: ${error}`)
|
||||||
var downloadJson = download.toJSON()
|
var taskJson = abManagerTask.toJSON()
|
||||||
this.clientEmitter(user.id, 'abmerge_failed', downloadJson)
|
this.clientEmitter(user.id, 'abmerge_failed', taskJson)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clientEmitter(user.id, 'abmerge_started', download.toJSON())
|
this.clientEmitter(user.id, 'abmerge_started', abManagerTask.toJSON())
|
||||||
this.runAudiobookMerge(libraryItem, download)
|
this.runAudiobookMerge(libraryItem, abManagerTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
async runAudiobookMerge(libraryItem, download) {
|
async runAudiobookMerge(libraryItem, abManagerTask) {
|
||||||
|
|
||||||
// If changing audio file type then encoding is needed
|
// If changing audio file type then encoding is needed
|
||||||
var audioTracks = libraryItem.media.tracks
|
var audioTracks = libraryItem.media.tracks
|
||||||
var audioRequiresEncode = audioTracks[0].metadata.ext !== download.ext
|
var audioRequiresEncode = audioTracks[0].metadata.ext !== abManagerTask.ext
|
||||||
var shouldIncludeCover = libraryItem.media.coverPath
|
var shouldIncludeCover = libraryItem.media.coverPath
|
||||||
var firstTrackIsM4b = audioTracks[0].metadata.ext.toLowerCase() === '.m4b'
|
var firstTrackIsM4b = audioTracks[0].metadata.ext.toLowerCase() === '.m4b'
|
||||||
var isOneTrack = audioTracks.length === 1
|
var isOneTrack = audioTracks.length === 1
|
||||||
@ -115,7 +116,7 @@ class AbMergeManager {
|
|||||||
const ffmpegInputs = []
|
const ffmpegInputs = []
|
||||||
|
|
||||||
if (!isOneTrack) {
|
if (!isOneTrack) {
|
||||||
var concatFilePath = Path.join(download.dirpath, 'files.txt')
|
var concatFilePath = Path.join(abManagerTask.dirpath, 'files.txt')
|
||||||
console.log('Write files.txt', concatFilePath)
|
console.log('Write files.txt', concatFilePath)
|
||||||
await writeConcatFile(audioTracks, concatFilePath)
|
await writeConcatFile(audioTracks, concatFilePath)
|
||||||
ffmpegInputs.push({
|
ffmpegInputs.push({
|
||||||
@ -138,8 +139,8 @@ class AbMergeManager {
|
|||||||
'-map 0:a',
|
'-map 0:a',
|
||||||
'-acodec aac',
|
'-acodec aac',
|
||||||
'-ac 2',
|
'-ac 2',
|
||||||
'-b:a 64k',
|
'-b:a 64k'
|
||||||
'-movflags use_metadata_tags'
|
// '-movflags use_metadata_tags'
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
ffmpegOptions.push('-max_muxing_queue_size 1000')
|
ffmpegOptions.push('-max_muxing_queue_size 1000')
|
||||||
@ -150,34 +151,33 @@ class AbMergeManager {
|
|||||||
ffmpegOptions.push('-c:a copy')
|
ffmpegOptions.push('-c:a copy')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (download.ext === '.m4b') {
|
if (abManagerTask.ext === '.m4b') {
|
||||||
ffmpegOutputOptions.push('-f mp4')
|
ffmpegOutputOptions.push('-f mp4')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ffmetadata file
|
|
||||||
var metadataFilePath = Path.join(download.dirpath, 'metadata.txt')
|
|
||||||
await writeMetadataFile(libraryItem, metadataFilePath)
|
|
||||||
ffmpegInputs.push({
|
|
||||||
input: metadataFilePath
|
|
||||||
})
|
|
||||||
ffmpegOptions.push('-map_metadata 1')
|
|
||||||
|
|
||||||
// Embed cover art
|
var chaptersFilePath = null
|
||||||
if (shouldIncludeCover) {
|
if (libraryItem.media.chapters.length) {
|
||||||
var coverPath = libraryItem.media.coverPath.replace(/\\/g, '/')
|
chaptersFilePath = Path.join(abManagerTask.dirpath, 'chapters.txt')
|
||||||
ffmpegInputs.push({
|
try {
|
||||||
input: coverPath,
|
await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath)
|
||||||
options: ['-f image2pipe']
|
} catch (error) {
|
||||||
})
|
Logger.error(`[AbMergeManager] Write chapters.txt failed`, error)
|
||||||
ffmpegOptions.push('-c:v copy')
|
chaptersFilePath = null
|
||||||
ffmpegOptions.push('-map 2:v')
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath)
|
||||||
|
toneMetadataObject.TrackNumber = 1
|
||||||
|
abManagerTask.toneMetadataObject = toneMetadataObject
|
||||||
|
|
||||||
|
Logger.debug(`[AbMergeManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject)
|
||||||
|
|
||||||
var workerData = {
|
var workerData = {
|
||||||
inputs: ffmpegInputs,
|
inputs: ffmpegInputs,
|
||||||
options: ffmpegOptions,
|
options: ffmpegOptions,
|
||||||
outputOptions: ffmpegOutputOptions,
|
outputOptions: ffmpegOutputOptions,
|
||||||
output: download.path,
|
output: abManagerTask.path,
|
||||||
}
|
}
|
||||||
|
|
||||||
var worker = null
|
var worker = null
|
||||||
@ -186,19 +186,19 @@ class AbMergeManager {
|
|||||||
worker = new workerThreads.Worker(workerPath, { workerData })
|
worker = new workerThreads.Worker(workerPath, { workerData })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[AbMergeManager] Start worker thread failed`, error)
|
Logger.error(`[AbMergeManager] Start worker thread failed`, error)
|
||||||
if (download.userId) {
|
if (abManagerTask.userId) {
|
||||||
var downloadJson = download.toJSON()
|
var taskJson = abManagerTask.toJSON()
|
||||||
this.clientEmitter(download.userId, 'abmerge_failed', downloadJson)
|
this.clientEmitter(abManagerTask.userId, 'abmerge_failed', taskJson)
|
||||||
}
|
}
|
||||||
this.removeDownload(download)
|
this.removeTask(abManagerTask)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
worker.on('message', (message) => {
|
worker.on('message', (message) => {
|
||||||
if (message != null && typeof message === 'object') {
|
if (message != null && typeof message === 'object') {
|
||||||
if (message.type === 'RESULT') {
|
if (message.type === 'RESULT') {
|
||||||
if (!download.isTimedOut) {
|
if (!abManagerTask.isTimedOut) {
|
||||||
this.sendResult(download, message)
|
this.sendResult(abManagerTask, message)
|
||||||
}
|
}
|
||||||
} else if (message.type === 'FFMPEG') {
|
} else if (message.type === 'FFMPEG') {
|
||||||
if (Logger[message.level]) {
|
if (Logger[message.level]) {
|
||||||
@ -209,78 +209,114 @@ class AbMergeManager {
|
|||||||
Logger.error('Invalid worker message', message)
|
Logger.error('Invalid worker message', message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.pendingDownloads.push({
|
this.pendingTasks.push({
|
||||||
id: download.id,
|
id: abManagerTask.id,
|
||||||
download,
|
abManagerTask,
|
||||||
worker
|
worker
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendResult(download, result) {
|
async sendResult(abManagerTask, result) {
|
||||||
download.clearTimeoutTimer()
|
abManagerTask.clearTimeoutTimer()
|
||||||
|
|
||||||
// Remove pending download
|
// Remove pending task
|
||||||
this.pendingDownloads = this.pendingDownloads.filter(d => d.id !== download.id)
|
this.pendingTasks = this.pendingTasks.filter(d => d.id !== abManagerTask.id)
|
||||||
|
|
||||||
if (result.isKilled) {
|
if (result.isKilled) {
|
||||||
if (download.userId) {
|
if (abManagerTask.userId) {
|
||||||
this.clientEmitter(download.userId, 'abmerge_killed', download.toJSON())
|
this.clientEmitter(abManagerTask.userId, 'abmerge_killed', abManagerTask.toJSON())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
if (download.userId) {
|
if (abManagerTask.userId) {
|
||||||
this.clientEmitter(download.userId, 'abmerge_failed', download.toJSON())
|
this.clientEmitter(abManagerTask.userId, 'abmerge_failed', abManagerTask.toJSON())
|
||||||
}
|
}
|
||||||
this.removeDownload(download)
|
this.removeTask(abManagerTask)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write metadata to merged file
|
||||||
|
const success = await toneHelpers.tagAudioFile(abManagerTask.path, abManagerTask.toneMetadataObject)
|
||||||
|
if (!success) {
|
||||||
|
Logger.error(`[AbMergeManager] Failed to write metadata to file "${abManagerTask.path}"`)
|
||||||
|
if (abManagerTask.userId) {
|
||||||
|
this.clientEmitter(abManagerTask.userId, 'abmerge_failed', abManagerTask.toJSON())
|
||||||
|
}
|
||||||
|
this.removeTask(abManagerTask)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move library item tracks to cache
|
||||||
|
const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${abManagerTask.libraryItemId}`)
|
||||||
|
await fs.ensureDir(itemCacheDir)
|
||||||
|
for (const trackPath of abManagerTask.originalTrackPaths) {
|
||||||
|
const trackFilename = Path.basename(trackPath)
|
||||||
|
const moveToPath = Path.join(itemCacheDir, trackFilename)
|
||||||
|
Logger.debug(`[AbMergeManager] Backing up original track "${trackPath}" to ${moveToPath}`)
|
||||||
|
await fs.move(trackPath, moveToPath, { overwrite: true }).catch((err) => {
|
||||||
|
Logger.error(`[AbMergeManager] Failed to move track "${trackPath}" to "${moveToPath}"`, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Set file permissions and ownership
|
// Set file permissions and ownership
|
||||||
await filePerms.setDefault(download.path)
|
await filePerms.setDefault(abManagerTask.path)
|
||||||
|
await filePerms.setDefault(itemCacheDir)
|
||||||
|
|
||||||
var filesize = await getFileSize(download.path)
|
// Move merged file to library item
|
||||||
download.setComplete(filesize)
|
const moveToPath = Path.join(abManagerTask.libraryItemPath, abManagerTask.filename)
|
||||||
if (download.userId) {
|
Logger.debug(`[AbMergeManager] Moving merged audiobook to library item at "${moveToPath}"`)
|
||||||
this.clientEmitter(download.userId, 'abmerge_ready', download.toJSON())
|
const moveSuccess = await fs.move(abManagerTask.path, moveToPath, { overwrite: true }).then(() => true).catch((err) => {
|
||||||
|
Logger.error(`[AbMergeManager] Failed to move merged audiobook from "${abManagerTask.path}" to "${moveToPath}"`, err)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!moveSuccess) {
|
||||||
|
// TODO: Revert cached og files?
|
||||||
}
|
}
|
||||||
download.setExpirationTimer(this.downloadExpired.bind(this))
|
|
||||||
|
|
||||||
this.downloads.push(download)
|
var filesize = await getFileSize(abManagerTask.path)
|
||||||
Logger.info(`[AbMergeManager] Download Ready ${download.id}`)
|
abManagerTask.setComplete(filesize)
|
||||||
|
if (abManagerTask.userId) {
|
||||||
|
this.clientEmitter(abManagerTask.userId, 'abmerge_ready', abManagerTask.toJSON())
|
||||||
|
}
|
||||||
|
// abManagerTask.setExpirationTimer(this.downloadExpired.bind(this))
|
||||||
|
|
||||||
|
// this.tasks.push(abManagerTask)
|
||||||
|
await this.removeTask(abManagerTask)
|
||||||
|
Logger.info(`[AbMergeManager] Ab task finished ${abManagerTask.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadExpired(download) {
|
// async downloadExpired(abManagerTask) {
|
||||||
Logger.info(`[AbMergeManager] Download ${download.id} expired`)
|
// Logger.info(`[AbMergeManager] Download ${abManagerTask.id} expired`)
|
||||||
|
|
||||||
if (download.userId) {
|
// if (abManagerTask.userId) {
|
||||||
this.clientEmitter(download.userId, 'abmerge_expired', download.toJSON())
|
// this.clientEmitter(abManagerTask.userId, 'abmerge_expired', abManagerTask.toJSON())
|
||||||
|
// }
|
||||||
|
// this.removeTask(abManagerTask)
|
||||||
|
// }
|
||||||
|
|
||||||
|
async downloadTimedOut(abManagerTask) {
|
||||||
|
Logger.info(`[AbMergeManager] Download ${abManagerTask.id} timed out (${abManagerTask.timeoutTimeMs}ms)`)
|
||||||
|
|
||||||
|
if (abManagerTask.userId) {
|
||||||
|
var taskJson = abManagerTask.toJSON()
|
||||||
|
taskJson.isTimedOut = true
|
||||||
|
this.clientEmitter(abManagerTask.userId, 'abmerge_failed', taskJson)
|
||||||
}
|
}
|
||||||
this.removeDownload(download)
|
this.removeTask(abManagerTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadTimedOut(download) {
|
async removeTask(abManagerTask) {
|
||||||
Logger.info(`[AbMergeManager] Download ${download.id} timed out (${download.timeoutTimeMs}ms)`)
|
Logger.info('[AbMergeManager] Removing task ' + abManagerTask.id)
|
||||||
|
|
||||||
if (download.userId) {
|
abManagerTask.clearTimeoutTimer()
|
||||||
var downloadJson = download.toJSON()
|
// abManagerTask.clearExpirationTimer()
|
||||||
downloadJson.isTimedOut = true
|
|
||||||
this.clientEmitter(download.userId, 'abmerge_failed', downloadJson)
|
|
||||||
}
|
|
||||||
this.removeDownload(download)
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeDownload(download) {
|
var pendingDl = this.pendingTasks.find(d => d.id === abManagerTask.id)
|
||||||
Logger.info('[AbMergeManager] Removing download ' + download.id)
|
|
||||||
|
|
||||||
download.clearTimeoutTimer()
|
|
||||||
download.clearExpirationTimer()
|
|
||||||
|
|
||||||
var pendingDl = this.pendingDownloads.find(d => d.id === download.id)
|
|
||||||
|
|
||||||
if (pendingDl) {
|
if (pendingDl) {
|
||||||
this.pendingDownloads = this.pendingDownloads.filter(d => d.id !== download.id)
|
this.pendingTasks = this.pendingTasks.filter(d => d.id !== abManagerTask.id)
|
||||||
Logger.warn(`[AbMergeManager] Removing download in progress - stopping worker`)
|
Logger.warn(`[AbMergeManager] Removing download in progress - stopping worker`)
|
||||||
if (pendingDl.worker) {
|
if (pendingDl.worker) {
|
||||||
try {
|
try {
|
||||||
@ -291,12 +327,12 @@ class AbMergeManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.remove(download.dirpath).then(() => {
|
await fs.remove(abManagerTask.dirpath).then(() => {
|
||||||
Logger.info('[AbMergeManager] Deleted download', download.dirpath)
|
Logger.info('[AbMergeManager] Deleted download', abManagerTask.dirpath)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
Logger.error('[AbMergeManager] Failed to delete download', err)
|
Logger.error('[AbMergeManager] Failed to delete download', err)
|
||||||
})
|
})
|
||||||
this.downloads = this.downloads.filter(d => d.id !== download.id)
|
this.tasks = this.tasks.filter(d => d.id !== abManagerTask.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = AbMergeManager
|
module.exports = AbMergeManager
|
||||||
|
@ -43,10 +43,7 @@ class AudioMetadataMangaer {
|
|||||||
|
|
||||||
// Write chapters file
|
// Write chapters file
|
||||||
var chaptersFilePath = null
|
var chaptersFilePath = null
|
||||||
var cachePath = Path.join(global.MetadataPath, 'cache/items')
|
const itemCacheDir = Path.join(global.MetadataPath, `cache/items/${libraryItem.id}`)
|
||||||
console.log('Items Cache Path', cachePath)
|
|
||||||
|
|
||||||
var itemCacheDir = Path.join(cachePath, libraryItem.id)
|
|
||||||
await fs.ensureDir(itemCacheDir)
|
await fs.ensureDir(itemCacheDir)
|
||||||
|
|
||||||
if (libraryItem.media.chapters.length) {
|
if (libraryItem.media.chapters.length) {
|
||||||
|
@ -3,10 +3,11 @@ const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
|||||||
const DEFAULT_EXPIRATION = 1000 * 60 * 60 // 60 minutes
|
const DEFAULT_EXPIRATION = 1000 * 60 * 60 // 60 minutes
|
||||||
const DEFAULT_TIMEOUT = 1000 * 60 * 30 // 30 minutes
|
const DEFAULT_TIMEOUT = 1000 * 60 * 30 // 30 minutes
|
||||||
|
|
||||||
class Download {
|
class AbManagerTask {
|
||||||
constructor(download) {
|
constructor() {
|
||||||
this.id = null
|
this.id = null
|
||||||
this.libraryItemId = null
|
this.libraryItemId = null
|
||||||
|
this.libraryItemPath = null
|
||||||
this.type = null
|
this.type = null
|
||||||
|
|
||||||
this.dirpath = null
|
this.dirpath = null
|
||||||
@ -14,6 +15,8 @@ class Download {
|
|||||||
this.ext = null
|
this.ext = null
|
||||||
this.filename = null
|
this.filename = null
|
||||||
this.size = 0
|
this.size = 0
|
||||||
|
this.toneMetadataObject = null
|
||||||
|
this.originalTrackPaths = []
|
||||||
|
|
||||||
this.userId = null
|
this.userId = null
|
||||||
this.isReady = false
|
this.isReady = false
|
||||||
@ -28,10 +31,6 @@ class Download {
|
|||||||
|
|
||||||
this.timeoutTimer = null
|
this.timeoutTimer = null
|
||||||
this.expirationTimer = null
|
this.expirationTimer = null
|
||||||
|
|
||||||
if (download) {
|
|
||||||
this.construct(download)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get mimeType() {
|
get mimeType() {
|
||||||
@ -52,13 +51,21 @@ class Download {
|
|||||||
isReady: this.isReady,
|
isReady: this.isReady,
|
||||||
startedAt: this.startedAt,
|
startedAt: this.startedAt,
|
||||||
finishedAt: this.finishedAt,
|
finishedAt: this.finishedAt,
|
||||||
expirationSeconds: this.expirationSeconds
|
expirationSeconds: this.expirationSeconds,
|
||||||
|
toneMetadataObject: this.toneMetadataObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setData(downloadData) {
|
||||||
|
downloadData.startedAt = Date.now()
|
||||||
|
downloadData.isProcessing = true
|
||||||
|
this.construct(downloadData)
|
||||||
|
}
|
||||||
|
|
||||||
construct(download) {
|
construct(download) {
|
||||||
this.id = download.id
|
this.id = download.id
|
||||||
this.libraryItemId = download.libraryItemId
|
this.libraryItemId = download.libraryItemId
|
||||||
|
this.libraryItemPath = download.libraryItemPath
|
||||||
this.type = download.type
|
this.type = download.type
|
||||||
|
|
||||||
this.dirpath = download.dirpath
|
this.dirpath = download.dirpath
|
||||||
@ -66,6 +73,7 @@ class Download {
|
|||||||
this.ext = download.ext
|
this.ext = download.ext
|
||||||
this.filename = download.filename
|
this.filename = download.filename
|
||||||
this.size = download.size || 0
|
this.size = download.size || 0
|
||||||
|
this.originalTrackPaths = download.originalTrackPaths
|
||||||
|
|
||||||
this.userId = download.userId
|
this.userId = download.userId
|
||||||
this.isReady = !!download.isReady
|
this.isReady = !!download.isReady
|
||||||
@ -79,12 +87,6 @@ class Download {
|
|||||||
this.expiresAt = download.expiresAt || null
|
this.expiresAt = download.expiresAt || null
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(downloadData) {
|
|
||||||
downloadData.startedAt = Date.now()
|
|
||||||
downloadData.isProcessing = true
|
|
||||||
this.construct(downloadData)
|
|
||||||
}
|
|
||||||
|
|
||||||
setComplete(fileSize) {
|
setComplete(fileSize) {
|
||||||
this.finishedAt = Date.now()
|
this.finishedAt = Date.now()
|
||||||
this.size = fileSize
|
this.size = fileSize
|
||||||
@ -117,4 +119,4 @@ class Download {
|
|||||||
clearTimeout(this.expirationTimer)
|
clearTimeout(this.expirationTimer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = Download
|
module.exports = AbManagerTask
|
@ -221,9 +221,9 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
this.router.post('/upload', MiscController.handleUpload.bind(this))
|
this.router.post('/upload', MiscController.handleUpload.bind(this))
|
||||||
this.router.get('/audiobook-merge/:id', MiscController.mergeAudiobook.bind(this))
|
this.router.get('/audiobook-merge/:id', MiscController.mergeAudiobook.bind(this))
|
||||||
this.router.get('/download/:id', MiscController.getDownload.bind(this))
|
this.router.get('/ab-manager-tasks/:id', MiscController.getAbManagerTask.bind(this))
|
||||||
this.router.delete('/download/:id', MiscController.removeDownload.bind(this))
|
this.router.delete('/ab-manager-tasks/:id', MiscController.removeAbManagerTask.bind(this))
|
||||||
this.router.get('/downloads', MiscController.getDownloads.bind(this))
|
this.router.get('/ab-manager-tasks', MiscController.getAbManagerTasks.bind(this))
|
||||||
this.router.patch('/settings', MiscController.updateServerSettings.bind(this)) // Root only
|
this.router.patch('/settings', MiscController.updateServerSettings.bind(this)) // Root only
|
||||||
this.router.post('/purgecache', MiscController.purgeCache.bind(this)) // Root only
|
this.router.post('/purgecache', MiscController.purgeCache.bind(this)) // Root only
|
||||||
this.router.post('/authorize', MiscController.authorize.bind(this))
|
this.router.post('/authorize', MiscController.authorize.bind(this))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user