New data model save covers, scanner, new api routes

This commit is contained in:
advplyr 2022-03-12 17:45:32 -06:00
parent 5f4e5cd3d8
commit 73257188f6
37 changed files with 1649 additions and 672 deletions

View file

@ -1,3 +1,4 @@
const { version } = require('../../package.json')
const Logger = require('../Logger')
const LibraryFile = require('./files/LibraryFile')
const Book = require('./entities/Book')
@ -22,8 +23,10 @@ class LibraryItem {
this.lastScan = null
this.scanVersion = null
// Entity was scanned and not found
// Was scanned and no longer exists
this.isMissing = false
// Was scanned and no longer has media files
this.isInvalid = false
this.mediaType = null
this.media = null
@ -51,6 +54,7 @@ class LibraryItem {
this.scanVersion = libraryItem.scanVersion || null
this.isMissing = !!libraryItem.isMissing
this.isInvalid = !!libraryItem.isInvalid
this.mediaType = libraryItem.mediaType
if (this.mediaType === 'book') {
@ -78,6 +82,7 @@ class LibraryItem {
lastScan: this.lastScan,
scanVersion: this.scanVersion,
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
mediaType: this.mediaType,
media: this.media.toJSON(),
libraryFiles: this.libraryFiles.map(f => f.toJSON())
@ -98,6 +103,7 @@ class LibraryItem {
addedAt: this.addedAt,
updatedAt: this.updatedAt,
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
mediaType: this.mediaType,
media: this.media.toJSONMinified(),
numFiles: this.libraryFiles.length
@ -121,6 +127,7 @@ class LibraryItem {
lastScan: this.lastScan,
scanVersion: this.scanVersion,
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
mediaType: this.mediaType,
media: this.media.toJSONExpanded(),
libraryFiles: this.libraryFiles.map(f => f.toJSON()),
@ -133,6 +140,42 @@ class LibraryItem {
this.libraryFiles.forEach((lf) => total += lf.metadata.size)
return total
}
get hasAudioFiles() {
return this.libraryFiles.some(lf => lf.fileType === 'audio')
}
get hasMediaFiles() {
return this.media.hasMediaFiles
}
// Data comes from scandir library item data
setData(libraryMediaType, payload) {
if (libraryMediaType === 'podcast') {
this.mediaType = 'podcast'
this.media = new Podcast()
} else {
this.mediaType = 'book'
this.media = new Book()
}
for (const key in payload) {
if (key === 'libraryFiles') {
this.libraryFiles = payload.libraryFiles.map(lf => lf.clone())
// Use first image library file as cover
var firstImageFile = this.libraryFiles.find(lf => lf.fileType === 'image')
if (firstImageFile) this.media.coverPath = firstImageFile.metadata.path
} else if (this[key] !== undefined) {
this[key] = payload[key]
}
}
if (payload.mediaMetadata) {
this.media.setData(payload.mediaMetadata)
}
this.addedAt = Date.now()
this.updatedAt = Date.now()
}
update(payload) {
var json = this.toJSON()
@ -149,7 +192,214 @@ class LibraryItem {
}
}
}
if (hasUpdates) {
this.updatedAt = Date.now()
}
return hasUpdates
}
updateMediaCover(coverPath) {
this.media.updateCover(coverPath)
this.updatedAt = Date.now()
return true
}
setMissing() {
this.isMissing = true
this.updatedAt = Date.now()
}
setInvalid() {
this.isInvalid = true
this.updatedAt = Date.now()
}
setLastScan() {
this.lastScan = Date.now()
this.scanVersion = version
}
saveMetadata() { }
// Returns null if file not found, true if file was updated, false if up to date
checkFileFound(fileFound) {
var hasUpdated = false
var existingFile = this.libraryFiles.find(lf => lf.ino === fileFound.ino)
var mediaFile = null
if (!existingFile) {
existingFile = this.libraryFiles.find(lf => lf.metadata.path === fileFound.metadata.path)
if (existingFile) {
// Update media file ino
mediaFile = this.media.findFileWithInode(existingFile.ino)
if (mediaFile) {
mediaFile.ino = fileFound.ino
}
// file inode was updated
existingFile.ino = fileFound.ino
hasUpdated = true
} else {
// file not found
return null
}
} else {
mediaFile = this.media.findFileWithInode(existingFile.ino)
}
if (existingFile.metadata.path !== fileFound.metadata.path) {
existingFile.metadata.path = fileFound.metadata.path
existingFile.metadata.relPath = fileFound.metadata.relPath
if (mediaFile) {
mediaFile.metadata.path = fileFound.metadata.path
mediaFile.metadata.relPath = fileFound.metadata.relPath
}
hasUpdated = true
}
var keysToCheck = ['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size']
keysToCheck.forEach((key) => {
if (existingFile.metadata[key] !== fileFound.metadata[key]) {
// Add modified flag on file data object if exists and was changed
if (key === 'mtimeMs' && existingFile.metadata[key]) {
fileFound.metadata.wasModified = true
}
existingFile.metadata[key] = fileFound.metadata[key]
if (mediaFile) {
if (key === 'mtimeMs') mediaFile.metadata.wasModified = true
mediaFile.metadata[key] = fileFound.metadata[key]
}
hasUpdated = true
}
})
return hasUpdated
}
// Data pulled from scandir during a scan, check it with current data
checkScanData(dataFound) {
var hasUpdated = false
if (this.isMissing) {
// Item no longer missing
this.isMissing = false
hasUpdated = true
}
if (dataFound.ino !== this.ino) {
this.ino = dataFound.ino
hasUpdated = true
}
if (dataFound.folderId !== this.folderId) {
Logger.warn(`[LibraryItem] Check scan item changed folder ${this.folderId} -> ${dataFound.folderId}`)
this.folderId = dataFound.folderId
hasUpdated = true
}
if (dataFound.path !== this.path) {
Logger.warn(`[LibraryItem] Check scan item changed path "${this.path}" -> "${dataFound.path}"`)
this.path = dataFound.path
this.relPath = dataFound.relPath
hasUpdated = true
}
var keysToCheck = ['mtimeMs', 'ctimeMs', 'birthtimeMs']
keysToCheck.forEach((key) => {
if (dataFound[key] != this[key]) {
this[key] = dataFound[key] || 0
hasUpdated = true
}
})
var newLibraryFiles = []
var existingLibraryFiles = []
dataFound.libraryFiles.forEach((lf) => {
var fileFoundCheck = this.checkFileFound(lf, true)
console.log('Check library file', fileFoundCheck, lf.metadata.filename)
if (fileFoundCheck === null) {
newLibraryFiles.push(lf)
} else if (fileFoundCheck) {
hasUpdated = true
existingLibraryFiles.push(lf)
} else {
existingLibraryFiles.push(lf)
}
})
const filesRemoved = []
// Remove files not found (inodes will all be up to date at this point)
this.libraryFiles = this.libraryFiles.filter(lf => {
if (!dataFound.libraryFiles.find(_lf => _lf.ino === lf.ino)) {
if (lf.metadata.path === this.media.coverPath) {
Logger.debug(`[LibraryItem] "${this.media.metadata.title}" check scan cover removed`)
this.media.updateCover('')
}
filesRemoved.push(lf.toJSON())
this.media.removeFileWithInode(lf.ino)
return false
}
return true
})
if (filesRemoved.length) {
this.media.checkUpdateMissingTracks()
hasUpdated = true
}
// Add library files to library item
if (newLibraryFiles.length) {
newLibraryFiles.forEach((lf) => this.libraryFiles.push(lf.clone()))
hasUpdated = true
}
// Check if invalid
this.isInvalid = !this.media.hasMediaFiles
// If cover path is in item folder, make sure libraryFile exists for it
if (this.media.coverPath && this.media.coverPath.startsWith(this.path)) {
var lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
if (!lf) {
Logger.warn(`[LibraryItem] Invalid cover path - library file dne "${this.media.coverPath}"`)
this.media.updateCover('')
hasUpdated = true
}
}
if (hasUpdated) {
this.setLastScan()
}
return {
updated: hasUpdated,
newLibraryFiles,
filesRemoved,
existingLibraryFiles // Existing file data may get re-scanned if forceRescan is set
}
}
// Set metadata from files
async syncFiles(preferOpfMetadata) {
var imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
console.log('image files', imageFiles.length, 'has cover', this.media.coverPath)
if (imageFiles.length && !this.media.coverPath) {
this.media.coverPath = imageFiles[0].metadata.path
Logger.debug('[LibraryItem] Set media cover path', this.media.coverPath)
}
var textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
if (!textMetadataFiles.length) {
return false
}
var hasUpdated = await this.media.syncMetadataFiles(textMetadataFiles, preferOpfMetadata)
if (hasUpdated) {
this.updatedAt = Date.now()
}
return hasUpdated
}
}
module.exports = LibraryItem