Updates to migrate off of old library model

This commit is contained in:
advplyr 2024-08-24 16:09:54 -05:00
parent 5d13faef33
commit 159ccd807f
11 changed files with 141 additions and 133 deletions

View file

@ -26,27 +26,27 @@ class LibraryScanner {
}
/**
* @param {string} libraryId
* @param {string} libraryId
* @returns {boolean}
*/
isLibraryScanning(libraryId) {
return this.librariesScanning.some(ls => ls.id === libraryId)
return this.librariesScanning.some((ls) => ls.id === libraryId)
}
/**
*
* @param {string} libraryId
*
* @param {string} libraryId
*/
setCancelLibraryScan(libraryId) {
const libraryScanning = this.librariesScanning.find(ls => ls.id === libraryId)
const libraryScanning = this.librariesScanning.find((ls) => ls.id === libraryId)
if (!libraryScanning) return
this.cancelLibraryScan[libraryId] = true
}
/**
*
* @param {import('../objects/Library')} library
* @param {boolean} [forceRescan]
*
* @param {import('../objects/Library')} library
* @param {boolean} [forceRescan]
*/
async scan(library, forceRescan = false) {
if (this.isLibraryScanning(library.id)) {
@ -89,7 +89,7 @@ class LibraryScanner {
libraryScan.setComplete()
Logger.info(`[LibraryScanner] Library scan ${libraryScan.id} completed in ${libraryScan.elapsedTimestamp} | ${libraryScan.resultStats}`)
this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
this.librariesScanning = this.librariesScanning.filter((ls) => ls.id !== library.id)
if (canceled && !libraryScan.totalResults) {
task.setFinished('Scan canceled')
@ -116,8 +116,8 @@ class LibraryScanner {
}
/**
*
* @param {import('./LibraryScan')} libraryScan
*
* @param {import('./LibraryScan')} libraryScan
* @param {boolean} forceRescan
* @returns {Promise<boolean>} true if scan canceled
*/
@ -151,14 +151,10 @@ class LibraryScanner {
let oldLibraryItemsUpdated = []
for (const existingLibraryItem of existingLibraryItems) {
// First try to find matching library item with exact file path
let libraryItemData = libraryItemDataFound.find(lid => lid.path === existingLibraryItem.path)
let libraryItemData = libraryItemDataFound.find((lid) => lid.path === existingLibraryItem.path)
if (!libraryItemData) {
// Fallback to finding matching library item with matching inode value
libraryItemData = libraryItemDataFound.find(lid =>
ItemToItemInoMatch(lid, existingLibraryItem) ||
ItemToFileInoMatch(lid, existingLibraryItem) ||
ItemToFileInoMatch(existingLibraryItem, lid)
)
libraryItemData = libraryItemDataFound.find((lid) => ItemToItemInoMatch(lid, existingLibraryItem) || ItemToFileInoMatch(lid, existingLibraryItem) || ItemToFileInoMatch(existingLibraryItem, lid))
if (libraryItemData) {
libraryScan.addLog(LogLevel.INFO, `Library item with path "${existingLibraryItem.path}" was not found, but library item inode "${existingLibraryItem.ino}" was found at path "${libraryItemData.path}"`)
}
@ -166,7 +162,7 @@ class LibraryScanner {
if (!libraryItemData) {
// Podcast folder can have no episodes and still be valid
if (libraryScan.libraryMediaType === 'podcast' && await fs.pathExists(existingLibraryItem.path)) {
if (libraryScan.libraryMediaType === 'podcast' && (await fs.pathExists(existingLibraryItem.path))) {
libraryScan.addLog(LogLevel.INFO, `Library item "${existingLibraryItem.relPath}" folder exists but has no episodes`)
} else {
libraryScan.addLog(LogLevel.WARN, `Library Item "${existingLibraryItem.path}" (inode: ${existingLibraryItem.ino}) is missing`)
@ -184,7 +180,7 @@ class LibraryScanner {
}
}
} else {
libraryItemDataFound = libraryItemDataFound.filter(lidf => lidf !== libraryItemData)
libraryItemDataFound = libraryItemDataFound.filter((lidf) => lidf !== libraryItemData)
let libraryItemDataUpdated = await libraryItemData.checkLibraryItemData(existingLibraryItem, libraryScan)
if (libraryItemDataUpdated || forceRescan) {
if (forceRescan || libraryItemData.hasLibraryFileChanges || libraryItemData.hasPathChange) {
@ -210,7 +206,10 @@ class LibraryScanner {
// Emit item updates in chunks of 10 to client
if (oldLibraryItemsUpdated.length === 10) {
// TODO: Should only emit to clients where library item is accessible
SocketAuthority.emitter('items_updated', oldLibraryItemsUpdated.map(li => li.toJSONExpanded()))
SocketAuthority.emitter(
'items_updated',
oldLibraryItemsUpdated.map((li) => li.toJSONExpanded())
)
oldLibraryItemsUpdated = []
}
@ -219,7 +218,10 @@ class LibraryScanner {
// Emit item updates to client
if (oldLibraryItemsUpdated.length) {
// TODO: Should only emit to clients where library item is accessible
SocketAuthority.emitter('items_updated', oldLibraryItemsUpdated.map(li => li.toJSONExpanded()))
SocketAuthority.emitter(
'items_updated',
oldLibraryItemsUpdated.map((li) => li.toJSONExpanded())
)
}
// Authors and series that were removed from books should be removed if they are now empty
@ -228,15 +230,18 @@ class LibraryScanner {
// Update missing library items
if (libraryItemIdsMissing.length) {
libraryScan.addLog(LogLevel.INFO, `Updating ${libraryItemIdsMissing.length} library items missing`)
await Database.libraryItemModel.update({
isMissing: true,
lastScan: Date.now(),
lastScanVersion: packageJson.version
}, {
where: {
id: libraryItemIdsMissing
await Database.libraryItemModel.update(
{
isMissing: true,
lastScan: Date.now(),
lastScanVersion: packageJson.version
},
{
where: {
id: libraryItemIdsMissing
}
}
})
)
}
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
@ -256,7 +261,10 @@ class LibraryScanner {
// Emit new items in chunks of 10 to client
if (newOldLibraryItems.length === 10) {
// TODO: Should only emit to clients where library item is accessible
SocketAuthority.emitter('items_added', newOldLibraryItems.map(li => li.toJSONExpanded()))
SocketAuthority.emitter(
'items_added',
newOldLibraryItems.map((li) => li.toJSONExpanded())
)
newOldLibraryItems = []
}
@ -265,15 +273,18 @@ class LibraryScanner {
// Emit new items to client
if (newOldLibraryItems.length) {
// TODO: Should only emit to clients where library item is accessible
SocketAuthority.emitter('items_added', newOldLibraryItems.map(li => li.toJSONExpanded()))
SocketAuthority.emitter(
'items_added',
newOldLibraryItems.map((li) => li.toJSONExpanded())
)
}
}
}
/**
* Get scan data for library folder
* @param {import('../objects/Library')} library
* @param {import('../objects/Folder')} folder
* @param {import('../objects/Library')} library
* @param {import('../objects/Folder')} folder
* @returns {LibraryItemScanData[]}
*/
async scanFolder(library, folder) {
@ -321,27 +332,29 @@ class LibraryScanner {
continue
}
items.push(new LibraryItemScanData({
libraryFolderId: folder.id,
libraryId: folder.libraryId,
mediaType: library.mediaType,
ino: libraryItemFolderStats.ino,
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
path: libraryItemData.path,
relPath: libraryItemData.relPath,
isFile,
mediaMetadata: libraryItemData.mediaMetadata || null,
libraryFiles: fileObjs
}))
items.push(
new LibraryItemScanData({
libraryFolderId: folder.id,
libraryId: folder.libraryId,
mediaType: library.mediaType,
ino: libraryItemFolderStats.ino,
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
path: libraryItemData.path,
relPath: libraryItemData.relPath,
isFile,
mediaMetadata: libraryItemData.mediaMetadata || null,
libraryFiles: fileObjs
})
)
}
return items
}
/**
* Scan files changed from Watcher
* @param {import('../Watcher').PendingFileUpdate[]} fileUpdates
* @param {import('../Watcher').PendingFileUpdate[]} fileUpdates
* @param {Task} pendingTask
*/
async scanFilesChanged(fileUpdates, pendingTask) {
@ -366,7 +379,7 @@ class LibraryScanner {
for (const folderId in folderGroups) {
const libraryId = folderGroups[folderId].libraryId
// const library = await Database.libraryModel.getOldById(libraryId)
const library = await Database.libraryModel.findByPk(libraryId, {
include: {
model: Database.libraryFolderModel,
@ -381,7 +394,7 @@ class LibraryScanner {
}
const folder = library.libraryFolders[0]
const relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath)
const relFilePaths = folderGroups[folderId].fileUpdates.map((fileUpdate) => fileUpdate.relPath)
const fileUpdateGroup = scanUtils.groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths)
if (!Object.keys(fileUpdateGroup).length) {
@ -432,7 +445,7 @@ class LibraryScanner {
/**
* Group array of PendingFileUpdate from Watcher by folder
* @param {import('../Watcher').PendingFileUpdate[]} fileUpdates
* @param {import('../Watcher').PendingFileUpdate[]} fileUpdates
* @returns {Record<string,{libraryId:string, folderId:string, fileUpdates:import('../Watcher').PendingFileUpdate[]}>}
*/
getFileUpdatesGrouped(fileUpdates) {
@ -453,9 +466,9 @@ class LibraryScanner {
/**
* Scan grouped paths for library folder coming from Watcher
* @param {import('../models/Library')} library
* @param {import('../models/LibraryFolder')} folder
* @param {Record<string, string[]>} fileUpdateGroup
* @param {import('../models/Library')} library
* @param {import('../models/LibraryFolder')} folder
* @param {Record<string, string[]>} fileUpdateGroup
* @returns {Promise<Record<string,number>>}
*/
async scanFolderUpdates(library, folder, fileUpdateGroup) {
@ -471,7 +484,7 @@ class LibraryScanner {
for (const itemDir in updateGroup) {
if (isSingleMediaFile(fileUpdateGroup, itemDir)) continue // Media in root path
const itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/'))
const itemDirNestedFiles = fileUpdateGroup[itemDir].filter((b) => b.includes('/'))
if (!itemDirNestedFiles.length) continue
const firstNest = itemDirNestedFiles[0].split('/').shift()
@ -523,7 +536,15 @@ class LibraryScanner {
const potentialChildDirs = [fullPath]
for (let i = 0; i < itemDirParts.length; i++) {
potentialChildDirs.push(Path.posix.join(fileUtils.filePathToPOSIX(folder.path), itemDir.split('/').slice(0, -1 - i).join('/')))
potentialChildDirs.push(
Path.posix.join(
fileUtils.filePathToPOSIX(folder.path),
itemDir
.split('/')
.slice(0, -1 - i)
.join('/')
)
)
}
// Check if book dir group is already an item
@ -535,10 +556,7 @@ class LibraryScanner {
let updatedLibraryItemDetails = {}
if (!existingLibraryItem) {
const isSingleMedia = isSingleMediaFile(fileUpdateGroup, itemDir)
existingLibraryItem =
await findLibraryItemByItemToItemInoMatch(library.id, fullPath) ||
await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia) ||
await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir])
existingLibraryItem = (await findLibraryItemByItemToItemInoMatch(library.id, fullPath)) || (await findLibraryItemByItemToFileInoMatch(library.id, fullPath, isSingleMedia)) || (await findLibraryItemByFileToItemInoMatch(library.id, fullPath, isSingleMedia, fileUpdateGroup[itemDir]))
if (existingLibraryItem) {
// Update library item paths for scan
existingLibraryItem.path = fullPath
@ -603,7 +621,7 @@ class LibraryScanner {
module.exports = new LibraryScanner()
function ItemToFileInoMatch(libraryItem1, libraryItem2) {
return libraryItem1.isFile && libraryItem2.libraryFiles.some(lf => lf.ino === libraryItem1.ino)
return libraryItem1.isFile && libraryItem2.libraryFiles.some((lf) => lf.ino === libraryItem1.ino)
}
function ItemToItemInoMatch(libraryItem1, libraryItem2) {
@ -611,9 +629,7 @@ function ItemToItemInoMatch(libraryItem1, libraryItem2) {
}
function hasAudioFiles(fileUpdateGroup, itemDir) {
return isSingleMediaFile(fileUpdateGroup, itemDir) ?
scanUtils.checkFilepathIsAudioFile(fileUpdateGroup[itemDir]) :
fileUpdateGroup[itemDir].some(scanUtils.checkFilepathIsAudioFile)
return isSingleMediaFile(fileUpdateGroup, itemDir) ? scanUtils.checkFilepathIsAudioFile(fileUpdateGroup[itemDir]) : fileUpdateGroup[itemDir].some(scanUtils.checkFilepathIsAudioFile)
}
function isSingleMediaFile(fileUpdateGroup, itemDir) {
@ -627,8 +643,7 @@ async function findLibraryItemByItemToItemInoMatch(libraryId, fullPath) {
libraryId: libraryId,
ino: ino
})
if (existingLibraryItem)
Logger.debug(`[LibraryScanner] Found library item with matching inode "${ino}" at path "${existingLibraryItem.path}"`)
if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with matching inode "${ino}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
@ -637,18 +652,20 @@ async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingle
// check if it was moved from another folder by comparing the ino to the library files
const ino = await fileUtils.getIno(fullPath)
if (!ino) return null
const existingLibraryItem = await Database.libraryItemModel.findOneOld([
const existingLibraryItem = await Database.libraryItemModel.findOneOld(
[
{
libraryId: libraryId
},
sequelize.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode)'), {
[sequelize.Op.gt]: 0
})
],
{
libraryId: libraryId
},
sequelize.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode)'), {
[sequelize.Op.gt]: 0
})
], {
inode: ino
})
if (existingLibraryItem)
Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`)
inode: ino
}
)
if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
@ -667,7 +684,6 @@ async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingle
[sequelize.Op.in]: itemFileInos
}
})
if (existingLibraryItem)
Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`)
if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
}