Add:New scanner and scanner server settings

This commit is contained in:
advplyr 2021-11-25 18:39:02 -06:00
parent bf11d266dc
commit a5fc382cad
17 changed files with 681 additions and 176 deletions

View file

@ -165,9 +165,9 @@ class AudioFile {
this.fullPath = fileData.fullPath
this.addedAt = Date.now()
this.trackNumFromMeta = fileData.trackNumFromMeta || null
this.trackNumFromFilename = fileData.trackNumFromFilename || null
this.cdNumFromFilename = fileData.cdNumFromFilename || null
this.trackNumFromMeta = fileData.trackNumFromMeta
this.trackNumFromFilename = fileData.trackNumFromFilename
this.cdNumFromFilename = fileData.cdNumFromFilename
this.format = probeData.format
this.duration = probeData.duration
@ -180,15 +180,13 @@ class AudioFile {
this.channelLayout = probeData.channelLayout
this.chapters = probeData.chapters || []
this.metadata = probeData.audioFileMetadata
this.embeddedCoverArt = probeData.embeddedCoverArt
}
validateTrackIndex(isSingleTrack) {
validateTrackIndex() {
var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta)
var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename)
if (isSingleTrack) { // Single audio track audiobook only use metadata tag and default to 1
return numFromMeta ? numFromMeta : 1
}
if (numFromMeta !== null) return numFromMeta
if (numFromFilename !== null) return numFromFilename
@ -284,5 +282,33 @@ class AudioFile {
})
return hasUpdates
}
updateFromScan(scannedAudioFile) {
var hasUpdated = false
var newjson = scannedAudioFile.toJSON()
if (this.manuallyVerified) newjson.manuallyVerified = true
if (this.exclude) newjson.exclude = true
newjson.addedAt = this.addedAt
for (const key in newjson) {
if (key === 'metadata') {
if (!this.metadata || !this.metadata.isEqual(scannedAudioFile.metadata)) {
this.metadata = scannedAudioFile.metadata
hasUpdated = true
// console.log('metadata updated for audio file')
}
} else if (key === 'chapters') {
if (this.syncChapters(newjson.chapters || [])) {
hasUpdated = true
}
} else if (this[key] !== newjson[key]) {
this[key] = newjson[key]
hasUpdated = true
// console.log('key', key, 'updated', this[key], newjson[key])
}
}
return hasUpdated
}
}
module.exports = AudioFile

View file

@ -101,5 +101,13 @@ class AudioFileMetadata {
}
return hasUpdates
}
isEqual(audioFileMetadata) {
if (!audioFileMetadata || !audioFileMetadata.toJSON) return false
for (const key in audioFileMetadata.toJSON()) {
if (audioFileMetadata[key] !== this[key]) return false
}
return true
}
}
module.exports = AudioFileMetadata

View file

@ -353,6 +353,11 @@ class Audiobook {
this.lastUpdate = Date.now()
}
setInvalid() {
this.isInvalid = true
this.lastUpdate = Date.now()
}
setBook(data) {
// Use first image file as cover
if (this.otherFiles && this.otherFiles.length) {
@ -400,6 +405,11 @@ class Audiobook {
}
}
updateAudioFile(updatedAudioFile) {
var audioFile = this.audioFiles.find(af => af.ino === updatedAudioFile.ino)
return audioFile.updateFromScan(updatedAudioFile)
}
addOtherFile(fileData) {
var file = new AudiobookFile()
file.setData(fileData)
@ -437,8 +447,8 @@ class Audiobook {
return this.book.updateCover(cover, coverFullPath)
}
checkHasTrackNum(trackNum) {
return this.tracks.find(t => t.index === trackNum)
checkHasTrackNum(trackNum, excludeIno) {
return this._audioFiles.find(t => t.index === trackNum && t.ino !== excludeIno)
}
updateAudioTracks(orderedFileData) {
@ -473,6 +483,7 @@ class Audiobook {
}
})
this.setChapters()
this.checkUpdateMissingTracks()
this.lastUpdate = Date.now()
}
@ -486,7 +497,7 @@ class Audiobook {
this.audioFiles = this.audioFiles.filter(f => f.ino !== track.ino)
}
checkUpdateMissingParts() {
checkUpdateMissingTracks() {
var currMissingParts = (this.missingParts || []).join(',') || ''
var current_index = 1
@ -515,13 +526,14 @@ class Audiobook {
}
// On scan check other files found with other files saved
async syncOtherFiles(newOtherFiles, metadataPath, forceRescan = false) {
async syncOtherFiles(newOtherFiles, metadataPath, opfMetadataOverrideDetails, forceRescan = false) {
var hasUpdates = false
var currOtherFileNum = this.otherFiles.length
var alreadyHasDescTxt = this.otherFiles.find(of => of.filename === 'desc.txt')
var alreadyHasReaderTxt = this.otherFiles.find(of => of.filename === 'reader.txt')
var otherFilenamesAlreadyInBook = this.otherFiles.map(ofile => ofile.filename)
var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
var newOtherFilePaths = newOtherFiles.map(f => f.path)
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
@ -553,21 +565,22 @@ class Audiobook {
}
}
// If OPF file and was not already there
var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
if (metadataOpf) {
if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
var xmlText = await readTextFile(metadataOpf.fullPath)
if (xmlText) {
var opfMetadata = await parseOpfMetadataXML(xmlText)
Logger.debug(`[Audiobook] Sync Other File "${metadataOpf.filename}" parsed:`, opfMetadata)
// Logger.debug(`[Audiobook] Sync Other File "${metadataOpf.filename}" parsed:`, opfMetadata)
if (opfMetadata) {
const bookUpdatePayload = {}
for (const key in opfMetadata) {
// Add genres only if genres are empty
if (key === 'genres') {
if (opfMetadata.genres.length && !this.book._genres.length) {
if (opfMetadata.genres.length && (!this.book._genres.length || opfMetadataOverrideDetails)) {
bookUpdatePayload[key] = opfMetadata.genres
}
} else if (opfMetadata[key] && !this.book[key]) {
} else if (opfMetadata[key] && (!this.book[key] || opfMetadataOverrideDetails)) {
bookUpdatePayload[key] = opfMetadata[key]
}
}
@ -789,7 +802,7 @@ class Audiobook {
}
// Look for desc.txt and reader.txt and update details if found
async saveDataFromTextFiles() {
async saveDataFromTextFiles(opfMetadataOverrideDetails) {
var bookUpdatePayload = {}
var descriptionText = await this.fetchTextFromTextFile('desc.txt')
if (descriptionText) {
@ -807,15 +820,15 @@ class Audiobook {
var xmlText = await readTextFile(metadataOpf.fullPath)
if (xmlText) {
var opfMetadata = await parseOpfMetadataXML(xmlText)
Logger.debug(`[Audiobook] "${this.title}" found "${metadataOpf.filename}" parsed:`, opfMetadata)
// Logger.debug(`[Audiobook] "${this.title}" found "${metadataOpf.filename}" parsed:`, opfMetadata)
if (opfMetadata) {
for (const key in opfMetadata) {
// Add genres only if genres are empty
if (key === 'genres') {
if (opfMetadata.genres.length && !this.book._genres.length) {
if (opfMetadata.genres.length && (!this.book._genres.length || opfMetadataOverrideDetails)) {
bookUpdatePayload[key] = opfMetadata.genres
}
} else if (opfMetadata[key] && !this.book[key] && !bookUpdatePayload[key]) {
} else if (opfMetadata[key] && ((!this.book[key] && !bookUpdatePayload[key]) || opfMetadataOverrideDetails)) {
bookUpdatePayload[key] = opfMetadata[key]
}
}
@ -836,10 +849,10 @@ class Audiobook {
}
// Audio file metadata tags map to book details (will not overwrite)
setDetailsFromFileMetadata() {
setDetailsFromFileMetadata(overrideExistingDetails = false) {
if (!this.audioFiles.length) return false
var audioFile = this.audioFiles[0]
return this.book.setDetailsFromFileMetadata(audioFile.metadata)
return this.book.setDetailsFromFileMetadata(audioFile.metadata, overrideExistingDetails)
}
// Returns null if file not found, true if file was updated, false if up to date
@ -884,9 +897,15 @@ class Audiobook {
return hasUpdated
}
checkScanData(dataFound) {
checkScanData(dataFound, version) {
var hasUpdated = false
if (this.isMissing) {
// Audiobook no longer missing
this.isMissing = false
hasUpdated = true
}
if (dataFound.ino !== this.ino) {
this.ino = dataFound.ino
hasUpdated = true
@ -916,7 +935,7 @@ class Audiobook {
var audioFileFoundCheck = this.checkFileFound(af, true)
if (audioFileFoundCheck === null) {
newAudioFileData.push(af)
} else if (audioFileFoundCheck === true) {
} else if (audioFileFoundCheck) {
hasUpdated = true
}
})
@ -925,7 +944,7 @@ class Audiobook {
var fileFoundCheck = this.checkFileFound(otherFileData, false)
if (fileFoundCheck === null) {
newOtherFileData.push(otherFileData)
} else if (fileFoundCheck === true) {
} else if (fileFoundCheck) {
hasUpdated = true
}
})
@ -933,7 +952,7 @@ class Audiobook {
const audioFilesRemoved = []
const otherFilesRemoved = []
// inodes will all be up to date at this point
// Remove audio files not found (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())
@ -946,10 +965,11 @@ class Audiobook {
if (audioFilesRemoved.length) {
const audioFilesRemovedInodes = audioFilesRemoved.map(afr => afr.ino)
this.tracks = this.tracks.filter(t => !audioFilesRemovedInodes.includes(t.ino))
this.checkUpdateMissingParts()
this.checkUpdateMissingTracks()
hasUpdated = true
}
// Remove other files not found
this.otherFiles = this.otherFiles.filter(otherFile => {
if (!dataFound.otherFiles.find(_otherFile => _otherFile.ino === otherFile.ino)) {
otherFilesRemoved.push(otherFile.toJSON())
@ -969,6 +989,15 @@ class Audiobook {
hasUpdated = true
}
// Check if invalid (has no audio files or ebooks)
if (!this.audioFilesToInclude.length && !this.ebooks.length && !newAudioFileData.length && !newOtherFileData.length) {
this.isInvalid = true
}
if (hasUpdated) {
this.setLastScan(version)
}
return {
updated: hasUpdated,
newAudioFileData,

View file

@ -43,6 +43,7 @@ class Book {
get _genres() { return this.genres || [] }
get shouldSearchForCover() {
if (this.cover) return false
if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
var timeSinceLastSearch = Date.now() - this.lastCoverSearch
return timeSinceLastSearch > 1000 * 60 * 60 * 24 * 7 // every 7 days do another lookup
@ -297,7 +298,7 @@ class Book {
return [genreTag]
}
setDetailsFromFileMetadata(audioFileMetadata) {
setDetailsFromFileMetadata(audioFileMetadata, overrideExistingDetails = false) {
const MetadataMapArray = [
{
tag: 'tagComposer',
@ -319,6 +320,10 @@ class Book {
tag: 'tagSubtitle',
key: 'subtitle'
},
{
tag: 'tagAlbum',
key: 'title',
},
{
tag: 'tagArtist',
key: 'author'
@ -342,12 +347,12 @@ class Book {
MetadataMapArray.forEach((mapping) => {
if (audioFileMetadata[mapping.tag]) {
// Genres can contain multiple
if (mapping.key === 'genres' && (!this[mapping.key].length || !this[mapping.key])) {
if (mapping.key === 'genres' && (!this[mapping.key].length || !this[mapping.key] || overrideExistingDetails)) {
updatePayload[mapping.key] = this.parseGenresTag(audioFileMetadata[mapping.tag])
Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key].join(',')}`)
} else if (!this[mapping.key]) {
// Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key].join(',')}`)
} else if (!this[mapping.key] || overrideExistingDetails) {
updatePayload[mapping.key] = audioFileMetadata[mapping.tag]
Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key]}`)
// Logger.debug(`[Book] Mapping metadata to key ${mapping.tag} => ${mapping.key}: ${updatePayload[mapping.key]}`)
}
}
})

View file

@ -46,6 +46,9 @@ class ServerSettings {
this.newTagExpireDays = settings.newTagExpireDays
this.scannerFindCovers = !!settings.scannerFindCovers
this.scannerParseSubtitle = settings.scannerParseSubtitle
this.scannerPreferAudioMetadata = !!settings.scannerPreferAudioMetadata
this.scannerPreferOpfMetadata = !!settings.scannerPreferOpfMetadata
this.coverDestination = settings.coverDestination || CoverDestination.METADATA
this.saveMetadataFile = !!settings.saveMetadataFile
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
@ -73,6 +76,8 @@ class ServerSettings {
newTagExpireDays: this.newTagExpireDays,
scannerFindCovers: this.scannerFindCovers,
scannerParseSubtitle: this.scannerParseSubtitle,
scannerPreferAudioMetadata: this.scannerPreferAudioMetadata,
scannerPreferOpfMetadata: this.scannerPreferOpfMetadata,
coverDestination: this.coverDestination,
saveMetadataFile: !!this.saveMetadataFile,
rateLimitLoginRequests: this.rateLimitLoginRequests,