mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-25 16:18:54 +01:00
Add new library item scanner
This commit is contained in:
parent
e63aab95d8
commit
42ff3d8314
@ -45,6 +45,11 @@ class Database {
|
|||||||
return this.models.library
|
return this.models.library
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {typeof import('./models/LibraryFolder')} */
|
||||||
|
get libraryFolderModel() {
|
||||||
|
return this.models.libraryFolder
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {typeof import('./models/Author')} */
|
/** @type {typeof import('./models/Author')} */
|
||||||
get authorModel() {
|
get authorModel() {
|
||||||
return this.models.author
|
return this.models.author
|
||||||
|
@ -16,7 +16,6 @@ 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')
|
||||||
|
|
||||||
@ -77,7 +76,6 @@ 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
|
||||||
|
@ -9,11 +9,13 @@ const libraryHelpers = require('../utils/libraryHelpers')
|
|||||||
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
||||||
const seriesFilters = require('../utils/queries/seriesFilters')
|
const seriesFilters = require('../utils/queries/seriesFilters')
|
||||||
|
const fileUtils = require('../utils/fileUtils')
|
||||||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const LibraryScanner = require('../scanner/LibraryScanner')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
const libraryFilters = require('../utils/queries/libraryFilters')
|
const libraryFilters = require('../utils/queries/libraryFilters')
|
||||||
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
|
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
|
||||||
@ -33,7 +35,7 @@ class LibraryController {
|
|||||||
// Validate folder paths exist or can be created & resolve rel paths
|
// Validate folder paths exist or can be created & resolve rel paths
|
||||||
// returns 400 if a folder fails to access
|
// returns 400 if a folder fails to access
|
||||||
newLibraryPayload.folders = newLibraryPayload.folders.map(f => {
|
newLibraryPayload.folders = newLibraryPayload.folders.map(f => {
|
||||||
f.fullPath = Path.resolve(f.fullPath)
|
f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath))
|
||||||
return f
|
return f
|
||||||
})
|
})
|
||||||
for (const folder of newLibraryPayload.folders) {
|
for (const folder of newLibraryPayload.folders) {
|
||||||
@ -87,7 +89,7 @@ class LibraryController {
|
|||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const includeArray = (req.query.include || '').split(',')
|
const includeArray = (req.query.include || '').split(',')
|
||||||
if (includeArray.includes('filterdata')) {
|
if (includeArray.includes('filterdata')) {
|
||||||
const filterdata = await libraryFilters.getFilterData(req.library)
|
const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
filterdata,
|
filterdata,
|
||||||
@ -119,7 +121,7 @@ class LibraryController {
|
|||||||
const newFolderPaths = []
|
const newFolderPaths = []
|
||||||
req.body.folders = req.body.folders.map(f => {
|
req.body.folders = req.body.folders.map(f => {
|
||||||
if (!f.id) {
|
if (!f.id) {
|
||||||
f.fullPath = Path.resolve(f.fullPath)
|
f.fullPath = fileUtils.filePathToPOSIX(Path.resolve(f.fullPath))
|
||||||
newFolderPaths.push(f.fullPath)
|
newFolderPaths.push(f.fullPath)
|
||||||
}
|
}
|
||||||
return f
|
return f
|
||||||
@ -670,7 +672,7 @@ class LibraryController {
|
|||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
*/
|
*/
|
||||||
async getLibraryFilterData(req, res) {
|
async getLibraryFilterData(req, res) {
|
||||||
const filterData = await libraryFilters.getFilterData(req.library)
|
const filterData = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
|
||||||
res.json(filterData)
|
res.json(filterData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -976,9 +978,13 @@ class LibraryController {
|
|||||||
forceRescan: req.query.force == 1
|
forceRescan: req.query.force == 1
|
||||||
}
|
}
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
await this.scanner.scan(req.library, options)
|
if (req.library.mediaType === 'podcast') {
|
||||||
// TODO: New library scanner
|
// TODO: New library scanner for podcasts
|
||||||
// await this.libraryScanner.scan(req.library, options)
|
await this.scanner.scan(req.library, options)
|
||||||
|
} else {
|
||||||
|
await 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')
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ const zipHelpers = require('../utils/zipHelpers')
|
|||||||
const { reqSupportsWebp } = require('../utils/index')
|
const { reqSupportsWebp } = require('../utils/index')
|
||||||
const { ScanResult } = require('../utils/constants')
|
const { ScanResult } = require('../utils/constants')
|
||||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||||
|
const LibraryItemScanner = require('../scanner/LibraryItemScanner')
|
||||||
|
|
||||||
class LibraryItemController {
|
class LibraryItemController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -475,7 +476,14 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.scanner.scanLibraryItemByRequest(req.libraryItem)
|
let result = 0
|
||||||
|
if (req.libraryItem.isPodcast) {
|
||||||
|
// TODO: New library item scanner for podcast
|
||||||
|
result = await this.scanner.scanLibraryItemByRequest(req.libraryItem)
|
||||||
|
} else {
|
||||||
|
result = await LibraryItemScanner.scanLibraryItem(req.libraryItem.id)
|
||||||
|
}
|
||||||
|
|
||||||
await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
|
await Database.resetLibraryIssuesFilterData(req.libraryItem.libraryId)
|
||||||
res.json({
|
res.json({
|
||||||
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
|
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
|
||||||
|
@ -2,6 +2,16 @@ const { DataTypes, Model } = require('sequelize')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const oldLibrary = require('../objects/Library')
|
const oldLibrary = require('../objects/Library')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef LibrarySettingsObject
|
||||||
|
* @property {number} coverAspectRatio BookCoverAspectRatio
|
||||||
|
* @property {boolean} disableWatcher
|
||||||
|
* @property {boolean} skipMatchingMediaWithAsin
|
||||||
|
* @property {boolean} skipMatchingMediaWithIsbn
|
||||||
|
* @property {string} autoScanCronExpression
|
||||||
|
* @property {boolean} audiobooksOnly
|
||||||
|
* @property {boolean} hideSingleBookSeries Do not show series that only have 1 book
|
||||||
|
*/
|
||||||
|
|
||||||
class Library extends Model {
|
class Library extends Model {
|
||||||
constructor(values, options) {
|
constructor(values, options) {
|
||||||
@ -23,7 +33,7 @@ class Library extends Model {
|
|||||||
this.lastScan
|
this.lastScan
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
this.lastScanVersion
|
this.lastScanVersion
|
||||||
/** @type {Object} */
|
/** @type {LibrarySettingsObject} */
|
||||||
this.settings
|
this.settings
|
||||||
/** @type {Object} */
|
/** @type {Object} */
|
||||||
this.extraData
|
this.extraData
|
||||||
|
@ -41,7 +41,6 @@ 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
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const uuidv4 = require("uuid").v4
|
const uuidv4 = require("uuid").v4
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { Sequelize } = require('sequelize')
|
const sequelize = require('sequelize')
|
||||||
const { LogLevel } = require('../utils/constants')
|
const { LogLevel } = require('../utils/constants')
|
||||||
const { getTitleIgnorePrefix, areEquivalent } = require('../utils/index')
|
const { getTitleIgnorePrefix, areEquivalent } = require('../utils/index')
|
||||||
const { parseOpfMetadataXML } = require('../utils/parsers/parseOpfMetadata')
|
const { parseOpfMetadataXML } = require('../utils/parsers/parseOpfMetadata')
|
||||||
@ -14,6 +14,7 @@ const { readTextFile, filePathToPOSIX, getFileTimestampsWithIno } = require('../
|
|||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
const CoverManager = require('../managers/CoverManager')
|
const CoverManager = require('../managers/CoverManager')
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
const fsExtra = require("../libs/fsExtra")
|
const fsExtra = require("../libs/fsExtra")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,10 +46,11 @@ class BookScanner {
|
|||||||
/**
|
/**
|
||||||
* @param {import('../models/LibraryItem')} existingLibraryItem
|
* @param {import('../models/LibraryItem')} existingLibraryItem
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
|
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @returns {import('../models/LibraryItem')}
|
* @returns {Promise<import('../models/LibraryItem')>}
|
||||||
*/
|
*/
|
||||||
async rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, libraryScan) {
|
async rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
|
||||||
/** @type {import('../models/Book')} */
|
/** @type {import('../models/Book')} */
|
||||||
const media = await existingLibraryItem.getMedia({
|
const media = await existingLibraryItem.getMedia({
|
||||||
include: [
|
include: [
|
||||||
@ -146,13 +148,13 @@ class BookScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if ebook was removed
|
// Check if ebook was removed
|
||||||
if (media.ebookFile && (libraryScan.library.settings.audiobooksOnly || libraryItemData.checkEbookFileRemoved(media.ebookFile))) {
|
if (media.ebookFile && (librarySettings.audiobooksOnly || libraryItemData.checkEbookFileRemoved(media.ebookFile))) {
|
||||||
media.ebookFile = null
|
media.ebookFile = null
|
||||||
hasMediaChanges = true
|
hasMediaChanges = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if ebook is not set and ebooks were found
|
// Check if ebook is not set and ebooks were found
|
||||||
if (!media.ebookFile && !libraryScan.library.settings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) {
|
if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) {
|
||||||
// Prefer to use an epub ebook then fallback to the first ebook found
|
// Prefer to use an epub ebook then fallback to the first ebook found
|
||||||
let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub')
|
let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub')
|
||||||
if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0]
|
if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0]
|
||||||
@ -179,7 +181,7 @@ class BookScanner {
|
|||||||
// Check for authors added
|
// Check for authors added
|
||||||
for (const authorName of bookMetadata.authors) {
|
for (const authorName of bookMetadata.authors) {
|
||||||
if (!media.authors.some(au => au.name === authorName)) {
|
if (!media.authors.some(au => au.name === authorName)) {
|
||||||
const existingAuthor = Database.libraryFilterData[libraryScan.libraryId].authors.find(au => au.name === authorName)
|
const existingAuthor = Database.libraryFilterData[libraryItemData.libraryId].authors.find(au => au.name === authorName)
|
||||||
if (existingAuthor) {
|
if (existingAuthor) {
|
||||||
await Database.bookAuthorModel.create({
|
await Database.bookAuthorModel.create({
|
||||||
bookId: media.id,
|
bookId: media.id,
|
||||||
@ -191,10 +193,10 @@ class BookScanner {
|
|||||||
const newAuthor = await Database.authorModel.create({
|
const newAuthor = await Database.authorModel.create({
|
||||||
name: authorName,
|
name: authorName,
|
||||||
lastFirst: parseNameString.nameToLastFirst(authorName),
|
lastFirst: parseNameString.nameToLastFirst(authorName),
|
||||||
libraryId: libraryScan.libraryId
|
libraryId: libraryItemData.libraryId
|
||||||
})
|
})
|
||||||
await media.addAuthor(newAuthor)
|
await media.addAuthor(newAuthor)
|
||||||
Database.addAuthorToFilterData(libraryScan.libraryId, newAuthor.name, newAuthor.id)
|
Database.addAuthorToFilterData(libraryItemData.libraryId, newAuthor.name, newAuthor.id)
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added new author "${authorName}"`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added new author "${authorName}"`)
|
||||||
authorsUpdated = true
|
authorsUpdated = true
|
||||||
}
|
}
|
||||||
@ -213,7 +215,7 @@ class BookScanner {
|
|||||||
// Check for series added
|
// Check for series added
|
||||||
for (const seriesObj of bookMetadata.series) {
|
for (const seriesObj of bookMetadata.series) {
|
||||||
if (!media.series.some(se => se.name === seriesObj.name)) {
|
if (!media.series.some(se => se.name === seriesObj.name)) {
|
||||||
const existingSeries = Database.libraryFilterData[libraryScan.libraryId].series.find(se => se.name === seriesObj.name)
|
const existingSeries = Database.libraryFilterData[libraryItemData.libraryId].series.find(se => se.name === seriesObj.name)
|
||||||
if (existingSeries) {
|
if (existingSeries) {
|
||||||
await Database.bookSeriesModel.create({
|
await Database.bookSeriesModel.create({
|
||||||
bookId: media.id,
|
bookId: media.id,
|
||||||
@ -226,10 +228,10 @@ class BookScanner {
|
|||||||
const newSeries = await Database.seriesModel.create({
|
const newSeries = await Database.seriesModel.create({
|
||||||
name: seriesObj.name,
|
name: seriesObj.name,
|
||||||
nameIgnorePrefix: getTitleIgnorePrefix(seriesObj.name),
|
nameIgnorePrefix: getTitleIgnorePrefix(seriesObj.name),
|
||||||
libraryId: libraryScan.libraryId
|
libraryId: libraryItemData.libraryId
|
||||||
})
|
})
|
||||||
await media.addSeries(newSeries)
|
await media.addSeries(newSeries, { through: { sequence: seriesObj.sequence } })
|
||||||
Database.addSeriesToFilterData(libraryScan.libraryId, newSeries.name, newSeries.id)
|
Database.addSeriesToFilterData(libraryItemData.libraryId, newSeries.name, newSeries.id)
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added new series "${seriesObj.name}"${seriesObj.sequence ? ` with sequence "${seriesObj.sequence}"` : ''}`)
|
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" added new series "${seriesObj.name}"${seriesObj.sequence ? ` with sequence "${seriesObj.sequence}"` : ''}`)
|
||||||
seriesUpdated = true
|
seriesUpdated = true
|
||||||
}
|
}
|
||||||
@ -304,7 +306,7 @@ class BookScanner {
|
|||||||
media.authors = await media.getAuthors({
|
media.authors = await media.getAuthors({
|
||||||
joinTableAttributes: ['createdAt'],
|
joinTableAttributes: ['createdAt'],
|
||||||
order: [
|
order: [
|
||||||
Sequelize.literal(`bookAuthor.createdAt ASC`)
|
sequelize.literal(`bookAuthor.createdAt ASC`)
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -312,7 +314,7 @@ class BookScanner {
|
|||||||
media.series = await media.getSeries({
|
media.series = await media.getSeries({
|
||||||
joinTableAttributes: ['sequence', 'createdAt'],
|
joinTableAttributes: ['sequence', 'createdAt'],
|
||||||
order: [
|
order: [
|
||||||
Sequelize.literal(`bookSeries.createdAt ASC`)
|
sequelize.literal(`bookSeries.createdAt ASC`)
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -356,16 +358,17 @@ class BookScanner {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
|
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @returns {import('../models/LibraryItem')}
|
* @returns {import('../models/LibraryItem')}
|
||||||
*/
|
*/
|
||||||
async scanNewBookLibraryItem(libraryItemData, libraryScan) {
|
async scanNewBookLibraryItem(libraryItemData, librarySettings, libraryScan) {
|
||||||
// Scan audio files found
|
// Scan audio files found
|
||||||
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(libraryScan.libraryMediaType, libraryItemData, libraryItemData.audioLibraryFiles)
|
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(libraryItemData.mediaType, libraryItemData, libraryItemData.audioLibraryFiles)
|
||||||
scannedAudioFiles = AudioFileScanner.runSmartTrackOrder(libraryItemData.relPath, scannedAudioFiles)
|
scannedAudioFiles = AudioFileScanner.runSmartTrackOrder(libraryItemData.relPath, scannedAudioFiles)
|
||||||
|
|
||||||
// Find ebook file (prefer epub)
|
// Find ebook file (prefer epub)
|
||||||
let ebookLibraryFile = libraryScan.library.settings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0]
|
let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0]
|
||||||
|
|
||||||
// Do not add library items that have no valid audio files and no ebook file
|
// Do not add library items that have no valid audio files and no ebook file
|
||||||
if (!ebookLibraryFile && !scannedAudioFiles.length) {
|
if (!ebookLibraryFile && !scannedAudioFiles.length) {
|
||||||
@ -393,7 +396,7 @@ class BookScanner {
|
|||||||
}
|
}
|
||||||
if (bookMetadata.authors.length) {
|
if (bookMetadata.authors.length) {
|
||||||
for (const authorName of bookMetadata.authors) {
|
for (const authorName of bookMetadata.authors) {
|
||||||
const matchingAuthor = Database.libraryFilterData[libraryScan.libraryId].authors.find(au => au.name === authorName)
|
const matchingAuthor = Database.libraryFilterData[libraryItemData.libraryId].authors.find(au => au.name === authorName)
|
||||||
if (matchingAuthor) {
|
if (matchingAuthor) {
|
||||||
bookObject.bookAuthors.push({
|
bookObject.bookAuthors.push({
|
||||||
authorId: matchingAuthor.id
|
authorId: matchingAuthor.id
|
||||||
@ -402,7 +405,7 @@ class BookScanner {
|
|||||||
// New author
|
// New author
|
||||||
bookObject.bookAuthors.push({
|
bookObject.bookAuthors.push({
|
||||||
author: {
|
author: {
|
||||||
libraryId: libraryScan.libraryId,
|
libraryId: libraryItemData.libraryId,
|
||||||
name: authorName,
|
name: authorName,
|
||||||
lastFirst: parseNameString.nameToLastFirst(authorName)
|
lastFirst: parseNameString.nameToLastFirst(authorName)
|
||||||
}
|
}
|
||||||
@ -413,7 +416,7 @@ class BookScanner {
|
|||||||
if (bookMetadata.series.length) {
|
if (bookMetadata.series.length) {
|
||||||
for (const seriesObj of bookMetadata.series) {
|
for (const seriesObj of bookMetadata.series) {
|
||||||
if (!seriesObj.name) continue
|
if (!seriesObj.name) continue
|
||||||
const matchingSeries = Database.libraryFilterData[libraryScan.libraryId].series.find(se => se.name === seriesObj.name)
|
const matchingSeries = Database.libraryFilterData[libraryItemData.libraryId].series.find(se => se.name === seriesObj.name)
|
||||||
if (matchingSeries) {
|
if (matchingSeries) {
|
||||||
bookObject.bookSeries.push({
|
bookObject.bookSeries.push({
|
||||||
seriesId: matchingSeries.id,
|
seriesId: matchingSeries.id,
|
||||||
@ -425,7 +428,7 @@ class BookScanner {
|
|||||||
series: {
|
series: {
|
||||||
name: seriesObj.name,
|
name: seriesObj.name,
|
||||||
nameIgnorePrefix: getTitleIgnorePrefix(seriesObj.name),
|
nameIgnorePrefix: getTitleIgnorePrefix(seriesObj.name),
|
||||||
libraryId: libraryScan.libraryId
|
libraryId: libraryItemData.libraryId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -476,22 +479,22 @@ class BookScanner {
|
|||||||
if (libraryItem.book.bookSeries?.length) {
|
if (libraryItem.book.bookSeries?.length) {
|
||||||
for (const bs of libraryItem.book.bookSeries) {
|
for (const bs of libraryItem.book.bookSeries) {
|
||||||
if (bs.series) {
|
if (bs.series) {
|
||||||
Database.addSeriesToFilterData(libraryScan.libraryId, bs.series.name, bs.series.id)
|
Database.addSeriesToFilterData(libraryItemData.libraryId, bs.series.name, bs.series.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (libraryItem.book.bookAuthors?.length) {
|
if (libraryItem.book.bookAuthors?.length) {
|
||||||
for (const ba of libraryItem.book.bookAuthors) {
|
for (const ba of libraryItem.book.bookAuthors) {
|
||||||
if (ba.author) {
|
if (ba.author) {
|
||||||
Database.addAuthorToFilterData(libraryScan.libraryId, ba.author.name, ba.author.id)
|
Database.addAuthorToFilterData(libraryItemData.libraryId, ba.author.name, ba.author.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Database.addNarratorsToFilterData(libraryScan.libraryId, libraryItem.book.narrators)
|
Database.addNarratorsToFilterData(libraryItemData.libraryId, libraryItem.book.narrators)
|
||||||
Database.addGenresToFilterData(libraryScan.libraryId, libraryItem.book.genres)
|
Database.addGenresToFilterData(libraryItemData.libraryId, libraryItem.book.genres)
|
||||||
Database.addTagsToFilterData(libraryScan.libraryId, libraryItem.book.tags)
|
Database.addTagsToFilterData(libraryItemData.libraryId, libraryItem.book.tags)
|
||||||
Database.addPublisherToFilterData(libraryScan.libraryId, libraryItem.book.publisher)
|
Database.addPublisherToFilterData(libraryItemData.libraryId, libraryItem.book.publisher)
|
||||||
Database.addLanguageToFilterData(libraryScan.libraryId, libraryItem.book.language)
|
Database.addLanguageToFilterData(libraryItemData.libraryId, libraryItem.book.language)
|
||||||
|
|
||||||
// Load for emitting to client
|
// Load for emitting to client
|
||||||
libraryItem.media = await libraryItem.getMedia({
|
libraryItem.media = await libraryItem.getMedia({
|
||||||
@ -949,5 +952,78 @@ class BookScanner {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check authors that were removed from a book and remove them if they no longer have any books
|
||||||
|
* keep authors without books that have a asin, description or imagePath
|
||||||
|
* @param {string} libraryId
|
||||||
|
* @param {import('./ScanLogger')} scanLogger
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
async checkAuthorsRemovedFromBooks(libraryId, scanLogger) {
|
||||||
|
const bookAuthorsToRemove = (await Database.authorModel.findAll({
|
||||||
|
where: [
|
||||||
|
{
|
||||||
|
id: scanLogger.authorsRemovedFromBooks,
|
||||||
|
asin: {
|
||||||
|
[sequelize.Op.or]: [null, ""]
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
[sequelize.Op.or]: [null, ""]
|
||||||
|
},
|
||||||
|
imagePath: {
|
||||||
|
[sequelize.Op.or]: [null, ""]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
|
||||||
|
],
|
||||||
|
attributes: ['id'],
|
||||||
|
raw: true
|
||||||
|
})).map(au => au.id)
|
||||||
|
if (bookAuthorsToRemove.length) {
|
||||||
|
await Database.authorModel.destroy({
|
||||||
|
where: {
|
||||||
|
id: bookAuthorsToRemove
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bookAuthorsToRemove.forEach((authorId) => {
|
||||||
|
Database.removeAuthorFromFilterData(libraryId, authorId)
|
||||||
|
// TODO: Clients were expecting full author in payload but its unnecessary
|
||||||
|
SocketAuthority.emitter('author_removed', { id: authorId, libraryId })
|
||||||
|
})
|
||||||
|
scanLogger.addLog(LogLevel.INFO, `Removed ${bookAuthorsToRemove.length} authors`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check series that were removed from books and remove them if they no longer have any books
|
||||||
|
* @param {string} libraryId
|
||||||
|
* @param {import('./ScanLogger')} scanLogger
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
async checkSeriesRemovedFromBooks(libraryId, scanLogger) {
|
||||||
|
const bookSeriesToRemove = (await Database.seriesModel.findAll({
|
||||||
|
where: [
|
||||||
|
{
|
||||||
|
id: scanLogger.seriesRemovedFromBooks
|
||||||
|
},
|
||||||
|
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
|
||||||
|
],
|
||||||
|
attributes: ['id'],
|
||||||
|
raw: true
|
||||||
|
})).map(se => se.id)
|
||||||
|
if (bookSeriesToRemove.length) {
|
||||||
|
await Database.seriesModel.destroy({
|
||||||
|
where: {
|
||||||
|
id: bookSeriesToRemove
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bookSeriesToRemove.forEach((seriesId) => {
|
||||||
|
Database.removeSeriesFromFilterData(libraryId, seriesId)
|
||||||
|
SocketAuthority.emitter('series_removed', { id: seriesId, libraryId })
|
||||||
|
})
|
||||||
|
scanLogger.addLog(LogLevel.INFO, `Removed ${bookSeriesToRemove.length} series`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new BookScanner()
|
module.exports = new BookScanner()
|
190
server/scanner/LibraryItemScanner.js
Normal file
190
server/scanner/LibraryItemScanner.js
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
const Path = require('path')
|
||||||
|
const { LogLevel, ScanResult } = require('../utils/constants')
|
||||||
|
|
||||||
|
const fileUtils = require('../utils/fileUtils')
|
||||||
|
const scanUtils = require('../utils/scandir')
|
||||||
|
const libraryFilters = require('../utils/queries/libraryFilters')
|
||||||
|
const Database = require('../Database')
|
||||||
|
const LibraryScan = require('./LibraryScan')
|
||||||
|
const LibraryItemScanData = require('./LibraryItemScanData')
|
||||||
|
const BookScanner = require('./BookScanner')
|
||||||
|
const ScanLogger = require('./ScanLogger')
|
||||||
|
const LibraryItem = require('../models/LibraryItem')
|
||||||
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
|
||||||
|
class LibraryItemScanner {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan single library item
|
||||||
|
*
|
||||||
|
* @param {string} libraryItemId
|
||||||
|
* @returns {number} ScanResult
|
||||||
|
*/
|
||||||
|
async scanLibraryItem(libraryItemId) {
|
||||||
|
// TODO: Add task manager
|
||||||
|
const libraryItem = await Database.libraryItemModel.findByPk(libraryItemId)
|
||||||
|
if (!libraryItem) {
|
||||||
|
Logger.error(`[LibraryItemScanner] Library item not found "${libraryItemId}"`)
|
||||||
|
return ScanResult.NOTHING
|
||||||
|
}
|
||||||
|
|
||||||
|
const library = await Database.libraryModel.findByPk(libraryItem.libraryId, {
|
||||||
|
include: {
|
||||||
|
model: Database.libraryFolderModel,
|
||||||
|
where: {
|
||||||
|
id: libraryItem.libraryFolderId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!library) {
|
||||||
|
Logger.error(`[LibraryItemScanner] Library "${libraryItem.libraryId}" not found for library item "${libraryItem.id}"`)
|
||||||
|
return ScanResult.NOTHING
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure library filter data is set
|
||||||
|
// this is used to check for existing authors & series
|
||||||
|
await libraryFilters.getFilterData(library.mediaType, library.id)
|
||||||
|
|
||||||
|
const scanLogger = new ScanLogger()
|
||||||
|
scanLogger.verbose = true
|
||||||
|
scanLogger.setData('libraryItem', libraryItemId)
|
||||||
|
|
||||||
|
const libraryItemPath = fileUtils.filePathToPOSIX(libraryItem.path)
|
||||||
|
const folder = library.libraryFolders[0]
|
||||||
|
const libraryItemScanData = await this.getLibraryItemScanData(libraryItemPath, library, folder, false)
|
||||||
|
|
||||||
|
if (await libraryItemScanData.checkLibraryItemData(libraryItem, scanLogger)) {
|
||||||
|
if (libraryItemScanData.hasLibraryFileChanges || libraryItemScanData.hasPathChange) {
|
||||||
|
const expandedLibraryItem = await this.rescanLibraryItem(libraryItem, libraryItemScanData, library.settings, scanLogger)
|
||||||
|
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(expandedLibraryItem)
|
||||||
|
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||||
|
|
||||||
|
await this.checkAuthorsAndSeriesRemovedFromBooks(library.id, scanLogger)
|
||||||
|
} else {
|
||||||
|
// TODO: Temporary while using old model to socket emit
|
||||||
|
const oldLibraryItem = await Database.libraryItemModel.getOldById(libraryItem.id)
|
||||||
|
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScanResult.UPDATED
|
||||||
|
}
|
||||||
|
return ScanResult.UPTODATE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove empty authors and series
|
||||||
|
* @param {string} libraryId
|
||||||
|
* @param {ScanLogger} scanLogger
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
async checkAuthorsAndSeriesRemovedFromBooks(libraryId, scanLogger) {
|
||||||
|
if (scanLogger.authorsRemovedFromBooks.length) {
|
||||||
|
await BookScanner.checkAuthorsRemovedFromBooks(libraryId, scanLogger)
|
||||||
|
}
|
||||||
|
if (scanLogger.seriesRemovedFromBooks.length) {
|
||||||
|
await BookScanner.checkSeriesRemovedFromBooks(libraryId, scanLogger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} libraryItemPath
|
||||||
|
* @param {import('../models/Library')} library
|
||||||
|
* @param {import('../models/LibraryFolder')} folder
|
||||||
|
* @param {boolean} isSingleMediaItem
|
||||||
|
* @returns {Promise<LibraryItemScanData>}
|
||||||
|
*/
|
||||||
|
async getLibraryItemScanData(libraryItemPath, library, folder, isSingleMediaItem) {
|
||||||
|
const libraryFolderPath = fileUtils.filePathToPOSIX(folder.path)
|
||||||
|
const libraryItemDir = libraryItemPath.replace(libraryFolderPath, '').slice(1)
|
||||||
|
|
||||||
|
let libraryItemData = {}
|
||||||
|
|
||||||
|
let fileItems = []
|
||||||
|
|
||||||
|
if (isSingleMediaItem) { // Single media item in root of folder
|
||||||
|
fileItems = [
|
||||||
|
{
|
||||||
|
fullpath: libraryItemPath,
|
||||||
|
path: libraryItemDir // actually the relPath (only filename here)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
libraryItemData = {
|
||||||
|
path: libraryItemPath, // full path
|
||||||
|
relPath: libraryItemDir, // only filename
|
||||||
|
mediaMetadata: {
|
||||||
|
title: Path.basename(libraryItemDir, Path.extname(libraryItemDir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileItems = await fileUtils.recurseFiles(libraryItemPath)
|
||||||
|
libraryItemData = scanUtils.getDataFromMediaDir(library.mediaType, libraryFolderPath, libraryItemDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryFiles = []
|
||||||
|
for (let i = 0; i < fileItems.length; i++) {
|
||||||
|
const fileItem = fileItems[i]
|
||||||
|
const newLibraryFile = new LibraryFile()
|
||||||
|
// fileItem.path is the relative path
|
||||||
|
await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {import('../models/LibraryItem')} existingLibraryItem
|
||||||
|
* @param {LibraryItemScanData} libraryItemData
|
||||||
|
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||||
|
* @param {LibraryScan} libraryScan
|
||||||
|
* @returns {Promise<LibraryItem>}
|
||||||
|
*/
|
||||||
|
async rescanLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
|
||||||
|
if (existingLibraryItem.mediaType === 'book') {
|
||||||
|
const libraryItem = await BookScanner.rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan)
|
||||||
|
return libraryItem
|
||||||
|
} else {
|
||||||
|
// TODO: Scan updated podcast
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {LibraryItemScanData} libraryItemData
|
||||||
|
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||||
|
* @param {LibraryScan} libraryScan
|
||||||
|
* @returns {Promise<LibraryItem>}
|
||||||
|
*/
|
||||||
|
async scanNewLibraryItem(libraryItemData, librarySettings, libraryScan) {
|
||||||
|
if (libraryScan.libraryMediaType === 'book') {
|
||||||
|
const newLibraryItem = await BookScanner.scanNewBookLibraryItem(libraryItemData, librarySettings, libraryScan)
|
||||||
|
if (newLibraryItem) {
|
||||||
|
libraryScan.addLog(LogLevel.INFO, `Created new library item "${newLibraryItem.relPath}"`)
|
||||||
|
}
|
||||||
|
return newLibraryItem
|
||||||
|
} else {
|
||||||
|
// TODO: Scan new podcast
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new LibraryItemScanner()
|
@ -7,18 +7,15 @@ const Database = require('../Database')
|
|||||||
const fs = require('../libs/fsExtra')
|
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 { LogLevel } = require('../utils/constants')
|
||||||
const libraryFilters = require('../utils/queries/libraryFilters')
|
const libraryFilters = require('../utils/queries/libraryFilters')
|
||||||
|
const LibraryItemScanner = require('./LibraryItemScanner')
|
||||||
const ScanOptions = require('./ScanOptions')
|
const ScanOptions = require('./ScanOptions')
|
||||||
const LibraryScan = require('./LibraryScan')
|
const LibraryScan = require('./LibraryScan')
|
||||||
const LibraryItemScanData = require('./LibraryItemScanData')
|
const LibraryItemScanData = require('./LibraryItemScanData')
|
||||||
const BookScanner = require('./BookScanner')
|
|
||||||
|
|
||||||
class LibraryScanner {
|
class LibraryScanner {
|
||||||
constructor(coverManager, taskManager) {
|
constructor() {
|
||||||
this.coverManager = coverManager
|
|
||||||
this.taskManager = taskManager
|
|
||||||
|
|
||||||
this.cancelLibraryScan = {}
|
this.cancelLibraryScan = {}
|
||||||
this.librariesScanning = []
|
this.librariesScanning = []
|
||||||
}
|
}
|
||||||
@ -93,7 +90,7 @@ class LibraryScanner {
|
|||||||
async scanLibrary(libraryScan) {
|
async scanLibrary(libraryScan) {
|
||||||
// Make sure library filter data is set
|
// Make sure library filter data is set
|
||||||
// this is used to check for existing authors & series
|
// this is used to check for existing authors & series
|
||||||
await libraryFilters.getFilterData(libraryScan.library)
|
await libraryFilters.getFilterData(libraryScan.library.mediaType, libraryScan.libraryId)
|
||||||
|
|
||||||
/** @type {LibraryItemScanData[]} */
|
/** @type {LibraryItemScanData[]} */
|
||||||
let libraryItemDataFound = []
|
let libraryItemDataFound = []
|
||||||
@ -151,7 +148,7 @@ class LibraryScanner {
|
|||||||
if (await libraryItemData.checkLibraryItemData(existingLibraryItem, libraryScan)) {
|
if (await libraryItemData.checkLibraryItemData(existingLibraryItem, libraryScan)) {
|
||||||
libraryScan.resultsUpdated++
|
libraryScan.resultsUpdated++
|
||||||
if (libraryItemData.hasLibraryFileChanges || libraryItemData.hasPathChange) {
|
if (libraryItemData.hasLibraryFileChanges || libraryItemData.hasPathChange) {
|
||||||
const libraryItem = await this.rescanLibraryItem(existingLibraryItem, libraryItemData, libraryScan)
|
const libraryItem = await LibraryItemScanner.rescanLibraryItem(existingLibraryItem, libraryItemData, libraryScan.library.settings, libraryScan)
|
||||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||||
oldLibraryItemsUpdated.push(oldLibraryItem)
|
oldLibraryItemsUpdated.push(oldLibraryItem)
|
||||||
} else {
|
} else {
|
||||||
@ -177,68 +174,8 @@ class LibraryScanner {
|
|||||||
SocketAuthority.emitter('items_updated', oldLibraryItemsUpdated.map(li => li.toJSONExpanded()))
|
SocketAuthority.emitter('items_updated', oldLibraryItemsUpdated.map(li => li.toJSONExpanded()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authors that were removed from a book and remove them if they no longer have any books
|
// Authors and series that were removed from books should be removed if they are now empty
|
||||||
// keep authors without books that have a asin, description or imagePath
|
await LibraryItemScanner.checkAuthorsAndSeriesRemovedFromBooks(libraryScan.libraryId, libraryScan)
|
||||||
if (libraryScan.authorsRemovedFromBooks.length) {
|
|
||||||
const bookAuthorsToRemove = (await Database.authorModel.findAll({
|
|
||||||
where: [
|
|
||||||
{
|
|
||||||
id: libraryScan.authorsRemovedFromBooks,
|
|
||||||
asin: {
|
|
||||||
[sequelize.Op.or]: [null, ""]
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
[sequelize.Op.or]: [null, ""]
|
|
||||||
},
|
|
||||||
imagePath: {
|
|
||||||
[sequelize.Op.or]: [null, ""]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
|
|
||||||
],
|
|
||||||
attributes: ['id'],
|
|
||||||
raw: true
|
|
||||||
})).map(au => au.id)
|
|
||||||
if (bookAuthorsToRemove.length) {
|
|
||||||
await Database.authorModel.destroy({
|
|
||||||
where: {
|
|
||||||
id: bookAuthorsToRemove
|
|
||||||
}
|
|
||||||
})
|
|
||||||
bookAuthorsToRemove.forEach((authorId) => {
|
|
||||||
Database.removeAuthorFromFilterData(libraryScan.libraryId, authorId)
|
|
||||||
// TODO: Clients were expecting full author in payload but its unnecessary
|
|
||||||
SocketAuthority.emitter('author_removed', { id: authorId, libraryId: libraryScan.libraryId })
|
|
||||||
})
|
|
||||||
libraryScan.addLog(LogLevel.INFO, `Removed ${bookAuthorsToRemove.length} authors`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check series that were removed from books and remove them if they no longer have any books
|
|
||||||
if (libraryScan.seriesRemovedFromBooks.length) {
|
|
||||||
const bookSeriesToRemove = (await Database.seriesModel.findAll({
|
|
||||||
where: [
|
|
||||||
{
|
|
||||||
id: libraryScan.seriesRemovedFromBooks
|
|
||||||
},
|
|
||||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
|
|
||||||
],
|
|
||||||
attributes: ['id'],
|
|
||||||
raw: true
|
|
||||||
})).map(se => se.id)
|
|
||||||
if (bookSeriesToRemove.length) {
|
|
||||||
await Database.seriesModel.destroy({
|
|
||||||
where: {
|
|
||||||
id: bookSeriesToRemove
|
|
||||||
}
|
|
||||||
})
|
|
||||||
bookSeriesToRemove.forEach((seriesId) => {
|
|
||||||
Database.removeSeriesFromFilterData(libraryScan.libraryId, seriesId)
|
|
||||||
SocketAuthority.emitter('series_removed', { id: seriesId, libraryId: libraryScan.libraryId })
|
|
||||||
})
|
|
||||||
libraryScan.addLog(LogLevel.INFO, `Removed ${bookSeriesToRemove.length} series`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update missing library items
|
// Update missing library items
|
||||||
if (libraryItemIdsMissing.length) {
|
if (libraryItemIdsMissing.length) {
|
||||||
@ -260,7 +197,7 @@ class LibraryScanner {
|
|||||||
if (libraryItemDataFound.length) {
|
if (libraryItemDataFound.length) {
|
||||||
let newOldLibraryItems = []
|
let newOldLibraryItems = []
|
||||||
for (const libraryItemData of libraryItemDataFound) {
|
for (const libraryItemData of libraryItemDataFound) {
|
||||||
const newLibraryItem = await this.scanNewLibraryItem(libraryItemData, libraryScan)
|
const newLibraryItem = await LibraryItemScanner.scanNewLibraryItem(libraryItemData, libraryScan.library.settings, libraryScan)
|
||||||
if (newLibraryItem) {
|
if (newLibraryItem) {
|
||||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(newLibraryItem)
|
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(newLibraryItem)
|
||||||
newOldLibraryItems.push(oldLibraryItem)
|
newOldLibraryItems.push(oldLibraryItem)
|
||||||
@ -353,38 +290,5 @@ class LibraryScanner {
|
|||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {import('../models/LibraryItem')} existingLibraryItem
|
|
||||||
* @param {LibraryItemScanData} libraryItemData
|
|
||||||
* @param {LibraryScan} libraryScan
|
|
||||||
*/
|
|
||||||
async rescanLibraryItem(existingLibraryItem, libraryItemData, libraryScan) {
|
|
||||||
if (existingLibraryItem.mediaType === 'book') {
|
|
||||||
const libraryItem = await BookScanner.rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, libraryScan)
|
|
||||||
return libraryItem
|
|
||||||
} else {
|
|
||||||
// TODO: Scan updated podcast
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {LibraryItemScanData} libraryItemData
|
|
||||||
* @param {LibraryScan} libraryScan
|
|
||||||
*/
|
|
||||||
async scanNewLibraryItem(libraryItemData, libraryScan) {
|
|
||||||
if (libraryScan.libraryMediaType === 'book') {
|
|
||||||
const newLibraryItem = await BookScanner.scanNewBookLibraryItem(libraryItemData, libraryScan)
|
|
||||||
if (newLibraryItem) {
|
|
||||||
libraryScan.addLog(LogLevel.INFO, `Created new library item "${newLibraryItem.relPath}"`)
|
|
||||||
}
|
|
||||||
return newLibraryItem
|
|
||||||
} else {
|
|
||||||
// TODO: Scan new podcast
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
module.exports = LibraryScanner
|
module.exports = new LibraryScanner()
|
70
server/scanner/ScanLogger.js
Normal file
70
server/scanner/ScanLogger.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
const uuidv4 = require("uuid").v4
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
const { LogLevel } = require('../utils/constants')
|
||||||
|
|
||||||
|
class ScanLogger {
|
||||||
|
constructor() {
|
||||||
|
this.id = null
|
||||||
|
this.type = null
|
||||||
|
this.name = null
|
||||||
|
this.verbose = false
|
||||||
|
|
||||||
|
this.startedAt = null
|
||||||
|
this.finishedAt = null
|
||||||
|
this.elapsed = null
|
||||||
|
|
||||||
|
/** @type {string[]} */
|
||||||
|
this.authorsRemovedFromBooks = []
|
||||||
|
/** @type {string[]} */
|
||||||
|
this.seriesRemovedFromBooks = []
|
||||||
|
|
||||||
|
this.logs = []
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
type: this.type,
|
||||||
|
name: this.name,
|
||||||
|
startedAt: this.startedAt,
|
||||||
|
finishedAt: this.finishedAt,
|
||||||
|
elapsed: this.elapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(type, name) {
|
||||||
|
this.id = uuidv4()
|
||||||
|
this.type = type
|
||||||
|
this.name = name
|
||||||
|
this.startedAt = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
setComplete() {
|
||||||
|
this.finishedAt = Date.now()
|
||||||
|
this.elapsed = this.finishedAt - this.startedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogLevelString(level) {
|
||||||
|
for (const key in LogLevel) {
|
||||||
|
if (LogLevel[key] === level) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'UNKNOWN'
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog(level, ...args) {
|
||||||
|
const logObj = {
|
||||||
|
timestamp: (new Date()).toISOString(),
|
||||||
|
message: args.join(' '),
|
||||||
|
levelName: this.getLogLevelString(level),
|
||||||
|
level
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.verbose) {
|
||||||
|
Logger.debug(`[Scan] "${this.name}":`, ...args)
|
||||||
|
}
|
||||||
|
this.logs.push(logObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = ScanLogger
|
@ -390,11 +390,12 @@ module.exports = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get filter data used in filter menus
|
* Get filter data used in filter menus
|
||||||
* @param {import('../../objects/Library')} oldLibrary
|
* @param {string} mediaType
|
||||||
|
* @param {string} libraryId
|
||||||
* @returns {Promise<object>}
|
* @returns {Promise<object>}
|
||||||
*/
|
*/
|
||||||
async getFilterData(oldLibrary) {
|
async getFilterData(mediaType, libraryId) {
|
||||||
const cachedFilterData = Database.libraryFilterData[oldLibrary.id]
|
const cachedFilterData = Database.libraryFilterData[libraryId]
|
||||||
if (cachedFilterData) {
|
if (cachedFilterData) {
|
||||||
const cacheElapsed = Date.now() - cachedFilterData.loadedAt
|
const cacheElapsed = Date.now() - cachedFilterData.loadedAt
|
||||||
// Cache library filters for 30 mins
|
// Cache library filters for 30 mins
|
||||||
@ -416,13 +417,13 @@ module.exports = {
|
|||||||
numIssues: 0
|
numIssues: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldLibrary.isPodcast) {
|
if (mediaType === 'podcast') {
|
||||||
const podcasts = await Database.podcastModel.findAll({
|
const podcasts = await Database.podcastModel.findAll({
|
||||||
include: {
|
include: {
|
||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
where: {
|
where: {
|
||||||
libraryId: oldLibrary.id
|
libraryId: libraryId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
attributes: ['tags', 'genres']
|
attributes: ['tags', 'genres']
|
||||||
@ -441,7 +442,7 @@ module.exports = {
|
|||||||
model: Database.libraryItemModel,
|
model: Database.libraryItemModel,
|
||||||
attributes: ['isMissing', 'isInvalid'],
|
attributes: ['isMissing', 'isInvalid'],
|
||||||
where: {
|
where: {
|
||||||
libraryId: oldLibrary.id
|
libraryId: libraryId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
attributes: ['tags', 'genres', 'publisher', 'narrators', 'language']
|
attributes: ['tags', 'genres', 'publisher', 'narrators', 'language']
|
||||||
@ -463,7 +464,7 @@ module.exports = {
|
|||||||
|
|
||||||
const series = await Database.seriesModel.findAll({
|
const series = await Database.seriesModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
libraryId: oldLibrary.id
|
libraryId: libraryId
|
||||||
},
|
},
|
||||||
attributes: ['id', 'name']
|
attributes: ['id', 'name']
|
||||||
})
|
})
|
||||||
@ -471,7 +472,7 @@ module.exports = {
|
|||||||
|
|
||||||
const authors = await Database.authorModel.findAll({
|
const authors = await Database.authorModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
libraryId: oldLibrary.id
|
libraryId: libraryId
|
||||||
},
|
},
|
||||||
attributes: ['id', 'name']
|
attributes: ['id', 'name']
|
||||||
})
|
})
|
||||||
@ -486,7 +487,7 @@ module.exports = {
|
|||||||
data.publishers = naturalSort([...data.publishers]).asc()
|
data.publishers = naturalSort([...data.publishers]).asc()
|
||||||
data.languages = naturalSort([...data.languages]).asc()
|
data.languages = naturalSort([...data.languages]).asc()
|
||||||
data.loadedAt = Date.now()
|
data.loadedAt = Date.now()
|
||||||
Database.libraryFilterData[oldLibrary.id] = data
|
Database.libraryFilterData[libraryId] = data
|
||||||
|
|
||||||
Logger.debug(`Loaded filterdata in ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
Logger.debug(`Loaded filterdata in ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
||||||
return data
|
return data
|
||||||
|
Loading…
Reference in New Issue
Block a user