audiobookshelf/server/Scanner.js

249 lines
8.7 KiB
JavaScript
Raw Normal View History

const Path = require('path')
2021-08-18 00:01:11 +02:00
const Logger = require('./Logger')
const BookFinder = require('./BookFinder')
const Audiobook = require('./Audiobook')
const audioFileScanner = require('./utils/audioFileScanner')
const { getAllAudiobookFiles } = require('./utils/scandir')
const { secondsToTimestamp } = require('./utils/fileUtils')
class Scanner {
constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) {
this.AudiobookPath = AUDIOBOOK_PATH
this.MetadataPath = METADATA_PATH
this.db = db
this.emitter = emitter
this.cancelScan = false
2021-08-18 00:01:11 +02:00
this.bookFinder = new BookFinder()
}
get audiobooks() {
return this.db.audiobooks
}
async scan() {
// TEMP - fix relative file paths
if (this.audiobooks.length) {
for (let i = 0; i < this.audiobooks.length; i++) {
2021-08-24 15:04:32 +02:00
var ab = this.audiobooks[i]
if (ab.fixRelativePath(this.AudiobookPath)) {
await this.db.updateAudiobook(ab)
}
}
}
2021-08-18 00:01:11 +02:00
const scanStart = Date.now()
var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath)
if (this.cancelScan) {
this.cancelScan = false
return null
}
var scanResults = {
removed: 0,
updated: 0,
added: 0
}
// Check for removed audiobooks
for (let i = 0; i < this.audiobooks.length; i++) {
var dataFound = audiobookDataFound.find(abd => abd.path === this.audiobooks[i].path)
if (!dataFound) {
Logger.info(`[Scanner] Removing audiobook "${this.audiobooks[i].title}" - no longer in dir`)
await this.db.removeEntity('audiobook', this.audiobooks[i].id)
if (!this.audiobooks[i]) {
Logger.error('[Scanner] Oops... audiobook is now invalid...')
continue;
}
scanResults.removed++
this.emitter('audiobook_removed', this.audiobooks[i].toJSONMinified())
}
if (this.cancelScan) {
this.cancelScan = false
return null
}
}
2021-08-18 00:01:11 +02:00
for (let i = 0; i < audiobookDataFound.length; i++) {
var audiobookData = audiobookDataFound[i]
var existingAudiobook = this.audiobooks.find(a => a.fullPath === audiobookData.fullPath)
if (existingAudiobook) {
Logger.debug(`[Scanner] Audiobook already added, check updates for "${existingAudiobook.title}"`)
if (!audiobookData.parts.length) {
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`)
await this.db.removeEntity('audiobook', existingAudiobook.id)
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
scanResults.removed++
} else {
// Check for audio files that were removed
var removedAudioFiles = existingAudiobook.audioFiles.filter(file => !audiobookData.parts.includes(file.filename))
if (removedAudioFiles.length) {
Logger.info(`[Scanner] ${removedAudioFiles.length} audio files removed for audiobook "${existingAudiobook.title}"`)
removedAudioFiles.forEach((af) => existingAudiobook.removeAudioFile(af))
}
// Check for audio files that were added
var newParts = audiobookData.parts.filter(part => !existingAudiobook.audioPartExists(part))
if (newParts.length) {
Logger.info(`[Scanner] ${newParts.length} new audio parts were found for audiobook "${existingAudiobook.title}"`)
// If previously invalid part, remove from invalid list because it will be re-scanned
newParts.forEach((part) => {
if (existingAudiobook.invalidParts.includes(part)) {
existingAudiobook.invalidParts = existingAudiobook.invalidParts.filter(p => p !== part)
}
})
// Scan new audio parts found
await audioFileScanner.scanParts(existingAudiobook, newParts)
}
if (!existingAudiobook.tracks.length) {
Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`)
await this.db.removeEntity('audiobook', existingAudiobook.id)
this.emitter('audiobook_removed', existingAudiobook.toJSONMinified())
} else {
var hasUpdates = removedAudioFiles.length || newParts.length
if (existingAudiobook.checkUpdateMissingParts()) {
Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`)
hasUpdates = true
}
if (existingAudiobook.syncOtherFiles(audiobookData.otherFiles)) {
hasUpdates = true
}
if (audiobookData.author && existingAudiobook.syncAuthorNames(audiobookData)) {
Logger.info(`[Scanner] "${existingAudiobook.title}" author names updated, "${existingAudiobook.authorLF}"`)
hasUpdates = true
}
if (hasUpdates) {
Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`)
existingAudiobook.lastUpdate = Date.now()
await this.db.updateAudiobook(existingAudiobook)
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
scanResults.updated++
}
}
} // end if update existing
2021-08-18 00:01:11 +02:00
} else {
if (!audiobookData.parts.length) {
Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData)
2021-08-18 00:01:11 +02:00
} else {
var audiobook = new Audiobook()
audiobook.setData(audiobookData)
await audioFileScanner.scanParts(audiobook, audiobookData.parts)
if (!audiobook.tracks.length) {
Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title)
2021-08-18 00:01:11 +02:00
} else {
audiobook.checkUpdateMissingParts()
Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`)
2021-08-18 00:01:11 +02:00
await this.db.insertAudiobook(audiobook)
this.emitter('audiobook_added', audiobook.toJSONMinified())
scanResults.added++
2021-08-18 00:01:11 +02:00
}
} // end if add new
2021-08-18 00:01:11 +02:00
}
var progress = Math.round(100 * (i + 1) / audiobookDataFound.length)
this.emitter('scan_progress', {
scanType: 'files',
progress: {
total: audiobookDataFound.length,
done: i + 1,
progress
}
})
if (this.cancelScan) {
this.cancelScan = false
break
}
2021-08-18 00:01:11 +02:00
}
const scanElapsed = Math.floor((Date.now() - scanStart) / 1000)
Logger.info(`[Scanned] Finished | ${scanResults.added} added | ${scanResults.updated} updated | ${scanResults.removed} removed | elapsed: ${secondsToTimestamp(scanElapsed)}`)
return scanResults
2021-08-18 00:01:11 +02:00
}
async fetchMetadata(id, trackIndex = 0) {
var audiobook = this.audiobooks.find(a => a.id === id)
if (!audiobook) {
return false
}
var tracks = audiobook.tracks
var index = isNaN(trackIndex) ? 0 : Number(trackIndex)
var firstTrack = tracks[index]
var firstTrackFullPath = firstTrack.fullPath
var scanResult = await audioFileScanner.scan(firstTrackFullPath)
return scanResult
}
async scanCovers() {
var audiobooksNeedingCover = this.audiobooks.filter(ab => !ab.cover && ab.author)
var found = 0
var notFound = 0
for (let i = 0; i < audiobooksNeedingCover.length; i++) {
var audiobook = audiobooksNeedingCover[i]
var options = {
titleDistance: 2,
authorDistance: 2
}
var results = await this.bookFinder.findCovers('openlibrary', audiobook.title, audiobook.author, options)
if (results.length) {
Logger.info(`[Scanner] Found best cover for "${audiobook.title}"`)
audiobook.book.cover = results[0]
await this.db.updateAudiobook(audiobook)
found++
} else {
notFound++
}
var progress = Math.round(100 * (i + 1) / audiobooksNeedingCover.length)
this.emitter('scan_progress', {
scanType: 'covers',
progress: {
total: audiobooksNeedingCover.length,
done: i + 1,
progress
}
})
if (this.cancelScan) {
this.cancelScan = false
break
}
}
return {
found,
notFound
}
}
2021-08-18 00:01:11 +02:00
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)
2021-08-18 00:01:11 +02:00
}
res.json(result)
}
async findCovers(req, res) {
var query = req.query
var result = await this.bookFinder.findCovers(query.provider, query.title, query.author || null)
res.json(result)
}
2021-08-18 00:01:11 +02:00
}
module.exports = Scanner