mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-08 00:54:33 +01:00
Change:Fallback to audio stream tags if probe format has no tags and remove old scanner #256
This commit is contained in:
parent
3f8551f9a1
commit
a17348f916
@ -73,30 +73,6 @@
|
||||
</li>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
|
||||
<div v-if="showExperimentalFeatures" class="p-4">
|
||||
<ui-btn :loading="checkingTrackNumbers" small @click="checkTrackNumbers">Check Track Numbers</ui-btn>
|
||||
<div v-if="trackNumData && trackNumData.length" class="w-full max-w-4xl py-2">
|
||||
<table class="tracksTable">
|
||||
<tr>
|
||||
<th class="text-left">Filename</th>
|
||||
<th class="w-32">Index</th>
|
||||
<th class="w-32"># From Metadata</th>
|
||||
<th class="w-32"># From Filename</th>
|
||||
<th class="w-32"># From Probe</th>
|
||||
<th class="w-32">Raw Tags</th>
|
||||
</tr>
|
||||
<tr v-for="trackData in trackNumData" :key="trackData.filename">
|
||||
<td class="text-xs">{{ trackData.filename }}</td>
|
||||
<td class="text-center">{{ trackData.currentTrackNum }}</td>
|
||||
<td class="text-center">{{ trackData.trackNumFromMeta }}</td>
|
||||
<td class="text-center">{{ trackData.trackNumFromFilename }}</td>
|
||||
<td class="text-center">{{ trackData.scanDataTrackNum }}</td>
|
||||
<td class="text-left text-xs">{{ JSON.stringify(trackData.rawTags || '') }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -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) {
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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') {
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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
|
||||
module.exports.probe = probe
|
Loading…
Reference in New Issue
Block a user