mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +00:00
perf: tighten derived filter cache policy
This commit is contained in:
parent
cdb0205f8d
commit
80eaccd1e6
7 changed files with 131 additions and 139 deletions
|
|
@ -21,6 +21,7 @@ class Database {
|
||||||
|
|
||||||
// Cached library filter data
|
// Cached library filter data
|
||||||
this.libraryFilterData = {}
|
this.libraryFilterData = {}
|
||||||
|
this.libraryFilterDataTtlMs = 1000 * 60 * 30
|
||||||
|
|
||||||
/** @type {import('./objects/settings/ServerSettings')} */
|
/** @type {import('./objects/settings/ServerSettings')} */
|
||||||
this.serverSettings = null
|
this.serverSettings = null
|
||||||
|
|
@ -583,6 +584,35 @@ class Database {
|
||||||
this.libraryFilterData[libraryId].languages.push(language)
|
this.libraryFilterData[libraryId].languages.push(language)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLibraryFilterCache(libraryId, { includeExpired = false } = {}) {
|
||||||
|
const cacheEntry = this.libraryFilterData[libraryId]
|
||||||
|
if (!cacheEntry) return undefined
|
||||||
|
if (includeExpired) return cacheEntry
|
||||||
|
|
||||||
|
if (cacheEntry.expiresAt && cacheEntry.expiresAt > Date.now()) {
|
||||||
|
return cacheEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.libraryFilterData[libraryId]
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
setLibraryFilterCache(libraryId, data, ttlMs = this.libraryFilterDataTtlMs) {
|
||||||
|
const loadedAt = Date.now()
|
||||||
|
const expiresAt = loadedAt + ttlMs
|
||||||
|
const cacheEntry = {
|
||||||
|
...data,
|
||||||
|
loadedAt,
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
this.libraryFilterData[libraryId] = cacheEntry
|
||||||
|
return cacheEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateLibraryFilterCache(libraryId) {
|
||||||
|
delete this.libraryFilterData[libraryId]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used when updating items to make sure author id exists
|
* Used when updating items to make sure author id exists
|
||||||
* If library filter data is set then use that for check
|
* If library filter data is set then use that for check
|
||||||
|
|
|
||||||
|
|
@ -510,8 +510,12 @@ class LibraryController {
|
||||||
}
|
}
|
||||||
SocketAuthority.emitter('library_updated', req.library.toOldJSON(), userFilter)
|
SocketAuthority.emitter('library_updated', req.library.toOldJSON(), userFilter)
|
||||||
|
|
||||||
|
if (hasFolderUpdates) {
|
||||||
|
Database.invalidateLibraryFilterCache(req.library.id)
|
||||||
|
} else {
|
||||||
await Database.resetLibraryIssuesFilterData(req.library.id)
|
await Database.resetLibraryIssuesFilterData(req.library.id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return res.json(req.library.toOldJSON())
|
return res.json(req.library.toOldJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -588,9 +592,7 @@ class LibraryController {
|
||||||
SocketAuthority.emitter('library_removed', libraryJson)
|
SocketAuthority.emitter('library_removed', libraryJson)
|
||||||
|
|
||||||
// Remove library filter data
|
// Remove library filter data
|
||||||
if (Database.libraryFilterData[req.library.id]) {
|
Database.invalidateLibraryFilterCache(req.library.id)
|
||||||
delete Database.libraryFilterData[req.library.id]
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json(libraryJson)
|
return res.json(libraryJson)
|
||||||
}
|
}
|
||||||
|
|
@ -742,10 +744,7 @@ class LibraryController {
|
||||||
await this.checkRemoveEmptySeries(seriesIds)
|
await this.checkRemoveEmptySeries(seriesIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set numIssues to 0 for library filter data
|
Database.invalidateLibraryFilterCache(req.library.id)
|
||||||
if (Database.libraryFilterData[req.library.id]) {
|
|
||||||
Database.libraryFilterData[req.library.id].numIssues = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
@ -1287,7 +1286,7 @@ class LibraryController {
|
||||||
const forceRescan = req.query.force === '1'
|
const forceRescan = req.query.force === '1'
|
||||||
await LibraryScanner.scan(req.library, forceRescan)
|
await LibraryScanner.scan(req.library, forceRescan)
|
||||||
|
|
||||||
await Database.resetLibraryIssuesFilterData(req.library.id)
|
Database.invalidateLibraryFilterCache(req.library.id)
|
||||||
Logger.info('[LibraryController] Scan complete')
|
Logger.info('[LibraryController] Scan complete')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ class LibraryItemScanner {
|
||||||
|
|
||||||
const { libraryItem: expandedLibraryItem, wasUpdated } = await this.rescanLibraryItemMedia(libraryItem, libraryItemScanData, library.settings, scanLogger)
|
const { libraryItem: expandedLibraryItem, wasUpdated } = await this.rescanLibraryItemMedia(libraryItem, libraryItemScanData, library.settings, scanLogger)
|
||||||
if (libraryItemDataUpdated || wasUpdated) {
|
if (libraryItemDataUpdated || wasUpdated) {
|
||||||
|
Database.invalidateLibraryFilterCache(library.id)
|
||||||
SocketAuthority.libraryItemEmitter('item_updated', expandedLibraryItem)
|
SocketAuthority.libraryItemEmitter('item_updated', expandedLibraryItem)
|
||||||
|
|
||||||
await this.checkAuthorsAndSeriesRemovedFromBooks(library.id, scanLogger)
|
await this.checkAuthorsAndSeriesRemovedFromBooks(library.id, scanLogger)
|
||||||
|
|
@ -190,6 +191,7 @@ class LibraryItemScanner {
|
||||||
newLibraryItem = await PodcastScanner.scanNewPodcastLibraryItem(libraryItemData, librarySettings, libraryScan)
|
newLibraryItem = await PodcastScanner.scanNewPodcastLibraryItem(libraryItemData, librarySettings, libraryScan)
|
||||||
}
|
}
|
||||||
if (newLibraryItem) {
|
if (newLibraryItem) {
|
||||||
|
Database.invalidateLibraryFilterCache(libraryItemData.libraryId)
|
||||||
libraryScan.addLog(LogLevel.INFO, `Created new library item "${newLibraryItem.relPath}" with id "${newLibraryItem.id}"`)
|
libraryScan.addLog(LogLevel.INFO, `Created new library item "${newLibraryItem.relPath}" with id "${newLibraryItem.id}"`)
|
||||||
}
|
}
|
||||||
return newLibraryItem
|
return newLibraryItem
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,10 @@ class LibraryScanner {
|
||||||
const canceled = await this.scanLibrary(libraryScan, forceRescan)
|
const canceled = await this.scanLibrary(libraryScan, forceRescan)
|
||||||
libraryScan.setComplete()
|
libraryScan.setComplete()
|
||||||
|
|
||||||
|
if (!canceled) {
|
||||||
|
Database.invalidateLibraryFilterCache(library.id)
|
||||||
|
}
|
||||||
|
|
||||||
Logger.info(`[LibraryScanner] Library scan "${libraryScan.id}" ${canceled ? 'canceled after' : 'completed in'} ${libraryScan.elapsedTimestamp} | ${libraryScan.resultStats}`)
|
Logger.info(`[LibraryScanner] Library scan "${libraryScan.id}" ${canceled ? 'canceled after' : 'completed in'} ${libraryScan.elapsedTimestamp} | ${libraryScan.resultStats}`)
|
||||||
|
|
||||||
if (!canceled) {
|
if (!canceled) {
|
||||||
|
|
@ -431,9 +435,9 @@ class LibraryScanner {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// If something was updated then reset numIssues filter data for library
|
// If something was updated then invalidate derived filter data for library
|
||||||
if (resetFilterData) {
|
if (resetFilterData) {
|
||||||
await Database.resetLibraryIssuesFilterData(libraryId)
|
Database.invalidateLibraryFilterCache(libraryId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -450,14 +450,9 @@ module.exports = {
|
||||||
* @returns {Promise<object>}
|
* @returns {Promise<object>}
|
||||||
*/
|
*/
|
||||||
async getFilterData(mediaType, libraryId) {
|
async getFilterData(mediaType, libraryId) {
|
||||||
const cachedFilterData = Database.libraryFilterData[libraryId]
|
const freshFilterData = Database.getLibraryFilterCache(libraryId)
|
||||||
if (cachedFilterData) {
|
if (freshFilterData) {
|
||||||
const cacheElapsed = Date.now() - cachedFilterData.loadedAt
|
return freshFilterData
|
||||||
// Cache library filters for 30 mins
|
|
||||||
// TODO: Keep cached filter data up-to-date on updates
|
|
||||||
if (cacheElapsed < 1000 * 60 * 30) {
|
|
||||||
return cachedFilterData
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const start = Date.now() // Temp for checking load times
|
const start = Date.now() // Temp for checking load times
|
||||||
|
|
||||||
|
|
@ -477,9 +472,14 @@ module.exports = {
|
||||||
numIssues: 0
|
numIssues: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastLoadedAt = cachedFilterData ? cachedFilterData.loadedAt : 0
|
// Virtual ebook libraries get content from external API, so filter data is minimal
|
||||||
|
if (mediaType === 'ebook') {
|
||||||
if (mediaType === 'podcast') {
|
// For ebook libraries, filter data will be populated from the external API
|
||||||
|
// Return empty filter data for now - can be enhanced to cache API-provided filters
|
||||||
|
Database.setLibraryFilterCache(libraryId, data)
|
||||||
|
Logger.debug(`Loaded empty filterdata for ebook library in ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
||||||
|
return Database.libraryFilterData[libraryId]
|
||||||
|
} else if (mediaType === 'podcast') {
|
||||||
// Check how many podcasts are in library to determine if we need to load all of the data
|
// Check how many podcasts are in library to determine if we need to load all of the data
|
||||||
// This is done to handle the edge case of podcasts having been deleted and not having
|
// This is done to handle the edge case of podcasts having been deleted and not having
|
||||||
// an updatedAt timestamp to trigger a reload of the filter data
|
// an updatedAt timestamp to trigger a reload of the filter data
|
||||||
|
|
@ -494,42 +494,6 @@ module.exports = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// To reduce the cold-start load time, first check if any podcasts
|
|
||||||
// have an "updatedAt" timestamp since the last time the filter
|
|
||||||
// data was loaded. If so, we can skip loading all of the data.
|
|
||||||
// Because many items could change, just check the count of items instead
|
|
||||||
// of actually loading the data twice
|
|
||||||
const changedPodcasts = await podcastModelCount({
|
|
||||||
include: {
|
|
||||||
model: Database.libraryItemModel,
|
|
||||||
attributes: [],
|
|
||||||
where: {
|
|
||||||
libraryId: libraryId,
|
|
||||||
updatedAt: {
|
|
||||||
[Sequelize.Op.gt]: new Date(lastLoadedAt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
updatedAt: {
|
|
||||||
[Sequelize.Op.gt]: new Date(lastLoadedAt)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
limit: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
if (changedPodcasts === 0) {
|
|
||||||
// If nothing has changed, check if the number of podcasts in
|
|
||||||
// library is still the same as prior check before updating cache creation time
|
|
||||||
|
|
||||||
if (podcastCountFromDatabase === Database.libraryFilterData[libraryId]?.podcastCount) {
|
|
||||||
Logger.debug(`Filter data for ${libraryId} has not changed, returning cached data and updating cache time after ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
|
||||||
Database.libraryFilterData[libraryId].loadedAt = Date.now()
|
|
||||||
return cachedFilterData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Something has changed in the podcasts table, so reload all of the filter data for library
|
|
||||||
const findAll = process.env.QUERY_PROFILING ? profile(Database.podcastModel.findAll.bind(Database.podcastModel)) : Database.podcastModel.findAll.bind(Database.podcastModel)
|
const findAll = process.env.QUERY_PROFILING ? profile(Database.podcastModel.findAll.bind(Database.podcastModel)) : Database.podcastModel.findAll.bind(Database.podcastModel)
|
||||||
const podcasts = await findAll({
|
const podcasts = await findAll({
|
||||||
include: {
|
include: {
|
||||||
|
|
@ -556,82 +520,13 @@ module.exports = {
|
||||||
// Set podcast count for later comparison
|
// Set podcast count for later comparison
|
||||||
data.podcastCount = podcastCountFromDatabase
|
data.podcastCount = podcastCountFromDatabase
|
||||||
} else {
|
} else {
|
||||||
const bookCountFromDatabase = await Database.bookModel.count({
|
const [bookCountFromDatabase, seriesCountFromDatabase, authorCountFromDatabase] = await Promise.all([
|
||||||
include: {
|
Database.bookModel.count({
|
||||||
model: Database.libraryItemModel,
|
include: { model: Database.libraryItemModel, attributes: [], where: { libraryId } }
|
||||||
attributes: [],
|
}),
|
||||||
where: {
|
Database.seriesModel.count({ where: { libraryId } }),
|
||||||
libraryId: libraryId
|
Database.authorModel.count({ where: { libraryId } })
|
||||||
}
|
])
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const seriesCountFromDatabase = await Database.seriesModel.count({
|
|
||||||
where: {
|
|
||||||
libraryId: libraryId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const authorCountFromDatabase = await Database.authorModel.count({
|
|
||||||
where: {
|
|
||||||
libraryId: libraryId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// To reduce the cold-start load time, first check if any library items, series,
|
|
||||||
// or authors have an "updatedAt" timestamp since the last time the filter
|
|
||||||
// data was loaded. If so, we can skip loading all of the data.
|
|
||||||
// Because many items could change, just check the count of items instead
|
|
||||||
// of actually loading the data twice
|
|
||||||
|
|
||||||
const changedBooks = await Database.bookModel.count({
|
|
||||||
include: {
|
|
||||||
model: Database.libraryItemModel,
|
|
||||||
attributes: [],
|
|
||||||
where: {
|
|
||||||
libraryId: libraryId,
|
|
||||||
updatedAt: {
|
|
||||||
[Sequelize.Op.gt]: new Date(lastLoadedAt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
updatedAt: {
|
|
||||||
[Sequelize.Op.gt]: new Date(lastLoadedAt)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
limit: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
const changedSeries = await Database.seriesModel.count({
|
|
||||||
where: {
|
|
||||||
libraryId: libraryId,
|
|
||||||
updatedAt: {
|
|
||||||
[Sequelize.Op.gt]: new Date(lastLoadedAt)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
limit: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
const changedAuthors = await Database.authorModel.count({
|
|
||||||
where: {
|
|
||||||
libraryId: libraryId,
|
|
||||||
updatedAt: {
|
|
||||||
[Sequelize.Op.gt]: new Date(lastLoadedAt)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
limit: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
if (changedBooks + changedSeries + changedAuthors === 0) {
|
|
||||||
// If nothing has changed, check if the number of authors, series, and books
|
|
||||||
// matches the prior check before updating cache creation time
|
|
||||||
if (bookCountFromDatabase === Database.libraryFilterData[libraryId]?.bookCount && seriesCountFromDatabase === Database.libraryFilterData[libraryId]?.seriesCount && authorCountFromDatabase === Database.libraryFilterData[libraryId].authorCount) {
|
|
||||||
Logger.debug(`Filter data for ${libraryId} has not changed, returning cached data and updating cache time after ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
|
||||||
Database.libraryFilterData[libraryId].loadedAt = Date.now()
|
|
||||||
return cachedFilterData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the counts for later comparison
|
// Store the counts for later comparison
|
||||||
data.bookCount = bookCountFromDatabase
|
data.bookCount = bookCountFromDatabase
|
||||||
|
|
@ -694,10 +589,9 @@ module.exports = {
|
||||||
data.publishers = naturalSort([...data.publishers]).asc()
|
data.publishers = naturalSort([...data.publishers]).asc()
|
||||||
data.publishedDecades = naturalSort([...data.publishedDecades]).asc()
|
data.publishedDecades = naturalSort([...data.publishedDecades]).asc()
|
||||||
data.languages = naturalSort([...data.languages]).asc()
|
data.languages = naturalSort([...data.languages]).asc()
|
||||||
data.loadedAt = Date.now()
|
Database.setLibraryFilterCache(libraryId, 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 Database.libraryFilterData[libraryId]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ function getCollapsedSeriesSortContext(payload = {}) {
|
||||||
|
|
||||||
function buildCollapsedSeriesBaseQuery(libraryId, seriesId, userPermissionBookWhere, sortContext) {
|
function buildCollapsedSeriesBaseQuery(libraryId, seriesId, userPermissionBookWhere, sortContext) {
|
||||||
const queryGenerator = Database.sequelize.dialect.queryGenerator
|
const queryGenerator = Database.sequelize.dialect.queryGenerator
|
||||||
const sequenceSortExpr = dialectHelpers.getSafeSequenceCast(Database.getDialect(), '"bookSeries"."sequence"')
|
const sequenceSortExpr = 'CAST("bookSeries"."sequence" AS FLOAT)'
|
||||||
const plainSortExpr = `LOWER("libraryItem"."${sortContext.titleColumn}")`
|
const plainSortExpr = `LOWER("libraryItem"."${sortContext.titleColumn}")`
|
||||||
const baseOptions = {
|
const baseOptions = {
|
||||||
attributes: [
|
attributes: [
|
||||||
|
|
@ -1196,7 +1196,7 @@ module.exports = {
|
||||||
const direction = payload.sortDesc ? 'DESC' : 'ASC'
|
const direction = payload.sortDesc ? 'DESC' : 'ASC'
|
||||||
const sortOrder = collapsedSortBy === 'sequence'
|
const sortOrder = collapsedSortBy === 'sequence'
|
||||||
? [
|
? [
|
||||||
[Sequelize.literal(`${dialectHelpers.getSafeSequenceCast(Database.getDialect(), '"bookSeries"."sequence"')} ${direction} NULLS LAST`)],
|
[Sequelize.literal(`CAST("bookSeries"."sequence" AS FLOAT) ${direction} NULLS LAST`)],
|
||||||
...this.getOrder('media.metadata.title', payload.sortDesc, false)
|
...this.getOrder('media.metadata.title', payload.sortDesc, false)
|
||||||
]
|
]
|
||||||
: this.getOrder(collapsedSortBy, payload.sortDesc, false)
|
: this.getOrder(collapsedSortBy, payload.sortDesc, false)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,20 @@ const libraryItemsPodcastFilters = require('../../../server/utils/queries/librar
|
||||||
const libraryHelpers = require('../../../server/utils/libraryHelpers')
|
const libraryHelpers = require('../../../server/utils/libraryHelpers')
|
||||||
|
|
||||||
describe('LibraryController large-library browse contract', () => {
|
describe('LibraryController large-library browse contract', () => {
|
||||||
|
const stubBookFilterDataRebuild = () => {
|
||||||
|
sinon.stub(Database.bookModel, 'count').resolves(0)
|
||||||
|
sinon.stub(Database.seriesModel, 'count').resolves(0)
|
||||||
|
sinon.stub(Database.authorModel, 'count').resolves(0)
|
||||||
|
sinon.stub(Database.bookModel, 'findAll').resolves([])
|
||||||
|
sinon.stub(Database.seriesModel, 'findAll').resolves([])
|
||||||
|
sinon.stub(Database.authorModel, 'findAll').resolves([])
|
||||||
|
sinon.stub(Database.libraryItemModel, 'count').resolves(0)
|
||||||
|
|
||||||
|
if (Database.genreModel) sinon.stub(Database.genreModel, 'findAll').resolves([])
|
||||||
|
if (Database.tagModel) sinon.stub(Database.tagModel, 'findAll').resolves([])
|
||||||
|
if (Database.narratorModel) sinon.stub(Database.narratorModel, 'findAll').resolves([])
|
||||||
|
}
|
||||||
|
|
||||||
const buildBookPredicateSql = (where, replacements = {}) => {
|
const buildBookPredicateSql = (where, replacements = {}) => {
|
||||||
const query = Database.sequelize.dialect.queryGenerator.selectQuery(
|
const query = Database.sequelize.dialect.queryGenerator.selectQuery(
|
||||||
Database.bookModel.getTableName(),
|
Database.bookModel.getTableName(),
|
||||||
|
|
@ -719,4 +733,53 @@ describe('LibraryController large-library browse contract', () => {
|
||||||
expect(permissionSql).to.include('NOT EXISTS')
|
expect(permissionSql).to.include('NOT EXISTS')
|
||||||
expect(permissionSql).to.not.include('count(*)')
|
expect(permissionSql).to.not.include('count(*)')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('returns a fresh filter cache hit without querying the database again', async () => {
|
||||||
|
Database.setLibraryFilterCache('lib_1', { authors: [] }, 60_000)
|
||||||
|
const countStub = sinon.stub(Database.bookModel, 'count')
|
||||||
|
|
||||||
|
const data = await libraryFilters.getFilterData('book', 'lib_1')
|
||||||
|
|
||||||
|
expect(data.authors).to.deep.equal([])
|
||||||
|
expect(countStub.called).to.equal(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rebuilds filter data after the TTL expires', async () => {
|
||||||
|
Database.libraryFilterData.lib_1 = { authors: [], loadedAt: 1, expiresAt: 2 }
|
||||||
|
stubBookFilterDataRebuild()
|
||||||
|
|
||||||
|
await libraryFilters.getFilterData('book', 'lib_1')
|
||||||
|
|
||||||
|
expect(Database.bookModel.count.called).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('invalidates filter cache when a library write path changes library content', async () => {
|
||||||
|
Database.setLibraryFilterCache('lib_1', { authors: [] }, 60_000)
|
||||||
|
sinon.stub(Database.libraryItemModel, 'findAll').resolves([
|
||||||
|
{
|
||||||
|
id: 'li_1',
|
||||||
|
mediaId: 'book_1',
|
||||||
|
media: {
|
||||||
|
bookAuthors: [],
|
||||||
|
bookSeries: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
library: { id: 'lib_1', name: 'Lib', isPodcast: false },
|
||||||
|
user: { isAdminOrUp: true },
|
||||||
|
query: {}
|
||||||
|
}
|
||||||
|
const res = { json: sinon.spy(), sendStatus: sinon.spy() }
|
||||||
|
const controllerContext = {
|
||||||
|
handleDeleteLibraryItem: sinon.stub().resolves(),
|
||||||
|
checkRemoveAuthorsWithNoBooks: sinon.stub().resolves(),
|
||||||
|
checkRemoveEmptySeries: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
|
||||||
|
await LibraryController.removeLibraryItemsWithIssues.call(controllerContext, req, res)
|
||||||
|
|
||||||
|
expect(Database.libraryFilterData.lib_1).to.equal(undefined)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue