mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-22 11:49:37 +00:00
Add:Tasks widget in appbar for merging m4bs & remove old m4b merge routes
This commit is contained in:
parent
441b8c5bb7
commit
39979ff8a3
16 changed files with 285 additions and 457 deletions
|
|
@ -34,6 +34,7 @@ const PodcastManager = require('./managers/PodcastManager')
|
|||
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
||||
const RssFeedManager = require('./managers/RssFeedManager')
|
||||
const CronManager = require('./managers/CronManager')
|
||||
const TaskManager = require('./managers/TaskManager')
|
||||
|
||||
class Server {
|
||||
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
|
||||
|
|
@ -66,22 +67,23 @@ class Server {
|
|||
this.auth = new Auth(this.db)
|
||||
|
||||
// Managers
|
||||
this.taskManager = new TaskManager(this.emitter.bind(this))
|
||||
this.notificationManager = new NotificationManager(this.db, this.emitter.bind(this))
|
||||
this.backupManager = new BackupManager(this.db, this.emitter.bind(this))
|
||||
this.logManager = new LogManager(this.db)
|
||||
this.cacheManager = new CacheManager()
|
||||
this.abMergeManager = new AbMergeManager(this.db, this.clientEmitter.bind(this))
|
||||
this.abMergeManager = new AbMergeManager(this.db, this.taskManager, this.clientEmitter.bind(this))
|
||||
this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
||||
this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this), this.notificationManager)
|
||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
|
||||
|
||||
this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
|
||||
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
||||
|
||||
// Routers
|
||||
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.notificationManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.notificationManager, this.taskManager, this.emitter.bind(this), this.clientEmitter.bind(this))
|
||||
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
|
||||
this.staticRouter = new StaticRouter(this.db)
|
||||
|
||||
|
|
@ -127,7 +129,6 @@ class Server {
|
|||
|
||||
async init() {
|
||||
Logger.info('[Server] Init v' + version)
|
||||
await this.abMergeManager.removeOrphanDownloads()
|
||||
await this.playbackSessionManager.removeOrphanStreams()
|
||||
|
||||
var previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
|
||||
|
|
|
|||
|
|
@ -110,55 +110,13 @@ class MiscController {
|
|||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// GET: api/ab-manager-tasks/:id
|
||||
async getAbManagerTask(req, res) {
|
||||
if (!req.user.canDownload) {
|
||||
Logger.error('User attempting to download without permission', req.user)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var taskId = req.params.id
|
||||
Logger.info('Download Request', taskId)
|
||||
var task = this.abMergeManager.getTask(taskId)
|
||||
if (!task) {
|
||||
Logger.error('Ab manager task request not found', taskId)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
var options = {
|
||||
headers: {
|
||||
'Content-Type': task.mimeType
|
||||
}
|
||||
}
|
||||
res.download(task.path, task.filename, options, (err) => {
|
||||
if (err) {
|
||||
Logger.error('Download Error', err)
|
||||
}
|
||||
// GET: api/tasks
|
||||
getTasks(req, res) {
|
||||
res.json({
|
||||
tasks: this.taskManager.tasks.map(t => t.toJSON())
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE: api/ab-manager-tasks/:id
|
||||
async removeAbManagerTask(req, res) {
|
||||
if (!req.user.canDownload || !req.user.canDelete) {
|
||||
Logger.error('User attempting to remove ab manager task without permission', req.user.username)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
this.abMergeManager.removeTaskById(req.params.id)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
// GET: api/ab-manager-tasks
|
||||
async getAbManagerTasks(req, res) {
|
||||
if (!req.user.canDownload) {
|
||||
Logger.error('User attempting to get ab manager tasks without permission', req.user.username)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
var taskData = {
|
||||
tasks: this.abMergeManager.tasks,
|
||||
pendingTasks: this.abMergeManager.pendingTasks
|
||||
}
|
||||
res.json(taskData)
|
||||
}
|
||||
|
||||
// PATCH: api/settings (admin)
|
||||
async updateServerSettings(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
|
|
|
|||
|
|
@ -4,23 +4,22 @@ const fs = require('../libs/fsExtra')
|
|||
|
||||
const workerThreads = require('worker_threads')
|
||||
const Logger = require('../Logger')
|
||||
const AbManagerTask = require('../objects/AbManagerTask')
|
||||
const Task = require('../objects/Task')
|
||||
const filePerms = require('../utils/filePerms')
|
||||
const { getId } = require('../utils/index')
|
||||
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
||||
const toneHelpers = require('../utils/toneHelpers')
|
||||
const { getFileSize } = require('../utils/fileUtils')
|
||||
|
||||
class AbMergeManager {
|
||||
constructor(db, clientEmitter) {
|
||||
constructor(db, taskManager, clientEmitter) {
|
||||
this.db = db
|
||||
this.taskManager = taskManager
|
||||
this.clientEmitter = clientEmitter
|
||||
|
||||
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
|
||||
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
||||
this.downloadDirPathExist = false
|
||||
|
||||
this.pendingTasks = []
|
||||
this.tasks = []
|
||||
}
|
||||
|
||||
async ensureDownloadDirPath() { // Creates download path if necessary and sets owner and permissions
|
||||
|
|
@ -39,85 +38,47 @@ class AbMergeManager {
|
|||
this.downloadDirPathExist = true
|
||||
}
|
||||
|
||||
getTask(taskId) {
|
||||
return this.tasks.find(d => d.id === taskId)
|
||||
}
|
||||
|
||||
removeTaskById(taskId) {
|
||||
var task = this.getTask(taskId)
|
||||
if (task) {
|
||||
this.removeTask(task)
|
||||
}
|
||||
}
|
||||
|
||||
async removeOrphanDownloads() {
|
||||
try {
|
||||
var dirs = await fs.readdir(this.downloadDirPath)
|
||||
if (!dirs || !dirs.length) return true
|
||||
|
||||
dirs = dirs.filter(d => d.startsWith('abmerge'))
|
||||
|
||||
await Promise.all(dirs.map(async (dirname) => {
|
||||
var fullPath = Path.join(this.downloadDirPath, dirname)
|
||||
Logger.info(`Removing Orphan Download ${dirname}`)
|
||||
return fs.remove(fullPath)
|
||||
}))
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async startAudiobookMerge(user, libraryItem) {
|
||||
var taskId = getId('abmerge')
|
||||
var dlpath = Path.join(this.downloadDirPath, taskId)
|
||||
Logger.info(`Start audiobook merge for ${libraryItem.id} - TaskId: ${taskId} - ${dlpath}`)
|
||||
const task = new Task()
|
||||
|
||||
var audiobookDirname = Path.basename(libraryItem.path)
|
||||
var filename = audiobookDirname + '.m4b'
|
||||
var taskData = {
|
||||
id: taskId,
|
||||
const audiobookDirname = Path.basename(libraryItem.path)
|
||||
const targetFilename = audiobookDirname + '.m4b'
|
||||
const itemCachePath = Path.join(this.itemsCacheDir, libraryItem.id)
|
||||
const tempFilepath = Path.join(itemCachePath, targetFilename)
|
||||
const taskData = {
|
||||
libraryItemId: libraryItem.id,
|
||||
type: 'abmerge',
|
||||
dirpath: dlpath,
|
||||
path: Path.join(dlpath, filename),
|
||||
filename,
|
||||
ext: '.m4b',
|
||||
userId: user.id,
|
||||
libraryItemPath: libraryItem.path,
|
||||
originalTrackPaths: libraryItem.media.tracks.map(t => t.metadata.path)
|
||||
userId: user.id,
|
||||
originalTrackPaths: libraryItem.media.tracks.map(t => t.metadata.path),
|
||||
tempFilepath,
|
||||
targetFilename,
|
||||
targetFilepath: Path.join(libraryItem.path, targetFilename),
|
||||
itemCachePath,
|
||||
toneMetadataObject: null
|
||||
}
|
||||
var abManagerTask = new AbManagerTask()
|
||||
abManagerTask.setData(taskData)
|
||||
abManagerTask.setTimeoutTimer(this.downloadTimedOut.bind(this))
|
||||
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
|
||||
task.setData('encode-m4b', 'Encoding M4b', taskDescription, taskData)
|
||||
this.taskManager.addTask(task)
|
||||
Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
|
||||
|
||||
try {
|
||||
await fs.mkdir(abManagerTask.dirpath)
|
||||
} catch (error) {
|
||||
Logger.error(`[AbMergeManager] Failed to make directory ${abManagerTask.dirpath}`)
|
||||
Logger.debug(`[AbMergeManager] Make directory error: ${error}`)
|
||||
var taskJson = abManagerTask.toJSON()
|
||||
this.clientEmitter(user.id, 'abmerge_failed', taskJson)
|
||||
return
|
||||
if (!await fs.pathExists(taskData.itemCachePath)) {
|
||||
await fs.mkdir(taskData.itemCachePath)
|
||||
}
|
||||
|
||||
this.clientEmitter(user.id, 'abmerge_started', abManagerTask.toJSON())
|
||||
this.runAudiobookMerge(libraryItem, abManagerTask)
|
||||
this.runAudiobookMerge(libraryItem, task)
|
||||
}
|
||||
|
||||
async runAudiobookMerge(libraryItem, abManagerTask) {
|
||||
async runAudiobookMerge(libraryItem, task) {
|
||||
// If changing audio file type then encoding is needed
|
||||
var audioTracks = libraryItem.media.tracks
|
||||
var audioRequiresEncode = audioTracks[0].metadata.ext !== abManagerTask.ext
|
||||
var shouldIncludeCover = libraryItem.media.coverPath
|
||||
var audioRequiresEncode = audioTracks[0].metadata.ext !== '.m4b'
|
||||
var firstTrackIsM4b = audioTracks[0].metadata.ext.toLowerCase() === '.m4b'
|
||||
var isOneTrack = audioTracks.length === 1
|
||||
|
||||
const ffmpegInputs = []
|
||||
|
||||
if (!isOneTrack) {
|
||||
var concatFilePath = Path.join(abManagerTask.dirpath, 'files.txt')
|
||||
console.log('Write files.txt', concatFilePath)
|
||||
var concatFilePath = Path.join(task.data.itemCachePath, 'files.txt')
|
||||
await writeConcatFile(audioTracks, concatFilePath)
|
||||
ffmpegInputs.push({
|
||||
input: concatFilePath,
|
||||
|
|
@ -132,7 +93,7 @@ class AbMergeManager {
|
|||
|
||||
const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning'
|
||||
var ffmpegOptions = [`-loglevel ${logLevel}`]
|
||||
var ffmpegOutputOptions = []
|
||||
var ffmpegOutputOptions = ['-f mp4']
|
||||
|
||||
if (audioRequiresEncode) {
|
||||
ffmpegOptions = ffmpegOptions.concat([
|
||||
|
|
@ -140,25 +101,20 @@ class AbMergeManager {
|
|||
'-acodec aac',
|
||||
'-ac 2',
|
||||
'-b:a 64k'
|
||||
// '-movflags use_metadata_tags'
|
||||
])
|
||||
} else {
|
||||
ffmpegOptions.push('-max_muxing_queue_size 1000')
|
||||
|
||||
if (isOneTrack && firstTrackIsM4b && !shouldIncludeCover) {
|
||||
if (isOneTrack && firstTrackIsM4b) {
|
||||
ffmpegOptions.push('-c copy')
|
||||
} else {
|
||||
ffmpegOptions.push('-c:a copy')
|
||||
}
|
||||
}
|
||||
if (abManagerTask.ext === '.m4b') {
|
||||
ffmpegOutputOptions.push('-f mp4')
|
||||
}
|
||||
|
||||
|
||||
var chaptersFilePath = null
|
||||
if (libraryItem.media.chapters.length) {
|
||||
chaptersFilePath = Path.join(abManagerTask.dirpath, 'chapters.txt')
|
||||
chaptersFilePath = Path.join(task.data.itemCachePath, 'chapters.txt')
|
||||
try {
|
||||
await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath)
|
||||
} catch (error) {
|
||||
|
|
@ -169,7 +125,7 @@ class AbMergeManager {
|
|||
|
||||
const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath)
|
||||
toneMetadataObject.TrackNumber = 1
|
||||
abManagerTask.toneMetadataObject = toneMetadataObject
|
||||
task.data.toneMetadataObject = toneMetadataObject
|
||||
|
||||
Logger.debug(`[AbMergeManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject)
|
||||
|
||||
|
|
@ -177,7 +133,7 @@ class AbMergeManager {
|
|||
inputs: ffmpegInputs,
|
||||
options: ffmpegOptions,
|
||||
outputOptions: ffmpegOutputOptions,
|
||||
output: abManagerTask.path,
|
||||
output: task.data.tempFilepath
|
||||
}
|
||||
|
||||
var worker = null
|
||||
|
|
@ -186,137 +142,83 @@ class AbMergeManager {
|
|||
worker = new workerThreads.Worker(workerPath, { workerData })
|
||||
} catch (error) {
|
||||
Logger.error(`[AbMergeManager] Start worker thread failed`, error)
|
||||
if (abManagerTask.userId) {
|
||||
var taskJson = abManagerTask.toJSON()
|
||||
this.clientEmitter(abManagerTask.userId, 'abmerge_failed', taskJson)
|
||||
}
|
||||
this.removeTask(abManagerTask)
|
||||
task.setFailed('Failed to start worker thread')
|
||||
this.removeTask(task, true)
|
||||
return
|
||||
}
|
||||
|
||||
worker.on('message', (message) => {
|
||||
if (message != null && typeof message === 'object') {
|
||||
if (message.type === 'RESULT') {
|
||||
if (!abManagerTask.isTimedOut) {
|
||||
this.sendResult(abManagerTask, message)
|
||||
}
|
||||
this.sendResult(task, message)
|
||||
} else if (message.type === 'FFMPEG') {
|
||||
if (Logger[message.level]) {
|
||||
Logger[message.level](message.log)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.error('Invalid worker message', message)
|
||||
}
|
||||
})
|
||||
this.pendingTasks.push({
|
||||
id: abManagerTask.id,
|
||||
abManagerTask,
|
||||
id: task.id,
|
||||
task,
|
||||
worker
|
||||
})
|
||||
}
|
||||
|
||||
async sendResult(abManagerTask, result) {
|
||||
abManagerTask.clearTimeoutTimer()
|
||||
|
||||
async sendResult(task, result) {
|
||||
// Remove pending task
|
||||
this.pendingTasks = this.pendingTasks.filter(d => d.id !== abManagerTask.id)
|
||||
this.pendingTasks = this.pendingTasks.filter(d => d.id !== task.id)
|
||||
|
||||
if (result.isKilled) {
|
||||
if (abManagerTask.userId) {
|
||||
this.clientEmitter(abManagerTask.userId, 'abmerge_killed', abManagerTask.toJSON())
|
||||
}
|
||||
task.setFailed('Ffmpeg task killed')
|
||||
this.removeTask(task, true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (abManagerTask.userId) {
|
||||
this.clientEmitter(abManagerTask.userId, 'abmerge_failed', abManagerTask.toJSON())
|
||||
}
|
||||
this.removeTask(abManagerTask)
|
||||
task.setFailed('Encoding failed')
|
||||
this.removeTask(task, true)
|
||||
return
|
||||
}
|
||||
|
||||
// Write metadata to merged file
|
||||
const success = await toneHelpers.tagAudioFile(abManagerTask.path, abManagerTask.toneMetadataObject)
|
||||
const success = await toneHelpers.tagAudioFile(task.data.tempFilepath, task.data.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)
|
||||
Logger.error(`[AbMergeManager] Failed to write metadata to file "${task.data.tempFilepath}"`)
|
||||
task.setFailed('Failed to write metadata to m4b file')
|
||||
this.removeTask(task, true)
|
||||
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) {
|
||||
for (const trackPath of task.data.originalTrackPaths) {
|
||||
const trackFilename = Path.basename(trackPath)
|
||||
const moveToPath = Path.join(itemCacheDir, trackFilename)
|
||||
const moveToPath = Path.join(task.data.itemCachePath, 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)
|
||||
})
|
||||
}
|
||||
|
||||
// Move m4b to target
|
||||
Logger.debug(`[AbMergeManager] Moving m4b from ${task.data.tempFilepath} to ${task.data.targetFilepath}`)
|
||||
await fs.move(task.data.tempFilepath, task.data.targetFilepath)
|
||||
|
||||
// Set file permissions and ownership
|
||||
await filePerms.setDefault(abManagerTask.path)
|
||||
await filePerms.setDefault(itemCacheDir)
|
||||
await filePerms.setDefault(task.data.targetFilepath)
|
||||
await filePerms.setDefault(task.data.itemCachePath)
|
||||
|
||||
// Move merged file to library item
|
||||
const moveToPath = Path.join(abManagerTask.libraryItemPath, abManagerTask.filename)
|
||||
Logger.debug(`[AbMergeManager] Moving merged audiobook to library item at "${moveToPath}"`)
|
||||
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?
|
||||
}
|
||||
|
||||
var filesize = await getFileSize(abManagerTask.path)
|
||||
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}`)
|
||||
task.setFinished()
|
||||
await this.removeTask(task, false)
|
||||
Logger.info(`[AbMergeManager] Ab task finished ${task.id}`)
|
||||
}
|
||||
|
||||
// async downloadExpired(abManagerTask) {
|
||||
// Logger.info(`[AbMergeManager] Download ${abManagerTask.id} expired`)
|
||||
|
||||
// if (abManagerTask.userId) {
|
||||
// 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.removeTask(abManagerTask)
|
||||
}
|
||||
|
||||
async removeTask(abManagerTask) {
|
||||
Logger.info('[AbMergeManager] Removing task ' + abManagerTask.id)
|
||||
|
||||
abManagerTask.clearTimeoutTimer()
|
||||
// abManagerTask.clearExpirationTimer()
|
||||
|
||||
var pendingDl = this.pendingTasks.find(d => d.id === abManagerTask.id)
|
||||
async removeTask(task, removeTempFilepath = false) {
|
||||
Logger.info('[AbMergeManager] Removing task ' + task.id)
|
||||
|
||||
const pendingDl = this.pendingTasks.find(d => d.id === task.id)
|
||||
if (pendingDl) {
|
||||
this.pendingTasks = this.pendingTasks.filter(d => d.id !== abManagerTask.id)
|
||||
this.pendingTasks = this.pendingTasks.filter(d => d.id !== task.id)
|
||||
Logger.warn(`[AbMergeManager] Removing download in progress - stopping worker`)
|
||||
if (pendingDl.worker) {
|
||||
try {
|
||||
|
|
@ -327,12 +229,17 @@ class AbMergeManager {
|
|||
}
|
||||
}
|
||||
|
||||
await fs.remove(abManagerTask.dirpath).then(() => {
|
||||
Logger.info('[AbMergeManager] Deleted download', abManagerTask.dirpath)
|
||||
}).catch((err) => {
|
||||
Logger.error('[AbMergeManager] Failed to delete download', err)
|
||||
})
|
||||
this.tasks = this.tasks.filter(d => d.id !== abManagerTask.id)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.taskManager.taskFinished(task)
|
||||
}
|
||||
}
|
||||
module.exports = AbMergeManager
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ const { writeMetadataFile } = require('../utils/ffmpegHelpers')
|
|||
const toneHelpers = require('../utils/toneHelpers')
|
||||
|
||||
class AudioMetadataMangaer {
|
||||
constructor(db, emitter, clientEmitter) {
|
||||
constructor(db, taskManager, emitter, clientEmitter) {
|
||||
this.db = db
|
||||
this.taskManager = taskManager
|
||||
this.emitter = emitter
|
||||
this.clientEmitter = clientEmitter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,22 +25,6 @@ class DownloadManager {
|
|||
return this.downloads.find(d => d.id === downloadId)
|
||||
}
|
||||
|
||||
async removeOrphanDownloads() {
|
||||
try {
|
||||
var dirs = await fs.readdir(this.downloadDirPath)
|
||||
if (!dirs || !dirs.length) return true
|
||||
|
||||
await Promise.all(dirs.map(async (dirname) => {
|
||||
var fullPath = Path.join(this.downloadDirPath, dirname)
|
||||
Logger.info(`Removing Orphan Download ${dirname}`)
|
||||
return fs.remove(fullPath)
|
||||
}))
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
downloadSocketRequest(socket, payload) {
|
||||
var client = socket.sheepClient
|
||||
var audiobook = this.db.audiobooks.find(a => a.id === payload.audiobookId)
|
||||
|
|
|
|||
20
server/managers/TaskManager.js
Normal file
20
server/managers/TaskManager.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
class TaskManager {
|
||||
constructor(emitter) {
|
||||
this.emitter = emitter
|
||||
|
||||
this.tasks = []
|
||||
}
|
||||
|
||||
addTask(task) {
|
||||
this.tasks.push(task)
|
||||
this.emitter('task_started', task.toJSON())
|
||||
}
|
||||
|
||||
taskFinished(task) {
|
||||
if (this.tasks.some(t => t.id === task.id)) {
|
||||
this.tasks = this.tasks.filter(t => t !== task.id)
|
||||
this.emitter('task_finished', task.toJSON())
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = TaskManager
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
const { AudioMimeType } = require('../utils/constants')
|
||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||
const DEFAULT_EXPIRATION = 1000 * 60 * 60 // 60 minutes
|
||||
const DEFAULT_TIMEOUT = 1000 * 60 * 30 // 30 minutes
|
||||
|
||||
class AbManagerTask {
|
||||
constructor() {
|
||||
this.id = null
|
||||
this.libraryItemId = null
|
||||
this.libraryItemPath = null
|
||||
this.type = null
|
||||
|
||||
this.dirpath = null
|
||||
this.path = null
|
||||
this.ext = null
|
||||
this.filename = null
|
||||
this.size = 0
|
||||
this.toneMetadataObject = null
|
||||
this.originalTrackPaths = []
|
||||
|
||||
this.userId = null
|
||||
this.isReady = false
|
||||
this.isTimedOut = false
|
||||
|
||||
this.startedAt = null
|
||||
this.finishedAt = null
|
||||
this.expiresAt = null
|
||||
|
||||
this.expirationTimeMs = 0
|
||||
this.timeoutTimeMs = 0
|
||||
|
||||
this.timeoutTimer = null
|
||||
this.expirationTimer = null
|
||||
}
|
||||
|
||||
get mimeType() {
|
||||
return getAudioMimeTypeFromExtname(this.ext) || AudioMimeType.MP3
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
type: this.type,
|
||||
dirpath: this.dirpath,
|
||||
path: this.path,
|
||||
ext: this.ext,
|
||||
filename: this.filename,
|
||||
size: this.size,
|
||||
userId: this.userId,
|
||||
isReady: this.isReady,
|
||||
startedAt: this.startedAt,
|
||||
finishedAt: this.finishedAt,
|
||||
expirationSeconds: this.expirationSeconds,
|
||||
toneMetadataObject: this.toneMetadataObject
|
||||
}
|
||||
}
|
||||
|
||||
setData(downloadData) {
|
||||
downloadData.startedAt = Date.now()
|
||||
downloadData.isProcessing = true
|
||||
this.construct(downloadData)
|
||||
}
|
||||
|
||||
construct(download) {
|
||||
this.id = download.id
|
||||
this.libraryItemId = download.libraryItemId
|
||||
this.libraryItemPath = download.libraryItemPath
|
||||
this.type = download.type
|
||||
|
||||
this.dirpath = download.dirpath
|
||||
this.path = download.path
|
||||
this.ext = download.ext
|
||||
this.filename = download.filename
|
||||
this.size = download.size || 0
|
||||
this.originalTrackPaths = download.originalTrackPaths
|
||||
|
||||
this.userId = download.userId
|
||||
this.isReady = !!download.isReady
|
||||
|
||||
this.startedAt = download.startedAt
|
||||
this.finishedAt = download.finishedAt || null
|
||||
|
||||
this.expirationTimeMs = download.expirationTimeMs || DEFAULT_EXPIRATION
|
||||
this.timeoutTimeMs = download.timeoutTimeMs || DEFAULT_TIMEOUT
|
||||
|
||||
this.expiresAt = download.expiresAt || null
|
||||
}
|
||||
|
||||
setComplete(fileSize) {
|
||||
this.finishedAt = Date.now()
|
||||
this.size = fileSize
|
||||
this.isReady = true
|
||||
this.expiresAt = this.finishedAt + this.expirationTimeMs
|
||||
}
|
||||
|
||||
setExpirationTimer(callback) {
|
||||
this.expirationTimer = setTimeout(() => {
|
||||
if (callback) {
|
||||
callback(this)
|
||||
}
|
||||
}, this.expirationTimeMs)
|
||||
}
|
||||
|
||||
setTimeoutTimer(callback) {
|
||||
this.timeoutTimer = setTimeout(() => {
|
||||
if (callback) {
|
||||
this.isTimedOut = true
|
||||
callback(this)
|
||||
}
|
||||
}, this.timeoutTimeMs)
|
||||
}
|
||||
|
||||
clearTimeoutTimer() {
|
||||
clearTimeout(this.timeoutTimer)
|
||||
}
|
||||
|
||||
clearExpirationTimer() {
|
||||
clearTimeout(this.expirationTimer)
|
||||
}
|
||||
}
|
||||
module.exports = AbManagerTask
|
||||
56
server/objects/Task.js
Normal file
56
server/objects/Task.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
const { getId } = require('../utils/index')
|
||||
|
||||
class Task {
|
||||
constructor() {
|
||||
this.id = null
|
||||
this.action = null // e.g. embed-metadata, encode-m4b, etc
|
||||
this.data = null // additional info for the action like libraryItemId
|
||||
|
||||
this.title = null
|
||||
this.description = null
|
||||
this.error = null
|
||||
|
||||
this.isFailed = false
|
||||
this.isFinished = false
|
||||
|
||||
this.startedAt = null
|
||||
this.finishedAt = null
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
action: this.action,
|
||||
data: this.data ? { ...this.data } : {},
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
error: this.error,
|
||||
isFailed: this.isFailed,
|
||||
isFinished: this.isFinished,
|
||||
startedAt: this.startedAt,
|
||||
finishedAt: this.finishedAt
|
||||
}
|
||||
}
|
||||
|
||||
setData(action, title, description, data = {}) {
|
||||
this.id = getId(action)
|
||||
this.action = action
|
||||
this.data = { ...data }
|
||||
this.title = title
|
||||
this.description = description
|
||||
this.startedAt = Date.now()
|
||||
}
|
||||
|
||||
setFailed(message) {
|
||||
this.error = message
|
||||
this.isFailed = true
|
||||
this.failedAt = Date.now()
|
||||
this.setFinished()
|
||||
}
|
||||
|
||||
setFinished() {
|
||||
this.isFinished = true
|
||||
this.finishedAt = Date.now()
|
||||
}
|
||||
}
|
||||
module.exports = Task
|
||||
|
|
@ -26,7 +26,7 @@ const Series = require('../objects/entities/Series')
|
|||
const FileSystemController = require('../controllers/FileSystemController')
|
||||
|
||||
class ApiRouter {
|
||||
constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, notificationManager, emitter, clientEmitter) {
|
||||
constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, notificationManager, taskManager, emitter, clientEmitter) {
|
||||
this.db = db
|
||||
this.auth = auth
|
||||
this.scanner = scanner
|
||||
|
|
@ -41,6 +41,7 @@ class ApiRouter {
|
|||
this.rssFeedManager = rssFeedManager
|
||||
this.cronManager = cronManager
|
||||
this.notificationManager = notificationManager
|
||||
this.taskManager = taskManager
|
||||
this.emitter = emitter
|
||||
this.clientEmitter = clientEmitter
|
||||
|
||||
|
|
@ -224,9 +225,7 @@ class ApiRouter {
|
|||
//
|
||||
this.router.post('/upload', MiscController.handleUpload.bind(this))
|
||||
this.router.get('/audiobook-merge/:id', MiscController.mergeAudiobook.bind(this))
|
||||
this.router.get('/ab-manager-tasks/:id', MiscController.getAbManagerTask.bind(this))
|
||||
this.router.delete('/ab-manager-tasks/:id', MiscController.removeAbManagerTask.bind(this))
|
||||
this.router.get('/ab-manager-tasks', MiscController.getAbManagerTasks.bind(this))
|
||||
this.router.get('/tasks', MiscController.getTasks.bind(this))
|
||||
this.router.patch('/settings', MiscController.updateServerSettings.bind(this)) // Root only
|
||||
this.router.post('/purgecache', MiscController.purgeCache.bind(this)) // Root only
|
||||
this.router.post('/authorize', MiscController.authorize.bind(this))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue