Updates to new library scanner and adding jsdoc types

This commit is contained in:
advplyr 2023-08-27 17:19:57 -05:00
parent ea1d051cfb
commit 2c8448d147
10 changed files with 416 additions and 16 deletions

View File

@ -16,11 +16,10 @@ const Logger = require('./Logger')
const Auth = require('./Auth') const Auth = require('./Auth')
const Watcher = require('./Watcher') const Watcher = require('./Watcher')
const Scanner = require('./scanner/Scanner') const Scanner = require('./scanner/Scanner')
const LibraryScanner = require('./scanner/LibraryScanner')
const Database = require('./Database') const Database = require('./Database')
const SocketAuthority = require('./SocketAuthority') const SocketAuthority = require('./SocketAuthority')
const routes = require('./routes/index')
const ApiRouter = require('./routers/ApiRouter') const ApiRouter = require('./routers/ApiRouter')
const HlsRouter = require('./routers/HlsRouter') const HlsRouter = require('./routers/HlsRouter')
@ -78,6 +77,7 @@ class Server {
this.rssFeedManager = new RssFeedManager() this.rssFeedManager = new RssFeedManager()
this.scanner = new Scanner(this.coverManager, this.taskManager) this.scanner = new Scanner(this.coverManager, this.taskManager)
this.libraryScanner = new LibraryScanner(this.coverManager, this.taskManager)
this.cronManager = new CronManager(this.scanner, this.podcastManager) this.cronManager = new CronManager(this.scanner, this.podcastManager)
// Routers // Routers

View File

@ -977,6 +977,8 @@ class LibraryController {
} }
res.sendStatus(200) res.sendStatus(200)
await this.scanner.scan(req.library, options) await this.scanner.scan(req.library, options)
// TODO: New library scanner
// await this.libraryScanner.scan(req.library, options)
await Database.resetLibraryIssuesFilterData(req.library.id) await Database.resetLibraryIssuesFilterData(req.library.id)
Logger.info('[LibraryController] Scan complete') Logger.info('[LibraryController] Scan complete')
} }

View File

@ -18,6 +18,31 @@ const Logger = require('../Logger')
* @property {string} title * @property {string} title
*/ */
/**
* @typedef AudioFileObject
* @property {number} index
* @property {string} ino
* @property {{filename:string, ext:string, path:string, relPath:string, size:number, mtimeMs:number, ctimeMs:number, birthtimeMs:number}} metadata
* @property {number} addedAt
* @property {number} updatedAt
* @property {number} trackNumFromMeta
* @property {number} discNumFromMeta
* @property {number} trackNumFromFilename
* @property {number} discNumFromFilename
* @property {boolean} manuallyVerified
* @property {string} format
* @property {number} duration
* @property {number} bitRate
* @property {string} language
* @property {string} codec
* @property {string} timeBase
* @property {number} channels
* @property {string} channelLayout
* @property {ChapterObject[]} chapters
* @property {Object} metaTags
* @property {string} mimeType
*/
class Book extends Model { class Book extends Model {
constructor(values, options) { constructor(values, options) {
super(values, options) super(values, options)
@ -52,7 +77,7 @@ class Book extends Model {
this.duration this.duration
/** @type {string[]} */ /** @type {string[]} */
this.narrators this.narrators
/** @type {Object} */ /** @type {AudioFileObject[]} */
this.audioFiles this.audioFiles
/** @type {EBookFileObject} */ /** @type {EBookFileObject} */
this.ebookFile this.ebookFile

View File

@ -3,6 +3,8 @@ const Logger = require('../Logger')
const oldLibraryItem = require('../objects/LibraryItem') const oldLibraryItem = require('../objects/LibraryItem')
const libraryFilters = require('../utils/queries/libraryFilters') const libraryFilters = require('../utils/queries/libraryFilters')
const { areEquivalent } = require('../utils/index') const { areEquivalent } = require('../utils/index')
const Book = require('./Book')
const Podcast = require('./Podcast')
/** /**
* @typedef LibraryFileObject * @typedef LibraryFileObject
@ -791,6 +793,11 @@ class LibraryItem extends Model {
return this.getOldLibraryItem(libraryItem) return this.getOldLibraryItem(libraryItem)
} }
/**
*
* @param {import('sequelize').FindOptions} options
* @returns {Promise<Book|Podcast>}
*/
getMedia(options) { getMedia(options) {
if (!this.mediaType) return Promise.resolve(null) if (!this.mediaType) return Promise.resolve(null)
const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}` const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}`

View File

@ -64,7 +64,7 @@ class AudioFile {
channelLayout: this.channelLayout, channelLayout: this.channelLayout,
chapters: this.chapters, chapters: this.chapters,
embeddedCoverArt: this.embeddedCoverArt, embeddedCoverArt: this.embeddedCoverArt,
metaTags: this.metaTags ? this.metaTags.toJSON() : {}, metaTags: this.metaTags?.toJSON() || {},
mimeType: this.mimeType mimeType: this.mimeType
} }
} }
@ -163,11 +163,16 @@ class AudioFile {
return new AudioFile(this.toJSON()) return new AudioFile(this.toJSON())
} }
/**
*
* @param {AudioFile} scannedAudioFile
* @returns {boolean} true if updates were made
*/
updateFromScan(scannedAudioFile) { updateFromScan(scannedAudioFile) {
let hasUpdated = false let hasUpdated = false
const newjson = scannedAudioFile.toJSON() const newjson = scannedAudioFile.toJSON()
const ignoreKeys = ['manuallyVerified', 'exclude', 'addedAt', 'updatedAt'] const ignoreKeys = ['manuallyVerified', 'ctimeMs', 'addedAt', 'updatedAt']
for (const key in newjson) { for (const key in newjson) {
if (key === 'metadata') { if (key === 'metadata') {

View File

@ -40,6 +40,7 @@ class ApiRouter {
constructor(Server) { constructor(Server) {
this.auth = Server.auth this.auth = Server.auth
this.scanner = Server.scanner this.scanner = Server.scanner
this.libraryScanner = Server.libraryScanner
this.playbackSessionManager = Server.playbackSessionManager this.playbackSessionManager = Server.playbackSessionManager
this.abMergeManager = Server.abMergeManager this.abMergeManager = Server.abMergeManager
this.backupManager = Server.backupManager this.backupManager = Server.backupManager

View File

@ -1,6 +1,197 @@
const Path = require('path')
const Logger = require('../Logger')
const prober = require('../utils/prober')
const LibraryItem = require('../models/LibraryItem')
const AudioFile = require('../objects/files/AudioFile')
class AudioFileScanner { class AudioFileScanner {
constructor() { } constructor() { }
/**
* Is array of numbers sequential, i.e. 1, 2, 3, 4
* @param {number[]} nums
* @returns {boolean}
*/
isSequential(nums) {
if (!nums?.length) return false
if (nums.length === 1) return true
let prev = nums[0]
for (let i = 1; i < nums.length; i++) {
if (nums[i] - prev > 1) return false
prev = nums[i]
}
return true
}
/**
* Remove
* @param {number[]} nums
* @returns {number[]}
*/
removeDupes(nums) {
if (!nums || !nums.length) return []
if (nums.length === 1) return nums
let nodupes = [nums[0]]
nums.forEach((num) => {
if (num > nodupes[nodupes.length - 1]) nodupes.push(num)
})
return nodupes
}
/**
* Order audio files by track/disc number
* @param {import('../models/Book')} book
* @param {import('../models/Book').AudioFileObject[]} audioFiles
* @returns {import('../models/Book').AudioFileObject[]}
*/
runSmartTrackOrder(book, audioFiles) {
let discsFromFilename = []
let tracksFromFilename = []
let discsFromMeta = []
let tracksFromMeta = []
audioFiles.forEach((af) => {
if (af.discNumFromFilename !== null) discsFromFilename.push(af.discNumFromFilename)
if (af.discNumFromMeta !== null) discsFromMeta.push(af.discNumFromMeta)
if (af.trackNumFromFilename !== null) tracksFromFilename.push(af.trackNumFromFilename)
if (af.trackNumFromMeta !== null) tracksFromMeta.push(af.trackNumFromMeta)
})
discsFromFilename.sort((a, b) => a - b)
discsFromMeta.sort((a, b) => a - b)
tracksFromFilename.sort((a, b) => a - b)
tracksFromMeta.sort((a, b) => a - b)
let discKey = null
if (discsFromMeta.length === audioFiles.length && this.isSequential(discsFromMeta)) {
discKey = 'discNumFromMeta'
} else if (discsFromFilename.length === audioFiles.length && this.isSequential(discsFromFilename)) {
discKey = 'discNumFromFilename'
}
let trackKey = null
tracksFromFilename = this.removeDupes(tracksFromFilename)
tracksFromMeta = this.removeDupes(tracksFromMeta)
if (tracksFromFilename.length > tracksFromMeta.length) {
trackKey = 'trackNumFromFilename'
} else {
trackKey = 'trackNumFromMeta'
}
if (discKey !== null) {
Logger.debug(`[AudioFileScanner] Smart track order for "${book.title}" using disc key ${discKey} and track key ${trackKey}`)
audioFiles.sort((a, b) => {
let Dx = a[discKey] - b[discKey]
if (Dx === 0) Dx = a[trackKey] - b[trackKey]
return Dx
})
} else {
Logger.debug(`[AudioFileScanner] Smart track order for "${book.title}" using track key ${trackKey}`)
audioFiles.sort((a, b) => a[trackKey] - b[trackKey])
}
for (let i = 0; i < audioFiles.length; i++) {
audioFiles[i].index = i + 1
}
return audioFiles
}
/**
* Get track and disc number from audio filename
* @param {{title:string, subtitle:string, series:string, sequence:string, publishedYear:string, narrators:string}} mediaMetadataFromScan
* @param {LibraryItem.LibraryFileObject} audioLibraryFile
* @returns {{trackNumber:number, discNumber:number}}
*/
getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) {
const { title, author, series, publishedYear } = mediaMetadataFromScan
const { filename, path } = audioLibraryFile.metadata
let partbasename = Path.basename(filename, Path.extname(filename))
// Remove title, author, series, and publishedYear from filename if there
if (title) partbasename = partbasename.replace(title, '')
if (author) partbasename = partbasename.replace(author, '')
if (series) partbasename = partbasename.replace(series, '')
if (publishedYear) partbasename = partbasename.replace(publishedYear)
// Look for disc number
let discNumber = null
const discMatch = partbasename.match(/\b(disc|cd) ?(\d\d?)\b/i)
if (discMatch && discMatch.length > 2 && discMatch[2]) {
if (!isNaN(discMatch[2])) {
discNumber = Number(discMatch[2])
}
// Remove disc number from filename
partbasename = partbasename.replace(/\b(disc|cd) ?(\d\d?)\b/i, '')
}
// Look for disc number in folder path e.g. /Book Title/CD01/audiofile.mp3
const pathdir = Path.dirname(path).split('/').pop()
if (pathdir && /^cd\d{1,3}$/i.test(pathdir)) {
const discFromFolder = Number(pathdir.replace(/cd/i, ''))
if (!isNaN(discFromFolder) && discFromFolder !== null) discNumber = discFromFolder
}
const numbersinpath = partbasename.match(/\d{1,4}/g)
const trackNumber = numbersinpath && numbersinpath.length ? parseInt(numbersinpath[0]) : null
return {
trackNumber,
discNumber
}
}
/**
*
* @param {string} mediaType
* @param {LibraryItem.LibraryFileObject} libraryFile
* @param {{title:string, subtitle:string, series:string, sequence:string, publishedYear:string, narrators:string}} mediaMetadataFromScan
* @returns {Promise<AudioFile>}
*/
async scan(mediaType, libraryFile, mediaMetadataFromScan) {
const probeData = await prober.probe(libraryFile.metadata.path)
if (probeData.error) {
Logger.error(`[MediaFileScanner] ${probeData.error} : "${libraryFile.metadata.path}"`)
return null
}
if (!probeData.audioStream) {
Logger.error('[MediaFileScanner] Invalid audio file no audio stream')
return null
}
const audioFile = new AudioFile()
audioFile.trackNumFromMeta = probeData.audioMetaTags.trackNumber
audioFile.discNumFromMeta = probeData.audioMetaTags.discNumber
if (mediaType === 'book') {
const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, libraryFile)
audioFile.trackNumFromFilename = trackNumber
audioFile.discNumFromFilename = discNumber
}
audioFile.setDataFromProbe(libraryFile, probeData)
return audioFile
}
/**
* Scan LibraryFiles and return AudioFiles
* @param {string} mediaType
* @param {import('./LibraryItemScanData')} libraryItemScanData
* @param {LibraryItem.LibraryFileObject[]} audioLibraryFiles
* @returns {Promise<AudioFile[]>}
*/
async executeMediaFileScans(mediaType, libraryItemScanData, audioLibraryFiles) {
const batchSize = 32
const results = []
for (let batch = 0; batch < audioLibraryFiles.length; batch += batchSize) {
const proms = []
for (let i = batch; i < Math.min(batch + batchSize, audioLibraryFiles.length); i++) {
proms.push(this.scan(mediaType, audioLibraryFiles[i], libraryItemScanData.mediaMetadata))
}
results.push(...await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr)))
}
return results
}
} }
module.exports = new AudioFileScanner() module.exports = new AudioFileScanner()

View File

@ -1,6 +1,7 @@
const packageJson = require('../../package.json') const packageJson = require('../../package.json')
const { LogLevel } = require('../utils/constants') const { LogLevel } = require('../utils/constants')
const LibraryItem = require('../models/LibraryItem') const LibraryItem = require('../models/LibraryItem')
const globals = require('../utils/globals')
class LibraryItemScanData { class LibraryItemScanData {
constructor(data) { constructor(data) {
@ -33,11 +34,41 @@ class LibraryItemScanData {
/** @type {boolean} */ /** @type {boolean} */
this.hasPathChange this.hasPathChange
/** @type {LibraryItem.LibraryFileObject[]} */ /** @type {LibraryItem.LibraryFileObject[]} */
this.libraryFilesRemoved this.libraryFilesRemoved = []
/** @type {LibraryItem.LibraryFileObject[]} */ /** @type {LibraryItem.LibraryFileObject[]} */
this.libraryFilesAdded this.libraryFilesAdded = []
/** @type {LibraryItem.LibraryFileObject[]} */ /** @type {LibraryItem.LibraryFileObject[]} */
this.libraryFilesModified this.libraryFilesModified = []
}
/** @type {boolean} */
get hasLibraryFileChanges() {
return this.libraryFilesRemoved.length + this.libraryFilesModified.length + this.libraryFilesAdded.length
}
/** @type {boolean} */
get hasAudioFileChanges() {
return this.audioLibraryFilesRemoved.length + this.audioLibraryFilesAdded.length + this.audioLibraryFilesModified
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFilesModified() {
return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFilesRemoved() {
return this.libraryFilesRemoved.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFilesAdded() {
return this.libraryFilesAdded.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFiles() {
return this.libraryFiles.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
} }
/** /**
@ -46,7 +77,7 @@ class LibraryItemScanData {
* @param {import('./LibraryScan')} libraryScan * @param {import('./LibraryScan')} libraryScan
*/ */
async checkLibraryItemData(existingLibraryItem, libraryScan) { async checkLibraryItemData(existingLibraryItem, libraryScan) {
const keysToCompare = ['libraryFolderId', 'ino', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'path', 'relPath', 'isFile'] const keysToCompare = ['libraryFolderId', 'ino', 'path', 'relPath', 'isFile']
this.hasChanges = false this.hasChanges = false
this.hasPathChange = false this.hasPathChange = false
for (const key of keysToCompare) { for (const key of keysToCompare) {
@ -61,6 +92,23 @@ class LibraryItemScanData {
} }
} }
// Check mtime, ctime and birthtime
if (existingLibraryItem.mtime.valueOf() !== this.mtimeMs) {
libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.relPath}" key "mtime" changed from "${existingLibraryItem.mtime.valueOf()}" to "${this.mtimeMs}"`)
existingLibraryItem.mtime = this.mtimeMs
this.hasChanges = true
}
if (existingLibraryItem.birthtime.valueOf() !== this.birthtimeMs) {
libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.relPath}" key "birthtime" changed from "${existingLibraryItem.birthtime.valueOf()}" to "${this.birthtimeMs}"`)
existingLibraryItem.birthtime = this.birthtimeMs
this.hasChanges = true
}
if (existingLibraryItem.ctime.valueOf() !== this.ctimeMs) {
libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.relPath}" key "ctime" changed from "${existingLibraryItem.ctime.valueOf()}" to "${this.ctimeMs}"`)
existingLibraryItem.ctime = this.ctimeMs
this.hasChanges = true
}
this.libraryFilesRemoved = [] this.libraryFilesRemoved = []
this.libraryFilesModified = [] this.libraryFilesModified = []
let libraryFilesAdded = this.libraryFiles.map(lf => lf) let libraryFilesAdded = this.libraryFiles.map(lf => lf)
@ -98,15 +146,24 @@ class LibraryItemScanData {
} }
} }
this.libraryFilesAdded = libraryFilesAdded
if (this.hasChanges) { if (this.hasChanges) {
existingLibraryItem.size = 0
existingLibraryItem.libraryFiles.forEach((lf) => existingLibraryItem.size += lf.metadata.size)
existingLibraryItem.lastScan = Date.now() existingLibraryItem.lastScan = Date.now()
existingLibraryItem.lastScanVersion = packageJson.version existingLibraryItem.lastScanVersion = packageJson.version
libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.path}" changed: [${existingLibraryItem.changed()?.join(',') || ''}]`)
if (this.hasLibraryFileChanges) {
existingLibraryItem.changed('libraryFiles', true)
}
await existingLibraryItem.save() await existingLibraryItem.save()
} else { } else {
libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.path}" is up-to-date`) libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.path}" is up-to-date`)
} }
this.libraryFilesAdded = libraryFilesAdded
} }
/** /**
@ -126,6 +183,10 @@ class LibraryItemScanData {
} }
for (const key in existingLibraryFile.metadata) { for (const key in existingLibraryFile.metadata) {
if (existingLibraryFile.metadata.relPath === 'metadata.json' || existingLibraryFile.metadata.relPath === 'metadata.abs') {
if (key === 'mtimeMs' || key === 'size') continue
}
if (existingLibraryFile.metadata[key] !== scannedLibraryFile.metadata[key]) { if (existingLibraryFile.metadata[key] !== scannedLibraryFile.metadata[key]) {
if (key !== 'path' && key !== 'relPath') { if (key !== 'path' && key !== 'relPath') {
libraryScan.addLog(LogLevel.DEBUG, `Library file "${existingLibraryFile.metadata.path}" for library item "${libraryItemPath}" key "${key}" changed from "${existingLibraryFile.metadata[key]}" to "${scannedLibraryFile.metadata[key]}"`) libraryScan.addLog(LogLevel.DEBUG, `Library file "${existingLibraryFile.metadata.path}" for library item "${libraryItemPath}" key "${key}" changed from "${existingLibraryFile.metadata[key]}" to "${scannedLibraryFile.metadata[key]}"`)
@ -143,5 +204,20 @@ class LibraryItemScanData {
return hasChanges return hasChanges
} }
/**
* Check if existing audio file on Book was removed
* @param {import('../models/Book').AudioFileObject} existingAudioFile
* @returns {boolean} true if audio file was removed
*/
checkAudioFileRemoved(existingAudioFile) {
if (!this.audioLibraryFilesRemoved.length) return false
// First check exact path
if (this.audioLibraryFilesRemoved.some(af => af.metadata.path === existingAudioFile.metadata.path)) {
return true
}
// Fallback to check inode value
return this.audioLibraryFilesRemoved.some(af => af.ino === existingAudioFile.ino)
}
} }
module.exports = LibraryItemScanData module.exports = LibraryItemScanData

View File

@ -7,7 +7,12 @@ const fs = require('../libs/fsExtra')
const fileUtils = require('../utils/fileUtils') const fileUtils = require('../utils/fileUtils')
const scanUtils = require('../utils/scandir') const scanUtils = require('../utils/scandir')
const { ScanResult, LogLevel } = require('../utils/constants') const { ScanResult, LogLevel } = require('../utils/constants')
const AudioFileScanner = require('./AudioFileScanner')
const ScanOptions = require('./ScanOptions')
const LibraryScan = require('./LibraryScan')
const LibraryItemScanData = require('./LibraryItemScanData') const LibraryItemScanData = require('./LibraryItemScanData')
const AudioFile = require('../objects/files/AudioFile')
const Book = require('../models/Book')
class LibraryScanner { class LibraryScanner {
constructor(coverManager, taskManager) { constructor(coverManager, taskManager) {
@ -102,7 +107,7 @@ class LibraryScanner {
where: { where: {
libraryId: libraryScan.libraryId libraryId: libraryScan.libraryId
}, },
attributes: ['id', 'mediaId', 'mediaType', 'path', 'relPath', 'ino', 'isMissing', 'mtime', 'ctime', 'birthtime', 'libraryFiles', 'libraryFolderId'] attributes: ['id', 'mediaId', 'mediaType', 'path', 'relPath', 'ino', 'isMissing', 'isFile', 'mtime', 'ctime', 'birthtime', 'libraryFiles', 'libraryFolderId', 'size']
}) })
const libraryItemIdsMissing = [] const libraryItemIdsMissing = []
@ -129,8 +134,8 @@ class LibraryScanner {
} }
} else { } else {
await libraryItemData.checkLibraryItemData(existingLibraryItem, libraryScan) await libraryItemData.checkLibraryItemData(existingLibraryItem, libraryScan)
if (libraryItemData.hasChanges) { if (libraryItemData.hasLibraryFileChanges || libraryItemData.hasPathChange) {
await this.rescanLibraryItem(existingLibraryItem, libraryItemData) await this.rescanLibraryItem(existingLibraryItem, libraryItemData, libraryScan)
} }
} }
} }
@ -222,9 +227,92 @@ class LibraryScanner {
* *
* @param {import('../models/LibraryItem')} existingLibraryItem * @param {import('../models/LibraryItem')} existingLibraryItem
* @param {LibraryItemScanData} libraryItemData * @param {LibraryItemScanData} libraryItemData
* @param {LibraryScan} libraryScan
*/ */
async rescanLibraryItem(existingLibraryItem, libraryItemData) { async rescanLibraryItem(existingLibraryItem, libraryItemData, libraryScan) {
if (existingLibraryItem.mediaType === 'book') {
/** @type {Book} */
const media = await existingLibraryItem.getMedia({
include: [
{
model: Database.authorModel,
through: {
attributes: ['createdAt']
}
},
{
model: Database.seriesModel,
through: {
attributes: ['sequence', 'createdAt']
}
}
]
})
let hasMediaChanges = libraryItemData.hasAudioFileChanges
if (libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== media.audioFiles.length) {
// Filter out audio files that were removed
media.audioFiles = media.audioFiles.filter(af => libraryItemData.checkAudioFileRemoved(af))
// Update audio files that were modified
if (libraryItemData.audioLibraryFilesModified.length) {
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified)
media.audioFiles = media.audioFiles.map((audioFileObj) => {
let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path)
if (!matchedScannedAudioFile) {
matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === audioFileObj.ino)
}
if (matchedScannedAudioFile) {
scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile)
const audioFile = new AudioFile(audioFileObj)
audioFile.updateFromScan(matchedScannedAudioFile)
return audioFile.toJSON()
}
return audioFileObj
})
// Modified audio files that were not found on the book
if (scannedAudioFiles.length) {
media.audioFiles.push(...scannedAudioFiles)
}
}
// Add new audio files scanned in
if (libraryItemData.audioLibraryFilesAdded.length) {
const scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesAdded)
media.audioFiles.push(...scannedAudioFiles)
}
// Add audio library files that are not already set on the book (safety check)
let audioLibraryFilesToAdd = []
for (const audioLibraryFile of libraryItemData.audioLibraryFiles) {
if (!media.audioFiles.some(af => af.ino === audioLibraryFile.ino)) {
libraryScan.addLog(LogLevel.DEBUG, `Existing audio library file "${audioLibraryFile.metadata.relPath}" was not set on book "${media.title}" so setting it now`)
audioLibraryFilesToAdd.push(audioLibraryFile)
}
}
if (audioLibraryFilesToAdd.length) {
const scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, audioLibraryFilesToAdd)
media.audioFiles.push(...scannedAudioFiles)
}
media.audioFiles = AudioFileScanner.runSmartTrackOrder(media, media.audioFiles)
media.duration = 0
media.audioFiles.forEach((af) => {
if (!isNaN(af.duration)) {
media.duration += af.duration
}
})
media.changed('audioFiles', true)
}
if (hasMediaChanges) {
await media.save()
}
}
} }
} }
module.exports = LibraryScanner module.exports = LibraryScanner

View File

@ -278,7 +278,12 @@ function parseProbeData(data, verbose = false) {
} }
} }
// Updated probe returns MediaProbeData object /**
* Run ffprobe on audio filepath
* @param {string} filepath
* @param {boolean} [verbose=false]
* @returns {import('../scanner/MediaProbeData')|{error:string}}
*/
function probe(filepath, verbose = false) { function probe(filepath, verbose = false) {
if (process.env.FFPROBE_PATH) { if (process.env.FFPROBE_PATH) {
ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH