diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue index 7d4ae2fb..27c378ac 100644 --- a/client/pages/audiobook/_id/edit.vue +++ b/client/pages/audiobook/_id/edit.vue @@ -73,30 +73,6 @@ - -
- Check Track Numbers -
- - - - - - - - - - - - - - - - - -
FilenameIndex# From Metadata# From Filename# From ProbeRaw Tags
{{ trackData.filename }}{{ trackData.currentTrackNum }}{{ trackData.trackNumFromMeta }}{{ trackData.trackNumFromFilename }}{{ trackData.scanDataTrackNum }}{{ JSON.stringify(trackData.rawTags || '') }}
-
-
@@ -137,8 +113,6 @@ export default { ghostClass: 'ghost' }, saving: false, - checkingTrackNumbers: false, - trackNumData: [], currentSort: 'current' } }, @@ -252,19 +226,6 @@ export default { }) this.currentSort = 'filename' }, - checkTrackNumbers() { - this.checkingTrackNumbers = true - this.$axios - .$get(`/api/scantracks/${this.audiobookId}`) - .then((res) => { - this.trackNumData = res - this.checkingTrackNumbers = false - }) - .catch((error) => { - console.error('Failed', error) - this.checkingTrackNumbers = false - }) - }, includeToggled(audio) { var new_index = 0 if (audio.include) { diff --git a/server/ApiController.js b/server/ApiController.js index c767b485..cfd99cac 100644 --- a/server/ApiController.js +++ b/server/ApiController.js @@ -5,7 +5,6 @@ const date = require('date-and-time') const Logger = require('./Logger') const { isObject } = require('./utils/index') -const audioFileScanner = require('./utils/audioFileScanner') const BookController = require('./controllers/BookController') const LibraryController = require('./controllers/LibraryController') @@ -18,9 +17,8 @@ const BookFinder = require('./BookFinder') const AuthorFinder = require('./AuthorFinder') class ApiController { - constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) { + constructor(MetadataPath, db, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) { this.db = db - this.scanner = scanner this.auth = auth this.streamManager = streamManager this.rssFeeds = rssFeeds @@ -167,8 +165,6 @@ class ApiController { this.router.get('/filesystem', this.getFileSystemPaths.bind(this)) - this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this)) - this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this)) this.router.post('/purgecache', this.purgeCache.bind(this)) @@ -362,19 +358,6 @@ class ApiController { res.json(dirs) } - async scanAudioTrackNums(req, res) { - if (!req.user || !req.user.isRoot) { - return res.sendStatus(403) - } - var audiobook = this.db.audiobooks.find(ab => ab.id === req.params.id) - if (!audiobook) { - return res.status(404).send('Audiobook not found') - } - - var scandata = await audioFileScanner.scanTrackNumbers(audiobook) - res.json(scandata) - } - async syncUserAudiobookData(req, res) { if (!req.body.data) { return res.status(403).send('Invalid local user audiobook data') diff --git a/server/HlsController.js b/server/HlsController.js index e29c4cbc..ed44c84f 100644 --- a/server/HlsController.js +++ b/server/HlsController.js @@ -4,9 +4,8 @@ const fs = require('fs-extra') const Logger = require('./Logger') class HlsController { - constructor(db, scanner, auth, streamManager, emitter, StreamsPath) { + constructor(db, auth, streamManager, emitter, StreamsPath) { this.db = db - this.scanner = scanner this.auth = auth this.streamManager = streamManager this.emitter = emitter diff --git a/server/Scanner.js b/server/Scanner.js deleted file mode 100644 index edd80114..00000000 --- a/server/Scanner.js +++ /dev/null @@ -1,768 +0,0 @@ -const fs = require('fs-extra') -const Path = require('path') - -// Utils -const Logger = require('./Logger') -const { version } = require('../package.json') -const audioFileScanner = require('./utils/audioFileScanner') -const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('./utils/scandir') -const { comparePaths, getIno, getId, secondsToTimestamp } = require('./utils/index') -const { ScanResult, CoverDestination } = require('./utils/constants') - -const BookFinder = require('./BookFinder') -const Audiobook = require('./objects/Audiobook') - -class Scanner { - constructor(AUDIOBOOK_PATH, METADATA_PATH, db, coverController, emitter) { - this.AudiobookPath = AUDIOBOOK_PATH - this.MetadataPath = METADATA_PATH - this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books') - - this.db = db - this.coverController = coverController - this.emitter = emitter - - this.cancelScan = false - this.cancelLibraryScan = {} - this.librariesScanning = [] - - this.bookFinder = new BookFinder() - } - - get audiobooks() { - return this.db.audiobooks - } - - getCoverDirectory(audiobook) { - if (this.db.serverSettings.coverDestination === CoverDestination.AUDIOBOOK) { - return { - fullPath: audiobook.fullPath, - relPath: '/s/book/' + audiobook.id - } - } else { - return { - fullPath: Path.posix.join(this.BookMetadataPath, audiobook.id), - relPath: Path.posix.join('/metadata', 'books', audiobook.id) - } - } - } - - async setAudioFileInos(audiobookDataAudioFiles, audiobookAudioFiles) { - for (let i = 0; i < audiobookDataAudioFiles.length; i++) { - var abdFile = audiobookDataAudioFiles[i] - var matchingFile = audiobookAudioFiles.find(af => comparePaths(af.path, abdFile.path)) - if (matchingFile) { - if (!matchingFile.ino) { - matchingFile.ino = await getIno(matchingFile.fullPath) - } - abdFile.ino = matchingFile.ino - } else { - abdFile.ino = await getIno(abdFile.fullPath) - if (!abdFile.ino) { - Logger.error('[Scanner] Invalid abdFile ino - ignoring abd audio file', abdFile.path) - } - } - } - return audiobookDataAudioFiles.filter(abdFile => !!abdFile.ino) - } - - // Only updates audio files with matching paths - syncAudiobookInodeValues(audiobook, { audioFiles, otherFiles }) { - var filesUpdated = 0 - - // Sync audio files & audio tracks with updated inodes - audiobook._audioFiles.forEach((audioFile) => { - var matchingAudioFile = audioFiles.find(af => af.ino !== audioFile.ino && af.path === audioFile.path) - if (matchingAudioFile) { - // Audio Track should always have the same ino as the equivalent audio file (not all audio files have a track) - var audioTrack = audiobook.tracks.find(t => t.ino === audioFile.ino) - if (audioTrack) { - Logger.debug(`[Scanner] Found audio file & track with mismatched inode "${audioFile.filename}" - updating it`) - audioTrack.ino = matchingAudioFile.ino - filesUpdated++ - } else { - Logger.debug(`[Scanner] Found audio file with mismatched inode "${audioFile.filename}" - updating it`) - } - - audioFile.ino = matchingAudioFile.ino - filesUpdated++ - } - }) - - // Sync other files with updated inodes - audiobook._otherFiles.forEach((otherFile) => { - var matchingOtherFile = otherFiles.find(of => of.ino !== otherFile.ino && of.path === otherFile.path) - if (matchingOtherFile) { - Logger.debug(`[Scanner] Found other file with mismatched inode "${otherFile.filename}" - updating it`) - otherFile.ino = matchingOtherFile.ino - filesUpdated++ - } - }) - - return filesUpdated - } - - async searchForCover(audiobook) { - var options = { - titleDistance: 2, - authorDistance: 2 - } - var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.authorFL, options) - if (results.length) { - Logger.debug(`[Scanner] Found best cover for "${audiobook.title}"`) - - // If the first cover result fails, attempt to download the second - for (let i = 0; i < results.length && i < 2; i++) { - - // Downloads and updates the book cover - var result = await this.coverController.downloadCoverFromUrl(audiobook, results[i]) - - if (result.error) { - Logger.error(`[Scanner] Failed to download cover from url "${results[i]}" | Attempt ${i + 1}`, result.error) - } else { - return true - } - } - } - return false - } - - async scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, hasUpdatedLibraryOrFolder, forceAudioFileScan) { - // Always sync files and inode values - var filesInodeUpdated = this.syncAudiobookInodeValues(existingAudiobook, audiobookData) - if (hasUpdatedIno || filesInodeUpdated > 0) { - Logger.info(`[Scanner] Updating inode value for "${existingAudiobook.title}" - ${filesInodeUpdated} files updated`) - hasUpdatedIno = true - } - - // TEMP: Check if is older audiobook and needs force rescan - if (!forceAudioFileScan && (!existingAudiobook.scanVersion || existingAudiobook.checkHasOldCoverPath())) { - Logger.info(`[Scanner] Force rescan for "${existingAudiobook.title}" | Last scan v${existingAudiobook.scanVersion} | Old Cover Path ${!!existingAudiobook.checkHasOldCoverPath()}`) - forceAudioFileScan = true - } - - // inode is required - audiobookData.audioFiles = audiobookData.audioFiles.filter(af => af.ino) - - // No valid ebook and audio files found, mark as incomplete - var ebookFiles = audiobookData.otherFiles.filter(f => f.filetype === 'ebook') - if (!audiobookData.audioFiles.length && !ebookFiles.length) { - Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found - marking as incomplete`) - existingAudiobook.setLastScan(version) - existingAudiobook.isInvalid = true - await this.db.updateAudiobook(existingAudiobook) - this.emitter('audiobook_updated', existingAudiobook.toJSONMinified()) - return ScanResult.UPDATED - } else if (existingAudiobook.isInvalid) { // Was incomplete but now is not - Logger.info(`[Scanner] "${existingAudiobook.title}" was incomplete but now has book files`) - existingAudiobook.isInvalid = false - } - - // Check for audio files that were removed - var abdAudioFileInos = audiobookData.audioFiles.map(af => af.ino) - var removedAudioFiles = existingAudiobook.audioFiles.filter(file => !abdAudioFileInos.includes(file.ino)) - if (removedAudioFiles.length) { - Logger.info(`[Scanner] ${removedAudioFiles.length} audio files removed for audiobook "${existingAudiobook.title}"`) - removedAudioFiles.forEach((af) => existingAudiobook.removeAudioFile(af)) - } - - // Check for mismatched audio tracks - tracks with no matching audio file - var removedAudioTracks = existingAudiobook.tracks.filter(track => !abdAudioFileInos.includes(track.ino)) - if (removedAudioTracks.length) { - Logger.error(`[Scanner] ${removedAudioTracks.length} tracks removed no matching audio file for audiobook "${existingAudiobook.title}"`) - removedAudioTracks.forEach((at) => existingAudiobook.removeAudioTrack(at)) - } - - // Check for new audio files and sync existing audio files - var newAudioFiles = [] - var hasUpdatedAudioFiles = false - audiobookData.audioFiles.forEach((file) => { - var existingAudioFile = existingAudiobook.getAudioFileByIno(file.ino) - if (existingAudioFile) { // Audio file exists, sync path (path may have been renamed) - if (existingAudiobook.syncAudioFile(existingAudioFile, file)) { - hasUpdatedAudioFiles = true - } - } else { - // New audio file, triple check for matching file path - var audioFileWithMatchingPath = existingAudiobook.getAudioFileByPath(file.fullPath) - if (audioFileWithMatchingPath) { - Logger.warn(`[Scanner] Audio file with path already exists with different inode, New: "${file.filename}" (${file.ino}) | Existing: ${audioFileWithMatchingPath.filename} (${audioFileWithMatchingPath.ino})`) - } else { - newAudioFiles.push(file) - } - } - }) - - - // Sync other files (all files that are not audio files) - Updates cover path - var hasOtherFileUpdates = false - var otherFilesUpdated = await existingAudiobook.syncOtherFiles(audiobookData.otherFiles, this.MetadataPath, false, forceAudioFileScan) - if (otherFilesUpdated) { - hasOtherFileUpdates = true - } - - // Rescan audio file metadata - if (forceAudioFileScan) { - Logger.info(`[Scanner] Rescanning ${existingAudiobook.audioFiles.length} audio files for "${existingAudiobook.title}"`) - var numAudioFilesUpdated = await audioFileScanner.rescanAudioFiles(existingAudiobook) - - // Set book details from metadata pulled from audio files - var bookMetadataUpdated = existingAudiobook.setDetailsFromFileMetadata() - if (bookMetadataUpdated) { - Logger.debug(`[Scanner] Book Metadata Updated for "${existingAudiobook.title}"`) - hasUpdatedAudioFiles = true - } - - if (numAudioFilesUpdated > 0) { - Logger.info(`[Scanner] Rescan complete, ${numAudioFilesUpdated} audio files were updated for "${existingAudiobook.title}"`) - hasUpdatedAudioFiles = true - - // Use embedded cover art if audiobook has no cover - if (existingAudiobook.hasEmbeddedCoverArt && !existingAudiobook.cover) { - var outputCoverDirs = this.getCoverDirectory(existingAudiobook) - var relativeDir = await existingAudiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath) - if (relativeDir) { - Logger.debug(`[Scanner] Saved embedded cover art "${relativeDir}"`) - } - } - } else { - Logger.info(`[Scanner] Rescan complete, audio files were up to date for "${existingAudiobook.title}"`) - } - } - - // Scan and add new audio files found and set tracks - if (newAudioFiles.length) { - Logger.info(`[Scanner] ${newAudioFiles.length} new audio files were found for audiobook "${existingAudiobook.title}"`) - await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles) - } - - // After scanning audio files, some may no longer be valid - // so make sure the directory still has valid book files - if (!existingAudiobook.tracks.length && !ebookFiles.length) { - Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found after update - marking as incomplete`) - existingAudiobook.setLastScan(version) - existingAudiobook.isInvalid = true - await this.db.updateAudiobook(existingAudiobook) - this.emitter('audiobook_updated', existingAudiobook.toJSONMinified()) - return ScanResult.UPDATED - } - - var hasUpdates = hasOtherFileUpdates || hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles - - // Check that audio tracks are in sequential order with no gaps - if (existingAudiobook.checkUpdateMissingTracks()) { - Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`) - hasUpdates = true - } - - // Syncs path and fullPath - if (existingAudiobook.syncPaths(audiobookData)) { - hasUpdates = true - } - - // If audiobook was missing before, it is now found - if (existingAudiobook.isMissing) { - existingAudiobook.isMissing = false - hasUpdates = true - Logger.info(`[Scanner] "${existingAudiobook.title}" was missing but now it is found`) - } - - if (hasUpdates || version !== existingAudiobook.scanVersion) { - existingAudiobook.setChapters() - existingAudiobook.setLastScan(version) - await this.db.updateAudiobook(existingAudiobook) - - Logger.info(`[Scanner] "${existingAudiobook.title}" was updated`) - this.emitter('audiobook_updated', existingAudiobook.toJSONMinified()) - return ScanResult.UPDATED - } - - return ScanResult.UPTODATE - } - - async scanNewAudiobook(audiobookData) { - var ebookFiles = audiobookData.otherFiles.map(f => f.filetype === 'ebook') - if (!audiobookData.audioFiles.length && !ebookFiles.length) { - Logger.error('[Scanner] No valid audio files and ebooks for Audiobook', audiobookData.path) - return null - } - - var audiobook = new Audiobook() - audiobook.setData(audiobookData) - - // Scan audio files and set tracks, pulls metadata - await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles) - - if (!audiobook.tracks.length && !audiobook.ebooks.length) { - Logger.warn('[Scanner] Invalid audiobook, no valid audio tracks and ebook files', audiobook.title) - return null - } - - // Look for desc.txt and reader.txt and update - await audiobook.saveDataFromTextFiles(false) - - // Extract embedded cover art if cover is not already in directory - if (audiobook.hasEmbeddedCoverArt && !audiobook.cover) { - var outputCoverDirs = this.getCoverDirectory(audiobook) - var relativeDir = await audiobook.saveEmbeddedCoverArt(outputCoverDirs.fullPath, outputCoverDirs.relPath) - if (relativeDir) { - Logger.debug(`[Scanner] Saved embedded cover art "${relativeDir}"`) - } - } - - // Set book details from metadata pulled from audio files - audiobook.setDetailsFromFileMetadata() - - // Check for gaps in track numbers - audiobook.checkUpdateMissingTracks() - - // Set chapters from audio files - audiobook.setChapters() - - audiobook.setLastScan(version) - - Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`) - await this.db.insertEntity('audiobook', audiobook) - this.emitter('audiobook_added', audiobook.toJSONMinified()) - return audiobook - } - - async scanAudiobookData(audiobookData, forceAudioFileScan = false) { - var scannerFindCovers = this.db.serverSettings.scannerFindCovers - var libraryId = audiobookData.libraryId - var folderId = audiobookData.folderId - - var hasUpdatedLibraryOrFolder = false - - var existingAudiobook = this.audiobooks.find(ab => ab.ino === audiobookData.ino) - - // Make sure existing audiobook has the same library & folder id - if (existingAudiobook && (existingAudiobook.libraryId !== libraryId || existingAudiobook.folderId !== folderId)) { - var existingAudiobookLibrary = this.db.libraries.find(lib => lib.id === existingAudiobook.libraryId) - - if (!existingAudiobookLibrary) { - Logger.error(`[Scanner] Audiobook "${existingAudiobook.title}" found in different library that no longer exists ${existingAudiobook.libraryId}`) - } else if (existingAudiobook.libraryId !== libraryId) { - Logger.warn(`[Scanner] Audiobook "${existingAudiobook.title}" found in different library "${existingAudiobookLibrary.name}"`) - } else { - Logger.warn(`[Scanner] Audiobook "${existingAudiobook.title}" found in different folder "${existingAudiobook.folderId}" of library "${existingAudiobookLibrary.name}"`) - } - - existingAudiobook.libraryId = libraryId - existingAudiobook.folderId = folderId - hasUpdatedLibraryOrFolder = true - Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`) - } - - // inode value may change when using shared drives, update inode if matching path is found - // Note: inode will not change on rename - var hasUpdatedIno = false - if (!existingAudiobook) { - // check an audiobook exists with matching path, then update inodes - existingAudiobook = this.audiobooks.find(a => a.path === audiobookData.path) - if (existingAudiobook) { - var oldIno = existingAudiobook.ino - existingAudiobook.ino = audiobookData.ino - Logger.debug(`[Scanner] Scan Audiobook Data: Updated inode from "${oldIno}" to "${existingAudiobook.ino}"`) - hasUpdatedIno = true - - if (existingAudiobook.libraryId !== libraryId || existingAudiobook.folderId !== folderId) { - Logger.warn(`[Scanner] Audiobook found by path is in a different library or folder, ${existingAudiobook.libraryId}/${existingAudiobook.folderId} should be ${libraryId}/${folderId}`) - - existingAudiobook.libraryId = libraryId - existingAudiobook.folderId = folderId - hasUpdatedLibraryOrFolder = true - Logger.info(`[Scanner] Updated Audiobook "${existingAudiobook.title}" library and folder to "${libraryId}" "${folderId}"`) - } - } - } - - var scanResult = null - var finalAudiobook = null - - if (existingAudiobook) { - finalAudiobook = existingAudiobook - - scanResult = await this.scanExistingAudiobook(existingAudiobook, audiobookData, hasUpdatedIno, hasUpdatedLibraryOrFolder, forceAudioFileScan) - - if (scanResult === ScanResult.REMOVED || scanResult === ScanResult.NOTHING) { - finalAudiobook = null - } - } else { - finalAudiobook = await this.scanNewAudiobook(audiobookData) - - scanResult = finalAudiobook ? ScanResult.ADDED : ScanResult.NOTHING - - if (finalAudiobook === ScanResult.NOTHING) { - finalAudiobook = null - scanResult = ScanResult.NOTHING - } else { - scanResult = ScanResult.ADDED - } - } - - // Scan for cover if enabled and has no cover - if (finalAudiobook && scannerFindCovers && !finalAudiobook.cover) { - if (finalAudiobook.book.shouldSearchForCover) { - var updatedCover = await this.searchForCover(finalAudiobook) - - finalAudiobook.book.updateLastCoverSearch(updatedCover) - - if (updatedCover && scanResult === ScanResult.UPTODATE) { - scanResult = ScanResult.UPDATED - } - await this.db.updateAudiobook(finalAudiobook) - this.emitter('audiobook_updated', finalAudiobook.toJSONMinified()) - } else { - Logger.debug(`[Scanner] Audiobook "${finalAudiobook.title}" cover already scanned - not re-scanning`) - } - } - - return scanResult - } - - async scan(libraryId, forceAudioFileScan = false) { - if (this.librariesScanning.includes(libraryId)) { - Logger.error(`[Scanner] Already scanning ${libraryId}`) - return - } - - var library = this.db.libraries.find(lib => lib.id === libraryId) - if (!library) { - Logger.error(`[Scanner] Library not found for scan ${libraryId}`) - return - } else if (!library.folders.length) { - Logger.warn(`[Scanner] Library has no folders to scan "${library.name}"`) - return - } - - var scanPayload = { - id: libraryId, - name: library.name, - folders: library.folders.length - } - this.emitter('scan_start', scanPayload) - Logger.info(`[Scanner] Starting scan of library "${library.name}" with ${library.folders.length} folders`) - - library.lastScan = Date.now() - await this.db.updateEntity('library', library) - - this.librariesScanning.push(scanPayload) - - var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId) - - // TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos" - if (audiobooksInLibrary.length) { - for (let i = 0; i < audiobooksInLibrary.length; i++) { - var ab = audiobooksInLibrary[i] - // Update ino if inos are not set - var shouldUpdateIno = ab.hasMissingIno - if (shouldUpdateIno) { - var filesWithMissingIno = ab.getFilesWithMissingIno() - - Logger.debug(`\nUpdating inos for "${ab.title}"`) - Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno) - - var hasUpdates = await ab.checkUpdateInos() - if (hasUpdates) { - await this.db.updateAudiobook(ab) - } - } - } - } - - const scanStart = Date.now() - var audiobookDataFound = [] - for (let i = 0; i < library.folders.length; i++) { - var folder = library.folders[i] - var abDataFoundInFolder = await scanRootDir(folder, this.db.serverSettings) - Logger.debug(`[Scanner] ${abDataFoundInFolder.length} ab data found in folder "${folder.fullPath}"`) - audiobookDataFound = audiobookDataFound.concat(abDataFoundInFolder) - } - - // Remove audiobooks with no inode - audiobookDataFound = audiobookDataFound.filter(abd => abd.ino) - - if (this.cancelLibraryScan[libraryId]) { - Logger.info(`[Scanner] Canceling scan ${libraryId}`) - delete this.cancelLibraryScan[libraryId] - this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId) - this.emitter('scan_complete', { id: libraryId, name: library.name, results: null }) - return null - } - - var scanResults = { - removed: 0, - updated: 0, - added: 0, - missing: 0 - } - - // Check for removed audiobooks - for (let i = 0; i < audiobooksInLibrary.length; i++) { - var audiobook = audiobooksInLibrary[i] - var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path)) - if (!dataFound) { - Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`) - audiobook.isMissing = true - audiobook.lastUpdate = Date.now() - scanResults.missing++ - await this.db.updateAudiobook(audiobook) - this.emitter('audiobook_updated', audiobook.toJSONMinified()) - } - if (this.cancelLibraryScan[libraryId]) { - Logger.info(`[Scanner] Canceling scan ${libraryId}`) - delete this.cancelLibraryScan[libraryId] - this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId) - this.emitter('scan_complete', { id: libraryId, name: library.name, results: scanResults }) - return - } - } - - // Check for new and updated audiobooks - for (let i = 0; i < audiobookDataFound.length; i++) { - var result = await this.scanAudiobookData(audiobookDataFound[i], forceAudioFileScan) - if (result === ScanResult.ADDED) scanResults.added++ - if (result === ScanResult.REMOVED) scanResults.removed++ - if (result === ScanResult.UPDATED) scanResults.updated++ - - var progress = Math.round(100 * (i + 1) / audiobookDataFound.length) - this.emitter('scan_progress', { - id: libraryId, - name: library.name, - progress: { - total: audiobookDataFound.length, - done: i + 1, - progress - } - }) - if (this.cancelLibraryScan[libraryId]) { - Logger.info(`[Scanner] Canceling scan ${libraryId}`) - delete this.cancelLibraryScan[libraryId] - break - } - } - const scanElapsed = Math.floor((Date.now() - scanStart) / 1000) - Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | ${scanResults.missing} missing | elapsed: ${secondsToTimestamp(scanElapsed)}`) - this.librariesScanning = this.librariesScanning.filter(l => l.id !== libraryId) - this.emitter('scan_complete', { id: libraryId, name: library.name, results: scanResults }) - } - - async scanAudiobookById(audiobookId) { - const audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId) - if (!audiobook) { - Logger.error(`[Scanner] Scan audiobook by id not found ${audiobookId}`) - return ScanResult.NOTHING - } - const library = this.db.libraries.find(lib => lib.id === audiobook.libraryId) - if (!library) { - Logger.error(`[Scanner] Scan audiobook by id library not found "${audiobook.libraryId}"`) - return ScanResult.NOTHING - } - const folder = library.folders.find(f => f.id === audiobook.folderId) - if (!folder) { - Logger.error(`[Scanner] Scan audiobook by id folder not found "${audiobook.folderId}" in library "${library.name}"`) - return ScanResult.NOTHING - } - if (!folder.libraryId) { - Logger.fatal(`[Scanner] Folder does not have a library id set...`, folder) - return ScanResult.NOTHING - } - - Logger.info(`[Scanner] Scanning Audiobook "${audiobook.title}"`) - return this.scanAudiobook(folder, audiobook.fullPath, true) - } - - async scanAudiobook(folder, audiobookFullPath, forceAudioFileScan = false) { - Logger.debug('[Scanner] scanAudiobook', audiobookFullPath) - var audiobookData = await getAudiobookFileData(folder, audiobookFullPath, this.db.serverSettings) - if (!audiobookData) { - return ScanResult.NOTHING - } - return this.scanAudiobookData(audiobookData, forceAudioFileScan) - } - - async scanFolderUpdates(libraryId, folderId, fileUpdateBookGroup) { - var library = this.db.libraries.find(lib => lib.id === libraryId) - if (!library) { - Logger.error(`[Scanner] Library "${libraryId}" not found for scan library updates`) - return null - } - var folder = library.folders.find(f => f.id === folderId) - if (!folder) { - Logger.error(`[Scanner] Folder "${folderId}" not found in library "${library.name}" for scan library updates`) - return null - } - - Logger.debug(`[Scanner] Scanning file update groups in folder "${folder.id}" of library "${library.name}"`) - - var bookGroupingResults = {} - for (const bookDir in fileUpdateBookGroup) { - var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), bookDir) - - // Check if book dir group is already an audiobook or in a subdir of an audiobook - var existingAudiobook = this.db.audiobooks.find(ab => fullPath.startsWith(ab.fullPath)) - if (existingAudiobook) { - - // Is the audiobook exactly - check if was deleted - if (existingAudiobook.fullPath === fullPath) { - var exists = await fs.pathExists(fullPath) - if (!exists) { - Logger.info(`[Scanner] Scanning file update group and audiobook was deleted "${existingAudiobook.title}" - marking as missing`) - existingAudiobook.isMissing = true - existingAudiobook.lastUpdate = Date.now() - await this.db.updateAudiobook(existingAudiobook) - this.emitter('audiobook_updated', existingAudiobook.toJSONMinified()) - - bookGroupingResults[bookDir] = ScanResult.REMOVED - continue; - } - } - - // Scan audiobook for updates - Logger.debug(`[Scanner] Folder update for relative path "${bookDir}" is in audiobook "${existingAudiobook.title}" - scan for updates`) - bookGroupingResults[bookDir] = await this.scanAudiobook(folder, existingAudiobook.fullPath) - continue; - } - - // Check if an audiobook is a subdirectory of this dir - var childAudiobook = this.db.audiobooks.find(ab => ab.fullPath.startsWith(fullPath)) - if (childAudiobook) { - Logger.warn(`[Scanner] Files were modified in a parent directory of an audiobook "${childAudiobook.title}" - ignoring`) - bookGroupingResults[bookDir] = ScanResult.NOTHING - continue; - } - - Logger.debug(`[Scanner] Folder update group must be a new book "${bookDir}" in library "${library.name}"`) - bookGroupingResults[bookDir] = await this.scanAudiobook(folder, fullPath) - } - - return bookGroupingResults - } - - // Array of file update objects that may have been renamed, removed or added - async filesChanged(fileUpdates) { - if (!fileUpdates.length) return null - - // Group files by folder - var folderGroups = {} - fileUpdates.forEach((file) => { - if (folderGroups[file.folderId]) { - folderGroups[file.folderId].fileUpdates.push(file) - } else { - folderGroups[file.folderId] = { - libraryId: file.libraryId, - folderId: file.folderId, - fileUpdates: [file] - } - } - }) - - const libraryScanResults = {} - - // Group files by book - for (const folderId in folderGroups) { - var libraryId = folderGroups[folderId].libraryId - var library = this.db.libraries.find(lib => lib.id === libraryId) - if (!library) { - Logger.error(`[Scanner] Library not found in files changed ${libraryId}`) - continue; - } - var folder = library.getFolderById(folderId) - if (!folder) { - Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`) - continue; - } - - var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath) - var fileUpdateBookGroup = groupFilesIntoAudiobookPaths(relFilePaths, true) - var folderScanResults = await this.scanFolderUpdates(libraryId, folderId, fileUpdateBookGroup) - libraryScanResults[libraryId] = folderScanResults - } - - Logger.debug(`[Scanner] Finished scanning file changes, results:`, libraryScanResults) - return libraryScanResults - } - - async saveMetadata(audiobookId) { - if (audiobookId) { - var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId) - if (!audiobook) { - return { - error: 'Audiobook not found' - } - } - var savedPath = await audiobook.writeNfoFile() - return { - audiobookId, - audiobookTitle: audiobook.title, - savedPath - } - } else { - var response = { - success: 0, - failed: 0 - } - for (let i = 0; i < this.db.audiobooks.length; i++) { - var audiobook = this.db.audiobooks[i] - var savedPath = await audiobook.writeNfoFile() - if (savedPath) { - Logger.info(`[Scanner] Saved metadata nfo ${savedPath}`) - response.success++ - } else { - response.failed++ - } - } - return response - } - } - - async find(req, res) { - var method = req.params.method - var query = req.query - - var result = null - - if (method === 'isbn') { - result = await this.bookFinder.findByISBN(query) - } else if (method === 'search') { - result = await this.bookFinder.search(query.provider, query.title, query.author || null) - } - - res.json(result) - } - - async findCovers(req, res) { - var query = req.query - var options = { - fallbackTitleOnly: !!query.fallbackTitleOnly - } - var result = await this.bookFinder.findCovers(query.provider, query.title, query.author || null, options) - res.json(result) - } - - async fixDuplicateIds() { - var ids = {} - var audiobooksUpdated = 0 - for (let i = 0; i < this.db.audiobooks.length; i++) { - var ab = this.db.audiobooks[i] - if (ids[ab.id]) { - var abCopy = new Audiobook(ab.toJSON()) - abCopy.id = getId('ab') - if (abCopy.book.cover) { - abCopy.book.cover = abCopy.book.cover.replace(ab.id, abCopy.id) - } - Logger.warn('Found duplicate ID - updating from', ab.id, 'to', abCopy.id) - await this.db.removeEntity('audiobook', ab.id) - await this.db.insertEntity('audiobook', abCopy) - audiobooksUpdated++ - } else { - ids[ab.id] = true - } - } - if (audiobooksUpdated) { - Logger.info(`[Scanner] Updated ${audiobooksUpdated} audiobook IDs`) - } - } -} -module.exports = Scanner \ No newline at end of file diff --git a/server/Server.js b/server/Server.js index 08f6a16e..21ec5504 100644 --- a/server/Server.js +++ b/server/Server.js @@ -17,8 +17,7 @@ const Logger = require('./Logger') // Classes const Auth = require('./Auth') const Watcher = require('./Watcher') -const Scanner = require('./Scanner') -const Scanner2 = require('./scanner/Scanner') +const Scanner = require('./scanner/Scanner') const Db = require('./Db') const BackupManager = require('./BackupManager') const LogManager = require('./LogManager') @@ -52,13 +51,12 @@ class Server { this.watcher = new Watcher(this.AudiobookPath) this.coverController = new CoverController(this.db, this.cacheManager, this.MetadataPath, this.AudiobookPath) this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this)) - this.scanner2 = new Scanner2(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this)) this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this), this.clientEmitter.bind(this)) this.rssFeeds = new RssFeeds(this.Port, this.db) this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this)) - this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this)) - this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath) + this.apiController = new ApiController(this.MetadataPath, this.db, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this)) + this.hlsController = new HlsController(this.db, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath) Logger.logManager = this.logManager @@ -311,18 +309,18 @@ class Server { async filesChanged(fileUpdates) { Logger.info('[Server]', fileUpdates.length, 'Files Changed') - await this.scanner2.scanFilesChanged(fileUpdates) + await this.scanner.scanFilesChanged(fileUpdates) } async scan(libraryId, options = {}) { Logger.info('[Server] Starting Scan') - await this.scanner2.scan(libraryId, options) + await this.scanner.scan(libraryId, options) // await this.scanner.scan(libraryId) Logger.info('[Server] Scan complete') } async scanAudiobook(socket, audiobookId) { - var result = await this.scanner2.scanAudiobookById(audiobookId) + var result = await this.scanner.scanAudiobookById(audiobookId) var scanResultName = '' for (const key in ScanResult) { if (ScanResult[key] === result) { @@ -334,7 +332,7 @@ class Server { cancelScan(id) { Logger.debug('[Server] Cancel scan', id) - this.scanner2.setCancelLibraryScan(id) + this.scanner.setCancelLibraryScan(id) } // Generates an NFO metadata file, if no audiobookId is passed then all audiobooks are done @@ -623,7 +621,7 @@ class Server { configPath: this.ConfigPath, user: client.user.toJSONForBrowser(), stream: client.stream || null, - librariesScanning: this.scanner2.librariesScanning, + librariesScanning: this.scanner.librariesScanning, backups: (this.backupManager.backups || []).map(b => b.toJSON()) } if (user.type === 'root') { diff --git a/server/objects/AudioFile.js b/server/objects/AudioFile.js index 99edac14..2e33f0ef 100644 --- a/server/objects/AudioFile.js +++ b/server/objects/AudioFile.js @@ -121,40 +121,6 @@ class AudioFile { } } - setData(data) { - this.index = data.index || null - this.ino = data.ino || null - this.filename = data.filename - this.ext = data.ext - this.path = data.path - this.fullPath = data.fullPath - this.addedAt = Date.now() - - this.trackNumFromMeta = data.trackNumFromMeta - this.trackNumFromFilename = data.trackNumFromFilename - this.cdNumFromFilename = data.cdNumFromFilename - - this.manuallyVerified = !!data.manuallyVerified - this.invalid = !!data.invalid - this.exclude = !!data.exclude - this.error = data.error || null - - this.format = data.format - this.duration = data.duration - this.size = data.size - this.bitRate = data.bit_rate || null - this.language = data.language - this.codec = data.codec || null - this.timeBase = data.time_base - this.channels = data.channels - this.channelLayout = data.channel_layout - this.chapters = data.chapters || [] - this.embeddedCoverArt = data.embedded_cover_art || null - - this.metadata = new AudioFileMetadata() - this.metadata.setData(data) - } - // New scanner creates AudioFile from AudioFileScanner setDataFromProbe(fileData, probeData) { this.index = fileData.index || null @@ -224,48 +190,6 @@ class AudioFile { return hasUpdates } - // Called from audioFileScanner.js with scanData - updateMetadata(data) { - if (!this.metadata) this.metadata = new AudioFileMetadata() - - var dataMap = { - format: data.format, - duration: data.duration, - size: data.size, - bitRate: data.bit_rate || null, - language: data.language, - codec: data.codec || null, - timeBase: data.time_base, - channels: data.channels, - channelLayout: data.channel_layout, - chapters: data.chapters || [], - embeddedCoverArt: data.embedded_cover_art || null, - trackNumFromMeta: data.trackNumFromMeta, - trackNumFromFilename: data.trackNumFromFilename, - cdNumFromFilename: data.cdNumFromFilename - } - - var hasUpdates = false - for (const key in dataMap) { - if (key === 'chapters') { - var chaptersUpdated = this.syncChapters(dataMap.chapters) - if (chaptersUpdated) { - hasUpdates = true - } - } else if (dataMap[key] !== this[key]) { - // Logger.debug(`[AudioFile] "${key}" from ${this[key]} => ${dataMap[key]}`) - this[key] = dataMap[key] - hasUpdates = true - } - } - - if (this.metadata.updateData(data)) { - hasUpdates = true - } - - return hasUpdates - } - clone() { return new AudioFile(this.toJSON()) } diff --git a/server/objects/Audiobook.js b/server/objects/Audiobook.js index 8e42b63a..3483317e 100644 --- a/server/objects/Audiobook.js +++ b/server/objects/Audiobook.js @@ -402,15 +402,8 @@ class Audiobook { } addAudioFile(audioFileData) { - if (audioFileData instanceof AudioFile) { - this.audioFiles.push(audioFileData) - return audioFileData - } else { - var audioFile = new AudioFile() - audioFile.setData(audioFileData) - this.audioFiles.push(audioFile) - return audioFile - } + this.audioFiles.push(audioFileData) + return audioFileData } updateAudioFile(updatedAudioFile) { diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js index c217f661..94311208 100644 --- a/server/scanner/AudioFileScanner.js +++ b/server/scanner/AudioFileScanner.js @@ -68,7 +68,7 @@ class AudioFileScanner { async scan(audioFileData, bookScanData, verbose = false) { var probeStart = Date.now() // Logger.debug(`[AudioFileScanner] Start Probe ${audioFileData.fullPath}`) - var probeData = await prober.probe2(audioFileData.fullPath, verbose) + var probeData = await prober.probe(audioFileData.fullPath, verbose) if (probeData.error) { Logger.error(`[AudioFileScanner] ${probeData.error} : "${audioFileData.fullPath}"`) return null diff --git a/server/scanner/AudioProbeData.js b/server/scanner/AudioProbeData.js index 82efa6fa..0fd294ec 100644 --- a/server/scanner/AudioProbeData.js +++ b/server/scanner/AudioProbeData.js @@ -21,20 +21,13 @@ class AudioProbeData { this.trackTotal = null } - getDefaultAudioStream(audioStreams) { - if (audioStreams.length === 1) return audioStreams[0] - var defaultStream = audioStreams.find(a => a.is_default) - if (!defaultStream) return audioStreams[0] - return defaultStream - } - getEmbeddedCoverArt(videoStream) { const ImageCodecs = ['mjpeg', 'jpeg', 'png'] return ImageCodecs.includes(videoStream.codec) ? videoStream.codec : null } setData(data) { - var audioStream = this.getDefaultAudioStream(data.audio_streams) + var audioStream = data.audio_stream this.embeddedCoverArt = data.video_stream ? this.getEmbeddedCoverArt(data.video_stream) : null this.format = data.format this.duration = data.duration diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 31d9ba3f..177d5db0 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -539,5 +539,63 @@ class Scanner { } return false } + + async saveMetadata(audiobookId) { + if (audiobookId) { + var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId) + if (!audiobook) { + return { + error: 'Audiobook not found' + } + } + var savedPath = await audiobook.writeNfoFile() + return { + audiobookId, + audiobookTitle: audiobook.title, + savedPath + } + } else { + var response = { + success: 0, + failed: 0 + } + for (let i = 0; i < this.db.audiobooks.length; i++) { + var audiobook = this.db.audiobooks[i] + var savedPath = await audiobook.writeNfoFile() + if (savedPath) { + Logger.info(`[Scanner] Saved metadata nfo ${savedPath}`) + response.success++ + } else { + response.failed++ + } + } + return response + } + } + + // TEMP: Old version created ids that had a chance of repeating + async fixDuplicateIds() { + var ids = {} + var audiobooksUpdated = 0 + for (let i = 0; i < this.db.audiobooks.length; i++) { + var ab = this.db.audiobooks[i] + if (ids[ab.id]) { + var abCopy = new Audiobook(ab.toJSON()) + abCopy.id = getId('ab') + if (abCopy.book.cover) { + abCopy.book.cover = abCopy.book.cover.replace(ab.id, abCopy.id) + } + Logger.warn('Found duplicate ID - updating from', ab.id, 'to', abCopy.id) + await this.db.removeEntity('audiobook', ab.id) + await this.db.insertEntity('audiobook', abCopy) + audiobooksUpdated++ + } else { + ids[ab.id] = true + } + } + if (audiobooksUpdated) { + Logger.info(`[Scanner] Updated ${audiobooksUpdated} audiobook IDs`) + } + } } module.exports = Scanner \ No newline at end of file diff --git a/server/utils/audioFileScanner.js b/server/utils/audioFileScanner.js deleted file mode 100644 index 05d4d8e4..00000000 --- a/server/utils/audioFileScanner.js +++ /dev/null @@ -1,317 +0,0 @@ -const Path = require('path') -const Logger = require('../Logger') -const prober = require('./prober') - -const ImageCodecs = ['mjpeg', 'jpeg', 'png'] - -function getDefaultAudioStream(audioStreams) { - if (audioStreams.length === 1) return audioStreams[0] - var defaultStream = audioStreams.find(a => a.is_default) - if (!defaultStream) return audioStreams[0] - return defaultStream -} - -async function scan(path, verbose = false) { - Logger.debug(`Scanning path "${path}"`) - var probeData = await prober.probe(path, verbose) - if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) { - return { - error: 'Invalid audio file' - } - } - if (!probeData.duration || !probeData.size) { - return { - error: 'Invalid duration or size' - } - } - var audioStream = getDefaultAudioStream(probeData.audio_streams) - - const finalData = { - format: probeData.format, - duration: probeData.duration, - size: probeData.size, - bit_rate: audioStream.bit_rate || probeData.bit_rate, - codec: audioStream.codec, - time_base: audioStream.time_base, - language: audioStream.language, - channel_layout: audioStream.channel_layout, - channels: audioStream.channels, - sample_rate: audioStream.sample_rate, - chapters: probeData.chapters || [] - } - - var hasCoverArt = probeData.video_stream ? ImageCodecs.includes(probeData.video_stream.codec) : false - if (hasCoverArt) { - finalData.embedded_cover_art = probeData.video_stream.codec - } - - for (const key in probeData) { - if (probeData[key] && key.startsWith('file_tag')) { - finalData[key] = probeData[key] - } - } - - if (finalData.file_tag_track) { - var track = finalData.file_tag_track - var trackParts = track.split('/').map(part => Number(part)) - if (trackParts.length > 0) { - finalData.trackNumber = trackParts[0] - } - if (trackParts.length > 1) { - finalData.trackTotal = trackParts[1] - } - } - - if (verbose && probeData.rawTags) { - finalData.rawTags = probeData.rawTags - } - - return finalData -} -module.exports.scan = scan - - -function isNumber(val) { - return !isNaN(val) && val !== null -} - -function getTrackNumberFromMeta(scanData) { - return !isNaN(scanData.trackNumber) && scanData.trackNumber !== null ? Math.trunc(Number(scanData.trackNumber)) : null -} - -function getTrackNumberFromFilename(title, author, series, publishYear, filename) { - var partbasename = Path.basename(filename, Path.extname(filename)) - - // Remove title, author, series, and publishYear from filename if there - if (title) partbasename = partbasename.replace(title, '') - if (author) partbasename = partbasename.replace(author, '') - if (series) partbasename = partbasename.replace(series, '') - if (publishYear) partbasename = partbasename.replace(publishYear) - - // Remove eg. "disc 1" from path - partbasename = partbasename.replace(/\bdisc \d\d?\b/i, '') - - // Remove "cd01" or "cd 01" from path - partbasename = partbasename.replace(/\bcd ?\d\d?\b/i, '') - - var numbersinpath = partbasename.match(/\d{1,4}/g) - if (!numbersinpath) return null - - var number = numbersinpath.length ? parseInt(numbersinpath[0]) : null - return number -} - -function getCdNumberFromFilename(title, author, series, publishYear, filename) { - var partbasename = Path.basename(filename, Path.extname(filename)) - - // Remove title, author, series, and publishYear from filename if there - if (title) partbasename = partbasename.replace(title, '') - if (author) partbasename = partbasename.replace(author, '') - if (series) partbasename = partbasename.replace(series, '') - if (publishYear) partbasename = partbasename.replace(publishYear) - - var cdNumber = null - - var cdmatch = partbasename.match(/\b(disc|cd) ?(\d\d?)\b/i) - if (cdmatch && cdmatch.length > 2 && cdmatch[2]) { - if (!isNaN(cdmatch[2])) { - cdNumber = Number(cdmatch[2]) - } - } - - return cdNumber -} - -async function scanAudioFiles(audiobook, newAudioFiles) { - if (!newAudioFiles || !newAudioFiles.length) { - Logger.error('[AudioFileScanner] Scan Audio Files no new files', audiobook.title) - return - } - - Logger.debug('[AudioFileScanner] Scanning audio files') - - var tracks = [] - var numDuplicateTracks = 0 - var numInvalidTracks = 0 - - for (let i = 0; i < newAudioFiles.length; i++) { - var audioFile = newAudioFiles[i] - var scanData = await scan(audioFile.fullPath) - if (!scanData || scanData.error) { - Logger.error('[AudioFileScanner] Scan failed for', audioFile.path) - continue; - } - - var trackNumFromMeta = getTrackNumberFromMeta(scanData) - var book = audiobook.book || {} - - var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename) - - var cdNumFromFilename = getCdNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename) - - // IF CD num was found but no track num - USE cd num as track num - if (!trackNumFromFilename && cdNumFromFilename) { - trackNumFromFilename = cdNumFromFilename - cdNumFromFilename = null - } - - var audioFileObj = { - ino: audioFile.ino, - filename: audioFile.filename, - path: audioFile.path, - fullPath: audioFile.fullPath, - ext: audioFile.ext, - ...scanData, - trackNumFromMeta, - trackNumFromFilename, - cdNumFromFilename - } - var audioFile = audiobook.addAudioFile(audioFileObj) - - var trackNumber = 1 - if (newAudioFiles.length > 1) { - trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename - if (trackNumber === null) { - Logger.debug('[AudioFileScanner] Invalid track number for', audioFile.filename) - audioFile.invalid = true - audioFile.error = 'Failed to get track number' - numInvalidTracks++ - continue; - } - } - - if (tracks.find(t => t.index === trackNumber)) { - // Logger.debug('[AudioFileScanner] Duplicate track number for', audioFile.filename) - audioFile.invalid = true - audioFile.error = 'Duplicate track number' - numDuplicateTracks++ - continue; - } - - audioFile.index = trackNumber - tracks.push(audioFile) - } - - if (!tracks.length) { - Logger.warn('[AudioFileScanner] No Tracks for audiobook', audiobook.id) - return - } - - if (numDuplicateTracks > 0) { - Logger.warn(`[AudioFileScanner] ${numDuplicateTracks} Duplicate tracks for "${audiobook.title}"`) - } - if (numInvalidTracks > 0) { - Logger.error(`[AudioFileScanner] ${numDuplicateTracks} Invalid tracks for "${audiobook.title}"`) - } - - tracks.sort((a, b) => a.index - b.index) - - audiobook.audioFiles.sort((a, b) => { - var aNum = isNumber(a.trackNumFromMeta) ? a.trackNumFromMeta : isNumber(a.trackNumFromFilename) ? a.trackNumFromFilename : 0 - var bNum = isNumber(b.trackNumFromMeta) ? b.trackNumFromMeta : isNumber(b.trackNumFromFilename) ? b.trackNumFromFilename : 0 - return aNum - bNum - }) - - // If first index is 0, increment all by 1 - if (tracks[0].index === 0) { - tracks = tracks.map(t => { - t.index += 1 - return t - }) - } - - var hasTracksAlready = audiobook.tracks.length - tracks.forEach((track) => { - audiobook.addTrack(track) - }) - if (hasTracksAlready) { - audiobook.tracks.sort((a, b) => a.index - b.index) - } -} -module.exports.scanAudioFiles = scanAudioFiles - - -async function rescanAudioFiles(audiobook) { - var audioFiles = audiobook.audioFiles - var updates = 0 - - for (let i = 0; i < audioFiles.length; i++) { - var audioFile = audioFiles[i] - var scanData = await scan(audioFile.fullPath) - if (!scanData || scanData.error) { - Logger.error('[AudioFileScanner] Scan failed for', audioFile.path) - // audiobook.invalidAudioFiles.push(parts[i]) - continue; - } - - var trackNumFromMeta = getTrackNumberFromMeta(scanData) - var book = audiobook.book || {} - - var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename) - - var cdNumFromFilename = getCdNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename) - - // IF CD num was found but no track num - USE cd num as track num - if (!trackNumFromFilename && cdNumFromFilename) { - trackNumFromFilename = cdNumFromFilename - cdNumFromFilename = null - } - - var metadataUpdate = { - ...scanData, - trackNumFromMeta, - trackNumFromFilename, - cdNumFromFilename - } - var hasUpdates = audioFile.updateMetadata(metadataUpdate) - if (hasUpdates) { - // Sync audio track with audio file - var matchingAudioTrack = audiobook.tracks.find(t => t.ino === audioFile.ino) - if (matchingAudioTrack) { - matchingAudioTrack.syncMetadata(audioFile) - } else if (!audioFile.exclude) { // If audio file is not excluded then it should have an audio track - - // Fallback to checking path - matchingAudioTrack = audiobook.tracks.find(t => t.path === audioFile.path) - if (matchingAudioTrack) { - Logger.error(`[AudioFileScanner] Audio File mismatch ino with audio track "${audioFile.filename}"`) - matchingAudioTrack.ino = audioFile.ino - matchingAudioTrack.syncMetadata(audioFile) - } else { - Logger.error(`[AudioFileScanner] Audio File has no matching Track ${audioFile.filename} for "${audiobook.title}"`) - - // Exclude audio file to prevent further errors - // audioFile.exclude = true - } - } - updates++ - } - } - - return updates -} -module.exports.rescanAudioFiles = rescanAudioFiles - -async function scanTrackNumbers(audiobook) { - var tracks = audiobook.tracks || [] - var scannedTrackNumData = [] - for (let i = 0; i < tracks.length; i++) { - var track = tracks[i] - var scanData = await scan(track.fullPath, true) - - var trackNumFromMeta = getTrackNumberFromMeta(scanData) - var book = audiobook.book || {} - var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, track.filename) - Logger.info(`[AudioFileScanner] Track # for "${track.filename}", Metadata: "${trackNumFromMeta}", Filename: "${trackNumFromFilename}", Current: "${track.index}"`) - scannedTrackNumData.push({ - filename: track.filename, - currentTrackNum: track.index, - trackNumFromFilename, - trackNumFromMeta, - scanDataTrackNum: scanData.file_tag_track, - rawTags: scanData.rawTags || null - }) - } - return scannedTrackNumData -} -module.exports.scanTrackNumbers = scanTrackNumbers \ No newline at end of file diff --git a/server/utils/prober.js b/server/utils/prober.js index 974eca07..d8b48827 100644 --- a/server/utils/prober.js +++ b/server/utils/prober.js @@ -98,6 +98,7 @@ function parseMediaStreamInfo(stream, all_streams, total_bit_rate) { language: tryGrabTag(stream, 'language'), title: tryGrabTag(stream, 'title') } + if (stream.tags) info.tags = stream.tags if (info.type === 'audio' || info.type === 'subtitle') { var disposition = stream.disposition || {} @@ -188,6 +189,14 @@ function parseTags(format, verbose) { return tags } +function getDefaultAudioStream(audioStreams) { + if (!audioStreams || !audioStreams.length) return null + if (audioStreams.length === 1) return audioStreams[0] + var defaultStream = audioStreams.find(a => a.is_default) + if (!defaultStream) return audioStreams[0] + return defaultStream +} + function parseProbeData(data, verbose = false) { try { var { format, streams, chapters } = data @@ -212,17 +221,26 @@ function parseProbeData(data, verbose = false) { const cleaned_streams = streams.map(s => parseMediaStreamInfo(s, streams, cleanedData.bit_rate)) cleanedData.video_stream = cleaned_streams.find(s => s.type === 'video') - cleanedData.audio_streams = cleaned_streams.filter(s => s.type === 'audio') - cleanedData.subtitle_streams = cleaned_streams.filter(s => s.type === 'subtitle') + var audioStreams = cleaned_streams.filter(s => s.type === 'audio') + cleanedData.audio_stream = getDefaultAudioStream(audioStreams) - if (cleanedData.audio_streams.length && cleanedData.video_stream) { + if (cleanedData.audio_stream && cleanedData.video_stream) { var videoBitrate = cleanedData.video_stream.bit_rate // If audio stream bitrate larger then video, most likely incorrect - if (cleanedData.audio_streams.find(astream => astream.bit_rate > videoBitrate)) { + if (cleanedData.audio_stream.bit_rate > videoBitrate) { cleanedData.video_stream.bit_rate = cleanedData.bit_rate } } + // If format does not have tags, check audio stream (https://github.com/advplyr/audiobookshelf/issues/256) + if (!format.tags && cleanedData.audio_stream && cleanedData.audio_stream.tags) { + var tags = parseTags(cleanedData.audio_stream) + cleanedData = { + ...cleanedData, + ...tags + } + } + cleanedData.chapters = parseChapters(chapters) return cleanedData @@ -232,22 +250,8 @@ function parseProbeData(data, verbose = false) { } } -function probe(filepath, verbose = false) { - return new Promise((resolve) => { - Ffmpeg.ffprobe(filepath, ['-show_chapters'], (err, raw) => { - if (err) { - console.error(err) - resolve(null) - } else { - resolve(parseProbeData(raw, verbose)) - } - }) - }) -} -module.exports.probe = probe - // Updated probe returns AudioProbeData object -function probe2(filepath, verbose = false) { +function probe(filepath, verbose = false) { return new Promise((resolve) => { Ffmpeg.ffprobe(filepath, ['-show_chapters'], (err, raw) => { if (err) { @@ -258,7 +262,7 @@ function probe2(filepath, verbose = false) { }) } else { var rawProbeData = parseProbeData(raw, verbose) - if (!rawProbeData || !rawProbeData.audio_streams.length) { + if (!rawProbeData || !rawProbeData.audio_stream) { resolve({ error: rawProbeData ? 'Invalid audio file: no audio streams found' : 'Probe Failed' }) @@ -271,4 +275,4 @@ function probe2(filepath, verbose = false) { }) }) } -module.exports.probe2 = probe2 \ No newline at end of file +module.exports.probe = probe \ No newline at end of file