2022-05-02 01:33:46 +02:00
const Path = require ( 'path' )
2022-11-24 22:53:58 +01:00
const SocketAuthority = require ( '../SocketAuthority' )
2022-05-02 01:33:46 +02:00
const Logger = require ( '../Logger' )
2022-11-24 22:53:58 +01:00
const fs = require ( '../libs/fsExtra' )
2024-06-29 19:04:23 +02:00
const ffmpegHelpers = require ( '../utils/ffmpegHelpers' )
2023-10-20 23:39:32 +02:00
const TaskManager = require ( './TaskManager' )
2023-04-02 23:13:18 +02:00
const Task = require ( '../objects/Task' )
2024-07-20 11:28:06 +02:00
const fileUtils = require ( '../utils/fileUtils' )
2022-05-02 01:33:46 +02:00
2024-08-24 11:01:00 +02:00
/ * *
* @ typedef UpdateMetadataOptions
* @ property { boolean } [ forceEmbedChapters = false ] - Whether to force embed chapters .
* @ property { boolean } [ backup = false ] - Whether to backup the files .
* /
2022-05-02 01:33:46 +02:00
class AudioMetadataMangaer {
2023-10-20 23:39:32 +02:00
constructor ( ) {
2023-04-02 23:13:18 +02:00
this . itemsCacheDir = Path . join ( global . MetadataPath , 'cache/items' )
this . MAX _CONCURRENT _TASKS = 1
this . tasksRunning = [ ]
this . tasksQueued = [ ]
}
/ * *
2024-06-29 19:04:23 +02:00
* Get queued task data
* @ return { Array }
* /
2023-04-02 23:13:18 +02:00
getQueuedTaskData ( ) {
2024-06-29 19:04:23 +02:00
return this . tasksQueued . map ( ( t ) => t . data )
2023-04-02 23:13:18 +02:00
}
getIsLibraryItemQueuedOrProcessing ( libraryItemId ) {
2024-06-29 19:04:23 +02:00
return this . tasksQueued . some ( ( t ) => t . data . libraryItemId === libraryItemId ) || this . tasksRunning . some ( ( t ) => t . data . libraryItemId === libraryItemId )
2022-05-02 01:33:46 +02:00
}
2024-07-06 23:00:48 +02:00
getMetadataObjectForApi ( libraryItem ) {
2024-06-29 19:04:23 +02:00
return ffmpegHelpers . getFFMetadataObject ( libraryItem , libraryItem . media . includedAudioFiles . length )
2023-04-02 23:13:18 +02:00
}
2024-08-11 22:15:34 +02:00
/ * *
*
* @ param { string } userId
* @ param { * } libraryItems
* @ param { * } options
* /
handleBatchEmbed ( userId , libraryItems , options = { } ) {
2023-04-02 23:13:18 +02:00
libraryItems . forEach ( ( li ) => {
2024-08-11 22:15:34 +02:00
this . updateMetadataForItem ( userId , li , options )
2023-04-02 23:13:18 +02:00
} )
2022-09-25 22:56:06 +02:00
}
2024-08-11 22:15:34 +02:00
/ * *
*
* @ param { string } userId
2024-08-24 11:01:00 +02:00
* @ param { import ( '../objects/LibraryItem' ) } libraryItem
* @ param { UpdateMetadataOptions } [ options = { } ]
2024-08-11 22:15:34 +02:00
* /
async updateMetadataForItem ( userId , libraryItem , options = { } ) {
2023-01-07 22:16:52 +01:00
const forceEmbedChapters = ! ! options . forceEmbedChapters
const backupFiles = ! ! options . backup
const audioFiles = libraryItem . media . includedAudioFiles
2022-09-25 22:56:06 +02:00
2023-04-02 23:13:18 +02:00
const task = new Task ( )
const itemCachePath = Path . join ( this . itemsCacheDir , libraryItem . id )
// Only writing chapters for single file audiobooks
2024-06-29 19:04:23 +02:00
const chapters = audioFiles . length == 1 || forceEmbedChapters ? libraryItem . media . chapters . map ( ( c ) => ( { ... c } ) ) : null
2023-04-02 23:13:18 +02:00
2023-05-27 00:57:56 +02:00
let mimeType = audioFiles [ 0 ] . mimeType
2024-06-29 19:04:23 +02:00
if ( audioFiles . some ( ( a ) => a . mimeType !== mimeType ) ) mimeType = null
2023-05-27 00:57:56 +02:00
2023-04-02 23:13:18 +02:00
// Create task
2024-08-24 11:01:00 +02:00
const libraryItemDir = libraryItem . isFile ? Path . dirname ( libraryItem . path ) : libraryItem . path
2023-04-02 23:13:18 +02:00
const taskData = {
2022-09-25 22:56:06 +02:00
libraryItemId : libraryItem . id ,
2024-08-24 11:01:00 +02:00
libraryItemDir ,
2024-08-11 22:15:34 +02:00
userId ,
2024-06-29 19:04:23 +02:00
audioFiles : audioFiles . map ( ( af ) => ( {
index : af . index ,
ino : af . ino ,
filename : af . metadata . filename ,
path : af . metadata . path ,
2024-07-20 11:28:06 +02:00
cachePath : Path . join ( itemCachePath , af . metadata . filename ) ,
duration : af . duration
2024-06-29 19:04:23 +02:00
} ) ) ,
2023-04-02 23:13:18 +02:00
coverPath : libraryItem . media . coverPath ,
2024-06-29 19:04:23 +02:00
metadataObject : ffmpegHelpers . getFFMetadataObject ( libraryItem , audioFiles . length ) ,
2023-04-02 23:13:18 +02:00
itemCachePath ,
chapters ,
2024-06-29 19:04:23 +02:00
mimeType ,
2023-04-02 23:13:18 +02:00
options : {
forceEmbedChapters ,
backupFiles
2024-07-20 11:28:06 +02:00
} ,
duration : libraryItem . media . duration
2022-09-25 22:56:06 +02:00
}
2024-09-21 00:18:29 +02:00
const taskTitleString = {
text : 'Embedding Metadata' ,
key : 'MessageTaskEmbeddingMetadata'
}
const taskDescriptionString = {
text : ` Embedding metadata in audiobook " ${ libraryItem . media . metadata . title } ". ` ,
key : 'MessageTaskEmbeddingMetadataDescription' ,
subs : [ libraryItem . media . metadata . title ]
}
task . setData ( 'embed-metadata' , taskTitleString , taskDescriptionString , false , taskData )
2023-04-02 23:13:18 +02:00
if ( this . tasksRunning . length >= this . MAX _CONCURRENT _TASKS ) {
Logger . info ( ` [AudioMetadataManager] Queueing embed metadata for audiobook " ${ libraryItem . media . metadata . title } " ` )
SocketAuthority . adminEmitter ( 'metadata_embed_queue_update' , {
libraryItemId : libraryItem . id ,
queued : true
} )
this . tasksQueued . push ( task )
} else {
this . runMetadataEmbed ( task )
}
}
2022-09-25 22:56:06 +02:00
2024-09-21 21:02:57 +02:00
/ * *
*
* @ param { import ( '../objects/Task' ) } task
* /
2023-04-02 23:13:18 +02:00
async runMetadataEmbed ( task ) {
this . tasksRunning . push ( task )
2023-10-20 23:39:32 +02:00
TaskManager . addTask ( task )
2022-09-25 22:56:06 +02:00
2023-04-02 23:13:18 +02:00
Logger . info ( ` [AudioMetadataManager] Starting metadata embed task ` , task . description )
2024-07-20 11:28:06 +02:00
// Ensure target directory is writable
2024-08-24 11:01:00 +02:00
const targetDirWritable = await fileUtils . isWritable ( task . data . libraryItemDir )
Logger . debug ( ` [AudioMetadataManager] Target directory ${ task . data . libraryItemDir } writable: ${ targetDirWritable } ` )
2024-07-20 11:28:06 +02:00
if ( ! targetDirWritable ) {
2024-08-24 11:01:00 +02:00
Logger . error ( ` [AudioMetadataManager] Target directory is not writable: ${ task . data . libraryItemDir } ` )
2024-09-21 21:02:57 +02:00
const taskFailedString = {
text : 'Target directory is not writable' ,
key : 'MessageTaskTargetDirectoryNotWritable'
}
task . setFailed ( taskFailedString )
2024-07-20 11:28:06 +02:00
this . handleTaskFinished ( task )
return
}
// Ensure target audio files are writable
for ( const af of task . data . audioFiles ) {
try {
await fs . access ( af . path , fs . constants . W _OK )
} catch ( err ) {
Logger . error ( ` [AudioMetadataManager] Audio file is not writable: ${ af . path } ` )
2024-09-21 21:02:57 +02:00
const taskFailedString = {
text : ` Audio file " ${ Path . basename ( af . path ) } " is not writable ` ,
key : 'MessageTaskAudioFileNotWritable' ,
subs : [ Path . basename ( af . path ) ]
}
task . setFailed ( taskFailedString )
2024-07-20 11:28:06 +02:00
this . handleTaskFinished ( task )
return
}
}
2023-04-02 23:13:18 +02:00
// Ensure item cache dir exists
2023-01-07 22:16:52 +01:00
let cacheDirCreated = false
2024-06-29 19:04:23 +02:00
if ( ! ( await fs . pathExists ( task . data . itemCachePath ) ) ) {
2024-07-20 11:28:06 +02:00
try {
await fs . mkdir ( task . data . itemCachePath )
cacheDirCreated = true
} catch ( err ) {
Logger . error ( ` [AudioMetadataManager] Failed to create cache directory ${ task . data . itemCachePath } ` , err )
2024-09-21 21:02:57 +02:00
const taskFailedString = {
text : 'Failed to create cache directory' ,
key : 'MessageTaskFailedToCreateCacheDirectory'
}
task . setFailed ( taskFailedString )
2024-07-20 11:28:06 +02:00
this . handleTaskFinished ( task )
return
}
2023-01-07 22:16:52 +01:00
}
2024-07-02 17:25:04 +02:00
// Create ffmetadata file
2024-06-29 19:04:23 +02:00
const ffmetadataPath = Path . join ( task . data . itemCachePath , 'ffmetadata.txt' )
2024-07-02 17:25:04 +02:00
const success = await ffmpegHelpers . writeFFMetadataFile ( task . data . metadataObject , task . data . chapters , ffmetadataPath )
if ( ! success ) {
Logger . error ( ` [AudioMetadataManager] Failed to write ffmetadata file for audiobook " ${ task . data . libraryItemId } " ` )
2024-09-21 21:02:57 +02:00
const taskFailedString = {
text : 'Failed to write metadata file' ,
key : 'MessageTaskFailedToWriteMetadataFile'
}
task . setFailed ( taskFailedString )
2023-04-02 23:13:18 +02:00
this . handleTaskFinished ( task )
2023-01-07 22:16:52 +01:00
return
2022-09-25 22:56:06 +02:00
}
2023-04-02 23:13:18 +02:00
// Tag audio files
2024-07-20 11:28:06 +02:00
let cummulativeProgress = 0
2023-04-02 23:13:18 +02:00
for ( const af of task . data . audioFiles ) {
2024-07-20 11:28:06 +02:00
const audioFileRelativeDuration = af . duration / task . data . duration
SocketAuthority . adminEmitter ( 'track_started' , {
2023-04-02 23:13:18 +02:00
libraryItemId : task . data . libraryItemId ,
ino : af . ino
} )
// Backup audio file
if ( task . data . options . backupFiles ) {
try {
const backupFilePath = Path . join ( task . data . itemCachePath , af . filename )
await fs . copy ( af . path , backupFilePath )
Logger . debug ( ` [AudioMetadataManager] Backed up audio file at " ${ backupFilePath } " ` )
} catch ( err ) {
Logger . error ( ` [AudioMetadataManager] Failed to backup audio file " ${ af . path } " ` , err )
2024-09-21 21:02:57 +02:00
const taskFailedString = {
text : ` Failed to backup audio file " ${ Path . basename ( af . path ) } " ` ,
key : 'MessageTaskFailedToBackupAudioFile' ,
subs : [ Path . basename ( af . path ) ]
}
task . setFailed ( taskFailedString )
2024-07-20 11:28:06 +02:00
this . handleTaskFinished ( task )
return
2023-04-02 23:13:18 +02:00
}
}
2024-07-20 11:28:06 +02:00
try {
await ffmpegHelpers . addCoverAndMetadataToFile ( af . path , task . data . coverPath , ffmetadataPath , af . index , task . data . mimeType , ( progress ) => {
SocketAuthority . adminEmitter ( 'task_progress' , { libraryItemId : task . data . libraryItemId , progress : cummulativeProgress + progress * audioFileRelativeDuration } )
SocketAuthority . adminEmitter ( 'track_progress' , { libraryItemId : task . data . libraryItemId , ino : af . ino , progress } )
} )
2023-04-02 23:13:18 +02:00
Logger . info ( ` [AudioMetadataManager] Successfully tagged audio file " ${ af . path } " ` )
2024-07-20 11:28:06 +02:00
} catch ( err ) {
Logger . error ( ` [AudioMetadataManager] Failed to tag audio file " ${ af . path } " ` , err )
2024-09-21 21:02:57 +02:00
const taskFailedString = {
text : ` Failed to embed metadata in file " ${ Path . basename ( af . path ) } " ` ,
key : 'MessageTaskFailedToEmbedMetadataInFile' ,
subs : [ Path . basename ( af . path ) ]
}
task . setFailed ( taskFailedString )
2024-07-20 11:28:06 +02:00
this . handleTaskFinished ( task )
return
2023-04-02 23:13:18 +02:00
}
2024-07-20 11:28:06 +02:00
SocketAuthority . adminEmitter ( 'track_finished' , {
2023-04-02 23:13:18 +02:00
libraryItemId : task . data . libraryItemId ,
ino : af . ino
} )
2024-07-20 11:28:06 +02:00
cummulativeProgress += audioFileRelativeDuration * 100
2022-09-25 22:56:06 +02:00
}
2023-01-07 22:16:52 +01:00
// Remove temp cache file/folder if not backing up
2023-04-02 23:13:18 +02:00
if ( ! task . data . options . backupFiles ) {
2023-01-07 22:16:52 +01:00
// If cache dir was created from this then remove it
if ( cacheDirCreated ) {
2023-04-02 23:13:18 +02:00
await fs . remove ( task . data . itemCachePath )
2023-01-07 22:16:52 +01:00
} else {
2024-06-29 19:04:23 +02:00
await fs . remove ( ffmetadataPath )
2023-01-07 22:16:52 +01:00
}
}
2023-04-02 23:13:18 +02:00
task . setFinished ( )
this . handleTaskFinished ( task )
2022-09-25 22:56:06 +02:00
}
2023-04-02 23:13:18 +02:00
handleTaskFinished ( task ) {
2023-10-20 23:39:32 +02:00
TaskManager . taskFinished ( task )
2024-06-29 19:04:23 +02:00
this . tasksRunning = this . tasksRunning . filter ( ( t ) => t . id !== task . id )
2023-04-02 23:13:18 +02:00
if ( this . tasksRunning . length < this . MAX _CONCURRENT _TASKS && this . tasksQueued . length ) {
Logger . info ( ` [AudioMetadataManager] Task finished and dequeueing next task. ${ this . tasksQueued } tasks queued. ` )
const nextTask = this . tasksQueued . shift ( )
SocketAuthority . emitter ( 'metadata_embed_queue_update' , {
libraryItemId : nextTask . data . libraryItemId ,
queued : false
} )
this . runMetadataEmbed ( nextTask )
} else if ( this . tasksRunning . length > 0 ) {
Logger . debug ( ` [AudioMetadataManager] Task finished but not dequeueing. Currently running ${ this . tasksRunning . length } tasks. ${ this . tasksQueued . length } tasks queued. ` )
} else {
Logger . debug ( ` [AudioMetadataManager] Task finished and no tasks remain in queue ` )
2022-09-25 22:56:06 +02:00
}
}
2022-05-02 01:33:46 +02:00
}
2022-06-04 19:17:42 +02:00
module . exports = AudioMetadataMangaer