This commit is contained in:
Jason Axley 2026-05-06 13:51:20 +02:00 committed by GitHub
commit f3a220c173
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 2441 additions and 304 deletions

View file

@ -2,14 +2,17 @@ const packageJson = require('../../package.json')
const { LogLevel } = require('../utils/constants')
const LibraryItem = require('../models/LibraryItem')
const globals = require('../utils/globals')
const LibraryFile = require('../objects/files/LibraryFile')
const LibraryScan = require('./LibraryScan')
const ScanLogger = require('./ScanLogger')
class LibraryItemScanData {
/**
* @typedef LibraryFileModifiedObject
* @typedef {Object} LibraryFileModifiedObject
* @property {LibraryItem.LibraryFileObject} old
* @property {LibraryItem.LibraryFileObject} new
* @param {{ libraryFolderId: any; libraryId: any; mediaType: any; ino: any; deviceId: any; mtimeMs: any; ctimeMs: any; birthtimeMs: any; path: any; relPath: any; isFile: any; mediaMetadata: any; libraryFiles: any; }} data
*/
constructor(data) {
/** @type {string} */
this.libraryFolderId = data.libraryFolderId
@ -19,6 +22,8 @@ class LibraryItemScanData {
this.mediaType = data.mediaType
/** @type {string} */
this.ino = data.ino
/** @type {string} */
this.deviceId = data.deviceId
/** @type {number} */
this.mtimeMs = data.mtimeMs
/** @type {number} */
@ -54,9 +59,10 @@ class LibraryItemScanData {
*/
get libraryItemObject() {
let size = 0
this.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
this.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
return {
ino: this.ino,
deviceId: this.deviceId,
path: this.path,
relPath: this.relPath,
mediaType: this.mediaType,
@ -80,107 +86,107 @@ class LibraryItemScanData {
/** @type {boolean} */
get hasAudioFileChanges() {
return (this.audioLibraryFilesRemoved.length + this.audioLibraryFilesAdded.length + this.audioLibraryFilesModified.length) > 0
return this.audioLibraryFilesRemoved.length + this.audioLibraryFilesAdded.length + this.audioLibraryFilesModified.length > 0
}
/** @type {LibraryFileModifiedObject[]} */
get audioLibraryFilesModified() {
return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFilesModified.filter((lf) => globals.SupportedAudioTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get audioLibraryFilesRemoved() {
return this.libraryFilesRemoved.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
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() || ''))
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() || ''))
return this.libraryFiles.filter((lf) => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryFileModifiedObject[]} */
get imageLibraryFilesModified() {
return this.libraryFilesModified.filter(lf => globals.SupportedImageTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFilesModified.filter((lf) => globals.SupportedImageTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get imageLibraryFilesRemoved() {
return this.libraryFilesRemoved.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFilesRemoved.filter((lf) => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get imageLibraryFilesAdded() {
return this.libraryFilesAdded.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFilesAdded.filter((lf) => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get imageLibraryFiles() {
return this.libraryFiles.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFiles.filter((lf) => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryFileModifiedObject[]} */
get ebookLibraryFilesModified() {
return this.libraryFilesModified.filter(lf => globals.SupportedEbookTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFilesModified.filter((lf) => globals.SupportedEbookTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get ebookLibraryFilesRemoved() {
return this.libraryFilesRemoved.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFilesRemoved.filter((lf) => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get ebookLibraryFilesAdded() {
return this.libraryFilesAdded.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFilesAdded.filter((lf) => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject[]} */
get ebookLibraryFiles() {
return this.libraryFiles.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
return this.libraryFiles.filter((lf) => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || ''))
}
/** @type {LibraryItem.LibraryFileObject} */
get descTxtLibraryFile() {
return this.libraryFiles.find(lf => lf.metadata.filename === 'desc.txt')
return this.libraryFiles.find((lf) => lf.metadata.filename === 'desc.txt')
}
/** @type {LibraryItem.LibraryFileObject} */
get readerTxtLibraryFile() {
return this.libraryFiles.find(lf => lf.metadata.filename === 'reader.txt')
return this.libraryFiles.find((lf) => lf.metadata.filename === 'reader.txt')
}
/** @type {LibraryItem.LibraryFileObject} */
get metadataAbsLibraryFile() {
return this.libraryFiles.find(lf => lf.metadata.filename === 'metadata.abs')
return this.libraryFiles.find((lf) => lf.metadata.filename === 'metadata.abs')
}
/** @type {LibraryItem.LibraryFileObject} */
get metadataJsonLibraryFile() {
return this.libraryFiles.find(lf => lf.metadata.filename === 'metadata.json')
return this.libraryFiles.find((lf) => lf.metadata.filename === 'metadata.json')
}
/** @type {LibraryItem.LibraryFileObject} */
get metadataOpfLibraryFile() {
return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.opf')
return this.libraryFiles.find((lf) => lf.metadata.ext.toLowerCase() === '.opf')
}
/** @type {LibraryItem.LibraryFileObject} */
get metadataNfoLibraryFile() {
return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.nfo')
return this.libraryFiles.find((lf) => lf.metadata.ext.toLowerCase() === '.nfo')
}
/**
*
* @param {LibraryItem} existingLibraryItem
* @param {import('./LibraryScan')} libraryScan
* @returns {boolean} true if changes found
*
* @param {LibraryItem} existingLibraryItem
* @param {import('./LibraryScan') | import('./ScanLogger')} libraryScan
* @returns {Promise<boolean>} true if changes found
*/
async checkLibraryItemData(existingLibraryItem, libraryScan) {
const keysToCompare = ['libraryFolderId', 'ino', 'path', 'relPath', 'isFile']
const keysToCompare = ['libraryFolderId', 'ino', 'deviceId', 'path', 'relPath', 'isFile']
this.hasChanges = false
this.hasPathChange = false
for (const key of keysToCompare) {
@ -219,28 +225,23 @@ class LibraryItemScanData {
this.libraryFilesRemoved = []
this.libraryFilesModified = []
let libraryFilesAdded = this.libraryFiles.map(lf => lf)
let libraryFilesAdded = this.libraryFiles.map((lf) => lf)
for (const existingLibraryFile of existingLibraryItem.libraryFiles) {
// Find matching library file using path first and fallback to using inode value
let matchingLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === existingLibraryFile.metadata.path)
if (!matchingLibraryFile) {
matchingLibraryFile = this.libraryFiles.find(lf => lf.ino === existingLibraryFile.ino)
if (matchingLibraryFile) {
libraryScan.addLog(LogLevel.INFO, `Library file with path "${existingLibraryFile.metadata.path}" not found, but found file with matching inode value "${existingLibraryFile.ino}" at path "${matchingLibraryFile.metadata.path}"`)
}
}
let matchingLibraryFile = this.findMatchingLibraryFileByPathOrInodeAndDeviceId(existingLibraryFile, libraryScan)
if (!matchingLibraryFile) { // Library file removed
if (!matchingLibraryFile) {
// Library file removed
libraryScan.addLog(LogLevel.INFO, `Library file "${existingLibraryFile.metadata.path}" was removed from library item "${existingLibraryItem.relPath}"`)
this.libraryFilesRemoved.push(existingLibraryFile)
existingLibraryItem.libraryFiles = existingLibraryItem.libraryFiles.filter(lf => lf !== existingLibraryFile)
existingLibraryItem.libraryFiles = existingLibraryItem.libraryFiles.filter((lf) => lf !== existingLibraryFile)
this.hasChanges = true
} else {
libraryFilesAdded = libraryFilesAdded.filter(lf => lf !== matchingLibraryFile)
libraryFilesAdded = libraryFilesAdded.filter((lf) => lf !== matchingLibraryFile)
let existingLibraryFileBefore = structuredClone(existingLibraryFile)
if (this.compareUpdateLibraryFile(existingLibraryItem.path, existingLibraryFile, matchingLibraryFile, libraryScan)) {
this.libraryFilesModified.push({old: existingLibraryFileBefore, new: existingLibraryFile})
if (LibraryItemScanData.compareUpdateLibraryFile(existingLibraryItem.path, existingLibraryFile, matchingLibraryFile, libraryScan)) {
this.libraryFilesModified.push({ old: existingLibraryFileBefore, new: existingLibraryFile })
this.hasChanges = true
}
}
@ -263,7 +264,7 @@ class LibraryItemScanData {
if (this.hasChanges) {
existingLibraryItem.size = 0
existingLibraryItem.libraryFiles.forEach((lf) => existingLibraryItem.size += lf.metadata.size)
existingLibraryItem.libraryFiles.forEach((lf) => (existingLibraryItem.size += lf.metadata.size))
existingLibraryItem.lastScan = Date.now()
existingLibraryItem.lastScanVersion = packageJson.version
@ -274,25 +275,25 @@ class LibraryItemScanData {
existingLibraryItem.changed('libraryFiles', true)
}
await existingLibraryItem.save()
return true
}
return false
return this.hasChanges
}
/**
* Update existing library file with scanned in library file data
* @param {string} libraryItemPath
* @param {LibraryItem.LibraryFileObject} existingLibraryFile
* @param {import('../objects/files/LibraryFile')} scannedLibraryFile
* @param {import('./LibraryScan')} libraryScan
* @param {LibraryItem.LibraryFileObject} existingLibraryFile
* @param {import('../objects/files/LibraryFile')} scannedLibraryFile
* @param {import('./LibraryScan') | import('./ScanLogger')} libraryScan
* @returns {boolean} false if no changes
*/
compareUpdateLibraryFile(libraryItemPath, existingLibraryFile, scannedLibraryFile, libraryScan) {
static compareUpdateLibraryFile(libraryItemPath, existingLibraryFile, scannedLibraryFile, libraryScan) {
let hasChanges = false
if (existingLibraryFile.ino !== scannedLibraryFile.ino) {
if (existingLibraryFile.ino !== scannedLibraryFile.ino && existingLibraryFile.deviceId !== scannedLibraryFile.deviceId) {
existingLibraryFile.ino = scannedLibraryFile.ino
existingLibraryFile.deviceId = scannedLibraryFile.deviceId
hasChanges = true
}
@ -315,40 +316,57 @@ class LibraryItemScanData {
return hasChanges
}
/**
* @returns {LibraryFile | undefined} if [existingLibraryFile] matches an existing libraryFile
* @param {LibraryItem.LibraryFileObject} [existingLibraryFile]
* @param {LibraryScan | ScanLogger} [libraryScan]
*/
findMatchingLibraryFileByPathOrInodeAndDeviceId(existingLibraryFile, libraryScan) {
if (!existingLibraryFile) return
let matchingLibraryFile = this.libraryFiles.find((lf) => lf.metadata.path === existingLibraryFile.metadata.path)
if (!matchingLibraryFile) {
matchingLibraryFile = this.libraryFiles.find((lf) => lf.ino === existingLibraryFile.ino && lf.deviceId === existingLibraryFile.deviceId)
if (matchingLibraryFile) {
libraryScan && libraryScan.addLog(LogLevel.INFO, `Library file with path "${existingLibraryFile.metadata.path}" not found, but found file with matching inode value "${existingLibraryFile.ino}" at path "${matchingLibraryFile.metadata.path}"`)
}
}
return matchingLibraryFile
}
/**
* Check if existing audio file on Book was removed
* @param {import('../models/Book').AudioFileObject} existingAudioFile
* @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)) {
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)
return this.audioLibraryFilesRemoved.some((af) => af.ino === existingAudioFile.ino && af.deviceId === existingAudioFile.deviceId)
}
/**
* Check if existing ebook file on Book was removed
* @param {import('../models/Book').EBookFileObject} ebookFile
* @param {import('../models/Book').EBookFileObject} ebookFile
* @returns {boolean} true if ebook file was removed
*/
checkEbookFileRemoved(ebookFile) {
if (!this.ebookLibraryFiles.length) return true
if (!this.ebookLibraryFilesRemoved.length) return false
if (this.ebookLibraryFiles.some(lf => lf.metadata.path === ebookFile.metadata.path)) {
return false
if (this.ebookLibraryFilesRemoved.some((lf) => lf.metadata.path === ebookFile.metadata.path)) {
return true
}
return !this.ebookLibraryFiles.some(lf => lf.ino === ebookFile.ino)
return this.ebookLibraryFilesRemoved.some((lf) => lf.ino === ebookFile.ino && lf.deviceId === ebookFile.deviceId)
}
/**
* Set data parsed from filenames
*
* @param {Object} bookMetadata
*
* @param {Object} bookMetadata
*/
setBookMetadataFromFilenames(bookMetadata) {
const keysToMap = ['title', 'subtitle', 'publishedYear', 'asin']
@ -374,4 +392,4 @@ class LibraryItemScanData {
}
}
}
module.exports = LibraryItemScanData
module.exports = LibraryItemScanData

View file

@ -23,7 +23,7 @@ class LibraryItemScanner {
* Scan single library item
*
* @param {string} libraryItemId
* @param {{relPath:string, path:string}} [updateLibraryItemDetails] used by watcher when item folder was renamed
* @param {{relPath:string, path:string, isFile: boolean}} [updateLibraryItemDetails] used by watcher when item folder was renamed
* @returns {number} ScanResult
*/
async scanLibraryItem(libraryItemId, updateLibraryItemDetails = null) {
@ -139,24 +139,11 @@ class LibraryItemScanner {
const newLibraryFile = new LibraryFile()
// fileItem.path is the relative path
await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path)
// TODO: BUGBUG - this is pushing the object, not a JSON string of the object like elsewhere
libraryFiles.push(newLibraryFile)
}
const libraryItemStats = await fileUtils.getFileTimestampsWithIno(libraryItemData.path)
return new LibraryItemScanData({
libraryFolderId: folder.id,
libraryId: library.id,
mediaType: library.mediaType,
ino: libraryItemStats.ino,
mtimeMs: libraryItemStats.mtimeMs || 0,
ctimeMs: libraryItemStats.ctimeMs || 0,
birthtimeMs: libraryItemStats.birthtimeMs || 0,
path: libraryItemData.path,
relPath: libraryItemData.relPath,
isFile: isSingleMediaItem,
mediaMetadata: libraryItemData.mediaMetadata || null,
libraryFiles
})
return await buildLibraryItemScanData(libraryItemData, folder, library, isSingleMediaItem, libraryFiles)
}
/**
@ -201,7 +188,7 @@ class LibraryItemScanner {
* @param {import('../models/Library')} library
* @param {import('../models/LibraryFolder')} folder
* @param {boolean} isSingleMediaItem
* @returns {Promise<LibraryItem>} ScanResult
* @returns {Promise<LibraryItem | null>} ScanResult
*/
async scanPotentialNewLibraryItem(libraryItemPath, library, folder, isSingleMediaItem) {
const libraryItemScanData = await this.getLibraryItemScanData(libraryItemPath, library, folder, isSingleMediaItem)
@ -219,3 +206,29 @@ class LibraryItemScanner {
}
}
module.exports = new LibraryItemScanner()
/**
* @param {{ path?: any; relPath?: any; mediaMetadata?: any; }} libraryItemData
* @param {import("../models/LibraryFolder")} folder
* @param {import("../models/Library")} library
* @param {boolean} isSingleMediaItem
* @param {LibraryFile[]} libraryFiles
*/
async function buildLibraryItemScanData(libraryItemData, folder, library, isSingleMediaItem, libraryFiles) {
const libraryItemStats = await fileUtils.getFileTimestampsWithIno(libraryItemData.path)
return new LibraryItemScanData({
libraryFolderId: folder.id,
libraryId: library.id,
mediaType: library.mediaType,
ino: libraryItemStats.ino,
deviceId: libraryItemStats.dev,
mtimeMs: libraryItemStats.mtimeMs || 0,
ctimeMs: libraryItemStats.ctimeMs || 0,
birthtimeMs: libraryItemStats.birthtimeMs || 0,
path: libraryItemData.path,
relPath: libraryItemData.relPath,
isFile: isSingleMediaItem,
mediaMetadata: libraryItemData.mediaMetadata || null,
libraryFiles
})
}

View file

@ -297,7 +297,7 @@ class LibraryScanner {
* Get scan data for library folder
* @param {import('../models/Library')} library
* @param {import('../models/LibraryFolder')} folder
* @returns {LibraryItemScanData[]}
* @returns {Promise<LibraryItemScanData[]>}
*/
async scanFolder(library, folder) {
const folderPath = fileUtils.filePathToPOSIX(folder.path)
@ -344,22 +344,7 @@ 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(createLibraryItemScanData(folder, library, libraryItemFolderStats, libraryItemData, isFile, fileObjs))
}
return items
}
@ -642,12 +627,25 @@ class LibraryScanner {
}
module.exports = new LibraryScanner()
/**
* @param {import("../models/LibraryItem") | LibraryItemScanData} libraryItem1
* @param {import("../models/LibraryItem") | LibraryItemScanData} libraryItem2
*/
function ItemToFileInoMatch(libraryItem1, libraryItem2) {
return libraryItem1.isFile && libraryItem2.libraryFiles.some((lf) => lf.ino === libraryItem1.ino)
return (
libraryItem1.isFile &&
libraryItem2.libraryFiles.some((lf) => {
return lf.ino === libraryItem1.ino && lf.deviceId === libraryItem1.deviceId
})
)
}
/**
* @param {LibraryItemScanData} libraryItem1
* @param {import("../models/LibraryItem")} libraryItem2
*/
function ItemToItemInoMatch(libraryItem1, libraryItem2) {
return libraryItem1.ino === libraryItem2.ino
return libraryItem1.ino === libraryItem2.ino && libraryItem1.deviceId === libraryItem2.deviceId
}
function hasAudioFiles(fileUpdateGroup, itemDir) {
@ -658,54 +656,111 @@ function isSingleMediaFile(fileUpdateGroup, itemDir) {
return itemDir === fileUpdateGroup[itemDir]
}
/**
* @param {UUIDV4} libraryId
* @param {string} fullPath
* @returns {Promise<import('../models/LibraryItem').LibraryItemExpanded | null>} library item that matches
*/
async function findLibraryItemByItemToItemInoMatch(libraryId, fullPath) {
const ino = await fileUtils.getIno(fullPath)
const deviceId = await fileUtils.getDeviceId(fullPath)
if (!ino) return null
const existingLibraryItem = await Database.libraryItemModel.findOneExpanded({
libraryId: libraryId,
ino: ino
ino: ino,
deviceId: deviceId
})
if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with matching inode "${ino}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
/**
* @param {UUIDV4} libraryId
* @param {string} fullPath
* @param {boolean} isSingleMedia
* @returns {Promise<import('../models/LibraryItem').LibraryItemExpanded | null>} library item that matches
*/
async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingleMedia) {
if (!isSingleMedia) return null
// check if it was moved from another folder by comparing the ino to the library files
const ino = await fileUtils.getIno(fullPath)
const deviceId = await fileUtils.getDeviceId(fullPath)
if (!ino) return null
const existingLibraryItem = await Database.libraryItemModel.findOneExpanded(
[
{
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.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode AND json_each.value->>"$.deviceId" = :deviceId)'), {
[sequelize.Op.gt]: 0
})
],
{
inode: ino
inode: ino,
deviceId: deviceId
}
)
if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
/**
* @param {UUIDV4} libraryId
* @param {string} fullPath
* @param {boolean} isSingleMedia
* @param {string[]} itemFiles
* @returns {Promise<import('../models/LibraryItem').LibraryItemExpanded | null>} library item that matches
*/
async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingleMedia, itemFiles) {
if (isSingleMedia) return null
// check if it was moved from the root folder by comparing the ino to the ino of the scanned files
// check if it was moved from the root folder by comparing the ino and deviceId to the ino and deviceId of the scanned files
let itemFileInos = []
for (const itemFile of itemFiles) {
const ino = await fileUtils.getIno(Path.posix.join(fullPath, itemFile))
if (ino) itemFileInos.push(ino)
const deviceId = await fileUtils.getDeviceId(Path.posix.join(fullPath, itemFile))
if (ino && deviceId) itemFileInos.push({ ino: ino, deviceId: deviceId })
}
if (!itemFileInos.length) return null
const existingLibraryItem = await Database.libraryItemModel.findOneExpanded({
libraryId: libraryId,
ino: {
[sequelize.Op.in]: itemFileInos
/** @type {import('../models/LibraryItem').LibraryItemExpanded | null} */
let existingLibraryItem = null
for (let item in itemFileInos) {
existingLibraryItem = await Database.libraryItemModel.findOneExpanded({
libraryId: libraryId,
[sequelize.Op.or]: itemFileInos
})
if (existingLibraryItem) {
break
}
})
}
if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`)
return existingLibraryItem
}
/**
* @param {{ id: any; libraryId: any; }} folder
* @param {{ mediaType: any; }} library
* @param {{ ino: any; dev: any; mtimeMs: any; ctimeMs: any; birthtimeMs: any; }} libraryItemFolderStats
* @param {{ path: any; relPath: any; mediaMetadata: any; }} libraryItemData
* @param {any} isFile
* @param {any} fileObjs
* @returns {LibraryItemScanData} new object
*/
function createLibraryItemScanData(folder, library, libraryItemFolderStats, libraryItemData, isFile, fileObjs) {
return new LibraryItemScanData({
libraryFolderId: folder.id,
libraryId: folder.libraryId,
mediaType: library.mediaType,
ino: libraryItemFolderStats.ino,
deviceId: libraryItemFolderStats.dev,
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
})
}

View file

@ -367,7 +367,7 @@ class PodcastScanner {
* @param {PodcastEpisode[]} podcastEpisodes Not the models for new podcasts
* @param {import('./LibraryItemScanData')} libraryItemData
* @param {import('./LibraryScan')} libraryScan
* @param {string} [existingLibraryItemId]
* @param {string | null} [existingLibraryItemId]
* @returns {Promise<PodcastMetadataObject>}
*/
async getPodcastMetadataFromScanData(podcastEpisodes, libraryItemData, libraryScan, existingLibraryItemId = null) {