mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-26 00:14:49 +01:00
Fix: Scanner check path and inode value for removed books, scanner v5 outlined
This commit is contained in:
parent
ea366c00ca
commit
3fa0fe4b64
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.6.25",
|
"version": "1.6.26",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.6.25",
|
"version": "1.6.26",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -454,7 +454,6 @@ class Scanner {
|
|||||||
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
||||||
|
|
||||||
// TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos"
|
// TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos"
|
||||||
// TEMP - update ino for each audiobook
|
|
||||||
if (audiobooksInLibrary.length) {
|
if (audiobooksInLibrary.length) {
|
||||||
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
||||||
var ab = audiobooksInLibrary[i]
|
var ab = audiobooksInLibrary[i]
|
||||||
@ -463,7 +462,7 @@ class Scanner {
|
|||||||
if (shouldUpdateIno) {
|
if (shouldUpdateIno) {
|
||||||
var filesWithMissingIno = ab.getFilesWithMissingIno()
|
var filesWithMissingIno = ab.getFilesWithMissingIno()
|
||||||
|
|
||||||
Logger.debug(`\n\Updating inos for "${ab.title}"`)
|
Logger.debug(`\nUpdating inos for "${ab.title}"`)
|
||||||
Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno)
|
Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno)
|
||||||
|
|
||||||
var hasUpdates = await ab.checkUpdateInos()
|
var hasUpdates = await ab.checkUpdateInos()
|
||||||
@ -504,7 +503,7 @@ class Scanner {
|
|||||||
// Check for removed audiobooks
|
// Check for removed audiobooks
|
||||||
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
for (let i = 0; i < audiobooksInLibrary.length; i++) {
|
||||||
var audiobook = audiobooksInLibrary[i]
|
var audiobook = audiobooksInLibrary[i]
|
||||||
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino)
|
var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path))
|
||||||
if (!dataFound) {
|
if (!dataFound) {
|
||||||
Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`)
|
Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`)
|
||||||
audiobook.isMissing = true
|
audiobook.isMissing = true
|
||||||
|
@ -153,6 +153,17 @@ class AudioFile {
|
|||||||
this.metadata.setData(data)
|
this.metadata.setData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New scanner creates AudioFile from AudioFileScanner
|
||||||
|
setData2(fileData, probeData) {
|
||||||
|
this.index = fileData.index || null
|
||||||
|
this.ino = fileData.ino || null
|
||||||
|
this.filename = fileData.filename
|
||||||
|
this.ext = fileData.ext
|
||||||
|
this.path = fileData.path
|
||||||
|
this.fullPath = fileData.fullPath
|
||||||
|
this.addedAt = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
syncChapters(updatedChapters) {
|
syncChapters(updatedChapters) {
|
||||||
if (this.chapters.length !== updatedChapters.length) {
|
if (this.chapters.length !== updatedChapters.length) {
|
||||||
this.chapters = updatedChapters.map(ch => ({ ...ch }))
|
this.chapters = updatedChapters.map(ch => ({ ...ch }))
|
||||||
|
@ -823,5 +823,141 @@ class Audiobook {
|
|||||||
var audioFile = this.audioFiles[0]
|
var audioFile = this.audioFiles[0]
|
||||||
return this.book.setDetailsFromFileMetadata(audioFile.metadata)
|
return this.book.setDetailsFromFileMetadata(audioFile.metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns null if file not found, true if file was updated, false if up to date
|
||||||
|
checkFileFound(fileFound, isAudioFile) {
|
||||||
|
var hasUpdated = false
|
||||||
|
|
||||||
|
const arrayToCheck = isAudioFile ? this.audioFiles : this.otherFiles
|
||||||
|
|
||||||
|
var existingFile = arrayToCheck.find(_af => _af.ino === fileFound.ino)
|
||||||
|
if (!existingFile) {
|
||||||
|
existingFile = arrayToCheck.find(_af => _af.path === fileFound.path)
|
||||||
|
if (existingFile) {
|
||||||
|
// file inode was updated
|
||||||
|
existingFile.ino = fileFound.ino
|
||||||
|
hasUpdated = true
|
||||||
|
} else {
|
||||||
|
// file not found
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingFile.filename !== fileFound.filename) {
|
||||||
|
existingFile.filename = fileFound.filename
|
||||||
|
existingFile.ext = fileFound.ext
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingFile.path !== fileFound.path) {
|
||||||
|
existingFile.path = fileFound.path
|
||||||
|
existingFile.fullPath = fileFound.fullPath
|
||||||
|
hasUpdated = true
|
||||||
|
} else if (existingFile.fullPath !== fileFound.fullPath) {
|
||||||
|
existingFile.fullPath = fileFound.fullPath
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAudioFile && existingFile.filetype !== fileFound.filetype) {
|
||||||
|
existingFile.filetype = fileFound.filetype
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
checkShouldScan(dataFound) {
|
||||||
|
var hasUpdated = false
|
||||||
|
|
||||||
|
if (dataFound.ino !== this.ino) {
|
||||||
|
this.ino = dataFound.ino
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataFound.folderId !== this.folderId) {
|
||||||
|
Logger.warn(`[Audiobook] Check scan audiobook changed folder ${this.folderId} -> ${dataFound.folderId}`)
|
||||||
|
this.folderId = dataFound.folderId
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataFound.path !== this.path) {
|
||||||
|
Logger.warn(`[Audiobook] Check scan audiobook changed path "${this.path}" -> "${dataFound.path}"`)
|
||||||
|
this.path = dataFound.path
|
||||||
|
this.fullPath = dataFound.fullPath
|
||||||
|
hasUpdated = true
|
||||||
|
} else if (dataFound.fullPath !== this.fullPath) {
|
||||||
|
Logger.warn(`[Audiobook] Check scan audiobook changed fullpath "${this.fullPath}" -> "${dataFound.fullPath}"`)
|
||||||
|
this.fullPath = dataFound.fullPath
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var newAudioFileData = []
|
||||||
|
var newOtherFileData = []
|
||||||
|
|
||||||
|
dataFound.audioFiles.forEach((af) => {
|
||||||
|
var audioFileFoundCheck = this.checkFileFound(af, true)
|
||||||
|
if (audioFileFoundCheck === null) {
|
||||||
|
newAudioFileData.push(af)
|
||||||
|
} else if (audioFileFoundCheck === true) {
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dataFound.otherFiles.forEach((otherFileData) => {
|
||||||
|
var fileFoundCheck = this.checkFileFound(otherFileData, false)
|
||||||
|
if (fileFoundCheck === null) {
|
||||||
|
newOtherFileData.push(otherFileData)
|
||||||
|
} else if (fileFoundCheck === true) {
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const audioFilesRemoved = []
|
||||||
|
const otherFilesRemoved = []
|
||||||
|
|
||||||
|
// inodes will all be up to date at this point
|
||||||
|
this.audioFiles = this.audioFiles.filter(af => {
|
||||||
|
if (!dataFound.audioFiles.find(_af => _af.ino === af.ino)) {
|
||||||
|
audioFilesRemoved.push(af.toJSON())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove all tracks that were associated with removed audio files
|
||||||
|
if (audioFilesRemoved.length) {
|
||||||
|
const audioFilesRemovedInodes = audioFilesRemoved.map(afr => afr.ino)
|
||||||
|
this.tracks = this.tracks.filter(t => !audioFilesRemovedInodes.includes(t.ino))
|
||||||
|
this.checkUpdateMissingParts()
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.otherFiles = this.otherFiles.filter(otherFile => {
|
||||||
|
if (!dataFound.otherFiles.find(_otherFile => _otherFile.ino === otherFile.ino)) {
|
||||||
|
otherFilesRemoved.push(otherFile.toJSON())
|
||||||
|
|
||||||
|
// Check remove cover
|
||||||
|
if (otherFile.fullPath === this.book.coverFullPath) {
|
||||||
|
Logger.debug(`[Audiobook] "${this.title}" Check scan book cover removed`)
|
||||||
|
this.book.removeCover()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (otherFilesRemoved.length) {
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updated: hasUpdated,
|
||||||
|
newAudioFileData,
|
||||||
|
newOtherFileData,
|
||||||
|
audioFilesRemoved,
|
||||||
|
otherFilesRemoved
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Audiobook
|
module.exports = Audiobook
|
22
server/scanner/AudioFileScanner.js
Normal file
22
server/scanner/AudioFileScanner.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const AudioFile = require('../objects/AudioFile')
|
||||||
|
const AudioProbeData = require('./AudioProbeData')
|
||||||
|
|
||||||
|
const prober = require('../utils/prober')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class AudioFileScanner {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async scan(audioFileData, verbose = false) {
|
||||||
|
var probeData = await prober.probe2(audioFileData.fullPath, verbose)
|
||||||
|
if (probeData.error) {
|
||||||
|
Logger.error(`[AudioFileScanner] ${probeData.error} : "${audioFileData.fullPath}"`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
var audioFile = new AudioFile()
|
||||||
|
// TODO: Build audio file
|
||||||
|
return audioFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new AudioFileScanner()
|
74
server/scanner/AudioProbeData.js
Normal file
74
server/scanner/AudioProbeData.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
const AudioFileMetadata = require('../objects/AudioFileMetadata')
|
||||||
|
|
||||||
|
class AudioProbeData {
|
||||||
|
constructor() {
|
||||||
|
this.embeddedCoverArt = null
|
||||||
|
this.format = null
|
||||||
|
this.duration = null
|
||||||
|
this.size = null
|
||||||
|
this.bitRate = null
|
||||||
|
this.codec = null
|
||||||
|
this.timeBase = null
|
||||||
|
this.language = null
|
||||||
|
this.channelLayout = null
|
||||||
|
this.channels = null
|
||||||
|
this.sampleRate = null
|
||||||
|
this.chapters = []
|
||||||
|
|
||||||
|
this.audioFileMetadata = null
|
||||||
|
|
||||||
|
this.trackNumber = null
|
||||||
|
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 = getDefaultAudioStream(data.audio_streams)
|
||||||
|
|
||||||
|
this.embeddedCoverArt = data.video_stream ? this.getEmbeddedCoverArt(data.video_stream) : false
|
||||||
|
this.format = data.format
|
||||||
|
this.duration = data.duration
|
||||||
|
this.size = data.size
|
||||||
|
this.bitRate = audioStream.bit_rate || data.bit_rate
|
||||||
|
this.codec = audioStream.codec
|
||||||
|
this.timeBase = audioStream.time_base
|
||||||
|
this.language = audioStream.language
|
||||||
|
this.channelLayout = audioStream.channel_layout
|
||||||
|
this.channels = audioStream.channels
|
||||||
|
this.sampleRate = audioStream.sample_rate
|
||||||
|
this.chapters = data.chapters || []
|
||||||
|
|
||||||
|
var metatags = {}
|
||||||
|
for (const key in data) {
|
||||||
|
if (data[key] && key.startsWith('file_tag')) {
|
||||||
|
metatags[key] = data[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.audioFileMetadata = new AudioFileMetadata()
|
||||||
|
this.audioFileMetadata.setData(metatags)
|
||||||
|
|
||||||
|
// Track ID3 tag might be "3/10" or just "3"
|
||||||
|
if (this.audioFileMetadata.tagTrack) {
|
||||||
|
var trackParts = this.audioFileMetadata.tagTrack.split('/').map(part => Number(part))
|
||||||
|
if (trackParts.length > 0) {
|
||||||
|
this.trackNumber = trackParts[0]
|
||||||
|
}
|
||||||
|
if (trackParts.length > 1) {
|
||||||
|
this.trackTotal = trackParts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = AudioProbeData
|
34
server/scanner/LibraryScan.js
Normal file
34
server/scanner/LibraryScan.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const Folder = require('../objects/Folder')
|
||||||
|
|
||||||
|
const { getId } = require('../utils/index')
|
||||||
|
|
||||||
|
class LibraryScan {
|
||||||
|
constructor() {
|
||||||
|
this.id = null
|
||||||
|
this.libraryId = null
|
||||||
|
this.libraryName = null
|
||||||
|
this.folders = null
|
||||||
|
|
||||||
|
this.scanOptions = null
|
||||||
|
|
||||||
|
this.startedAt = null
|
||||||
|
this.finishedAt = null
|
||||||
|
|
||||||
|
this.folderScans = []
|
||||||
|
}
|
||||||
|
|
||||||
|
get _scanOptions() { return this.scanOptions || {} }
|
||||||
|
get forceRescan() { return !!this._scanOptions.forceRescan }
|
||||||
|
|
||||||
|
setData(library, scanOptions) {
|
||||||
|
this.id = getId('lscan')
|
||||||
|
this.libraryId = library.id
|
||||||
|
this.libraryName = library.name
|
||||||
|
this.folders = library.folders.map(folder => Folder(folder.toJSON()))
|
||||||
|
|
||||||
|
this.scanOptions = scanOptions
|
||||||
|
|
||||||
|
this.startedAt = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = LibraryScan
|
68
server/scanner/ScanOptions.js
Normal file
68
server/scanner/ScanOptions.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
const { CoverDestination } = require('../utils/constants')
|
||||||
|
|
||||||
|
class ScanOptions {
|
||||||
|
constructor(options) {
|
||||||
|
this.forceRescan = false
|
||||||
|
|
||||||
|
this.metadataPrecedence = [
|
||||||
|
{
|
||||||
|
id: 'directory',
|
||||||
|
include: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reader-desc-txt',
|
||||||
|
include: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audio-file-metadata',
|
||||||
|
include: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'metadata-opf',
|
||||||
|
include: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'external-source',
|
||||||
|
include: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Server settings
|
||||||
|
this.parseSubtitles = false
|
||||||
|
this.findCovers = false
|
||||||
|
this.coverDestination = CoverDestination.METADATA
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
this.construct(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
construct(options) {
|
||||||
|
for (const key in options) {
|
||||||
|
if (key === 'metadataPrecedence' && options[key].length) {
|
||||||
|
this.metadataPrecedence = [...options[key]]
|
||||||
|
} else if (this[key] !== undefined) {
|
||||||
|
this[key] = options[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
forceRescan: this.forceRescan,
|
||||||
|
metadataPrecedence: this.metadataPrecedence,
|
||||||
|
parseSubtitles: this.parseSubtitles,
|
||||||
|
findCovers: this.findCovers,
|
||||||
|
coverDestination: this.coverDestination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(options, serverSettings) {
|
||||||
|
this.forceRescan = !!options.forceRescan
|
||||||
|
|
||||||
|
this.parseSubtitles = !!serverSettings.scannerParseSubtitle
|
||||||
|
this.findCovers = !!serverSettings.scannerFindCovers
|
||||||
|
this.coverDestination = serverSettings.coverDestination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = ScanOptions
|
172
server/scanner/Scanner.js
Normal file
172
server/scanner/Scanner.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
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 } = require('../utils/index')
|
||||||
|
const { secondsToTimestamp } = require('../utils/fileUtils')
|
||||||
|
const { ScanResult, CoverDestination } = require('../utils/constants')
|
||||||
|
|
||||||
|
const AudioFileScanner = require('./AudioFileScanner')
|
||||||
|
const BookFinder = require('../BookFinder')
|
||||||
|
const Audiobook = require('../objects/Audiobook')
|
||||||
|
const LibraryScan = require('./LibraryScan')
|
||||||
|
const ScanOptions = require('./ScanOptions')
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
async scan(libraryId, options = {}) {
|
||||||
|
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 scanOptions = new ScanOptions()
|
||||||
|
scanOptions.setData(options, this.db.serverSettings)
|
||||||
|
|
||||||
|
var libraryScan = new LibraryScan()
|
||||||
|
libraryScan.setData(library, scanOptions)
|
||||||
|
|
||||||
|
Logger.info(`[Scanner] Starting library scan ${libraryScan.id} for ${libraryScan.libraryName}`)
|
||||||
|
|
||||||
|
var results = await this.scanLibrary(libraryScan)
|
||||||
|
|
||||||
|
Logger.info(`[Scanner] Library scan ${libraryScan.id} complete`)
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanLibrary(libraryScan) {
|
||||||
|
var audiobookDataFound = []
|
||||||
|
for (let i = 0; i < libraryScan.folders.length; i++) {
|
||||||
|
var folder = libraryScan.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)
|
||||||
|
|
||||||
|
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryScan.libraryId)
|
||||||
|
|
||||||
|
const audiobooksToUpdate = []
|
||||||
|
const audiobooksToRescan = []
|
||||||
|
const newAudiobookData = []
|
||||||
|
|
||||||
|
// Check for existing & 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++
|
||||||
|
audiobooksToUpdate.push(audiobook)
|
||||||
|
} else {
|
||||||
|
var checkRes = audiobook.checkShouldRescan(dataFound)
|
||||||
|
if (checkRes.newAudioFileData.length || checkRes.newOtherFileData.length) {
|
||||||
|
// existing audiobook has new files
|
||||||
|
checkRes.audiobook = audiobook
|
||||||
|
audiobooksToRescan.push(checkRes)
|
||||||
|
} else if (checkRes.updated) {
|
||||||
|
audiobooksToUpdate.push(audiobook)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove this abf
|
||||||
|
audiobookDataFound = audiobookDataFound.filter(abf => abf.ino !== dataFound.ino)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potential NEW Audiobooks
|
||||||
|
for (let i = 0; i < audiobookDataFound.length; i++) {
|
||||||
|
var dataFound = audiobookDataFound[i]
|
||||||
|
var hasEbook = dataFound.otherFiles.find(otherFile => otherFile.filetype === 'ebook')
|
||||||
|
if (!hasEbook && !dataFound.audioFiles.length) {
|
||||||
|
Logger.info(`[Scanner] Directory found "${audiobookDataFound.path}" has no ebook or audio files`)
|
||||||
|
} else {
|
||||||
|
newAudiobookData.push(dataFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rescans = []
|
||||||
|
for (let i = 0; i < audiobooksToRescan.length; i++) {
|
||||||
|
var rescan = this.rescanAudiobook(audiobooksToRescan[i])
|
||||||
|
rescans.push(rescan)
|
||||||
|
}
|
||||||
|
var newscans = []
|
||||||
|
for (let i = 0; i < newAudiobookData.length; i++) {
|
||||||
|
var newscan = this.scanNewAudiobook(newAudiobookData[i])
|
||||||
|
newscans.push(newscan)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rescanResults = await Promise.all(rescans)
|
||||||
|
|
||||||
|
var newscanResults = await Promise.all(newscans)
|
||||||
|
|
||||||
|
// TODO: Return report
|
||||||
|
return {
|
||||||
|
updates: 0,
|
||||||
|
additions: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return scan result payload
|
||||||
|
async rescanAudiobook(audiobookCheckData) {
|
||||||
|
const { newAudioFileData, newOtherFileData, audiobook } = audiobookCheckData
|
||||||
|
if (newAudioFileData.length) {
|
||||||
|
var newAudioFiles = await this.scanAudioFiles(newAudioFileData)
|
||||||
|
// TODO: Update audiobook tracks
|
||||||
|
}
|
||||||
|
if (newOtherFileData.length) {
|
||||||
|
// TODO: Check other files
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updated: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanNewAudiobook(audiobookData) {
|
||||||
|
// TODO: Return new audiobook
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanAudioFiles(audioFileData) {
|
||||||
|
var proms = []
|
||||||
|
for (let i = 0; i < audioFileData.length; i++) {
|
||||||
|
var prom = AudioFileScanner.scan(audioFileData[i])
|
||||||
|
proms.push(prom)
|
||||||
|
}
|
||||||
|
return Promise.all(proms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = Scanner
|
@ -13,7 +13,7 @@ function getDefaultAudioStream(audioStreams) {
|
|||||||
|
|
||||||
async function scan(path, verbose = false) {
|
async function scan(path, verbose = false) {
|
||||||
Logger.debug(`Scanning path "${path}"`)
|
Logger.debug(`Scanning path "${path}"`)
|
||||||
var probeData = await prober(path, verbose)
|
var probeData = await prober.probe(path, verbose)
|
||||||
if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) {
|
if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) {
|
||||||
return {
|
return {
|
||||||
error: 'Invalid audio file'
|
error: 'Invalid audio file'
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
var Ffmpeg = require('fluent-ffmpeg')
|
var Ffmpeg = require('fluent-ffmpeg')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
|
||||||
|
const AudioProbeData = require('../scanner/AudioProbeData')
|
||||||
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
||||||
@ -241,4 +244,31 @@ function probe(filepath, verbose = false) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
module.exports = probe
|
module.exports.probe = probe
|
||||||
|
|
||||||
|
// Updated probe returns AudioProbeData object
|
||||||
|
function probe2(filepath, verbose = false) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Ffmpeg.ffprobe(filepath, ['-show_chapters'], (err, raw) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
var errorMsg = err ? err.message : null
|
||||||
|
resolve({
|
||||||
|
error: errorMsg || 'Probe Error'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
var rawProbeData = parseProbeData(raw, verbose)
|
||||||
|
if (!rawProbeData || !rawProbeData.audio_streams.length) {
|
||||||
|
resolve({
|
||||||
|
error: rawProbeData ? 'Invalid audio file: no audio streams found' : 'Probe Failed'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
var probeData = new AudioProbeData()
|
||||||
|
probeData.setData(rawProbeData)
|
||||||
|
resolve(probeData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
module.exports.probe2 = probe2
|
Loading…
Reference in New Issue
Block a user