Fix library consolidation filter and implement podcast support

This commit is contained in:
Tiberiu Ichim 2026-02-15 15:46:46 +02:00
parent b3cdd880e1
commit 23034e6672
12 changed files with 153 additions and 14 deletions

View file

@ -401,6 +401,7 @@ class Database {
// Update server settings with version/build
let updateServerSettings = false
const oldVersion = this.serverSettings.version
if (packageJson.version !== this.serverSettings.version) {
Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`)
this.serverSettings.version = packageJson.version
@ -413,9 +414,48 @@ class Database {
}
if (updateServerSettings) {
await this.updateServerSettings()
if (this.compareVersions(oldVersion, '2.32.7') < 0) {
await this.populateIsNotConsolidated()
}
}
}
async populateIsNotConsolidated() {
Logger.info(`[Database] Populating isNotConsolidated flag for all items...`)
const items = await this.models.libraryItem.findAll({
include: [
{
model: this.models.book,
include: {
model: this.models.author,
through: {
attributes: ['createdAt']
}
}
},
{
model: this.models.podcast
}
]
})
let count = 0
let updatedCount = 0
for (const item of items) {
count++
// LibraryItem.js hook afterFind sets item.media
const isNotConsolidated = item.checkIsNotConsolidated()
if (item.isNotConsolidated !== isNotConsolidated) {
Logger.debug(`[Database] Updating isNotConsolidated for ${item.relPath} to ${isNotConsolidated} (was ${item.isNotConsolidated})`)
item.isNotConsolidated = isNotConsolidated
await item.save()
updatedCount++
}
}
Logger.info(`[Database] Finished populating isNotConsolidated flag. Checked ${count} items, updated ${updatedCount}`)
}
/**
* Create root user
* @param {string} username

View file

@ -82,6 +82,7 @@ async function handleMoveLibraryItem(libraryItem, targetLibrary, targetFolder, n
libraryItem.relPath = newRelPath
libraryItem.isMissing = false
libraryItem.isInvalid = false
libraryItem.isNotConsolidated = libraryItem.checkIsNotConsolidated()
libraryItem.changed('updatedAt', true)
await libraryItem.save({ transaction })

View file

@ -0,0 +1,45 @@
/**
* @typedef MigrationContext
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @property {import('../Logger')} logger - a Logger object.
*
* @typedef MigrationOptions
* @property {MigrationContext} context - an object containing the migration context.
*/
const migrationVersion = '2.32.2'
const migrationName = `${migrationVersion}-add-is-not-consolidated-column`
const loggerPrefix = `[${migrationVersion} migration]`
/**
* Adds isNotConsolidated column to libraryItems table.
*
* @param {MigrationOptions} options
*/
async function up({ context: { queryInterface, logger } }) {
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
const tableDescription = await queryInterface.describeTable('libraryItems')
if (!tableDescription['isNotConsolidated']) {
await queryInterface.addColumn('libraryItems', 'isNotConsolidated', {
type: queryInterface.sequelize.Sequelize.BOOLEAN,
defaultValue: false
})
logger.info(`${loggerPrefix} added column "isNotConsolidated" to table "libraryItems"`)
} else {
logger.info(`${loggerPrefix} column "isNotConsolidated" already exists in table "libraryItems"`)
}
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
}
async function down({ context: { queryInterface, logger } }) {
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
await queryInterface.removeColumn('libraryItems', 'isNotConsolidated')
logger.info(`${loggerPrefix} removed column "isNotConsolidated" from table "libraryItems"`)
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
}
module.exports = { up, down }

View file

@ -688,7 +688,11 @@ class LibraryItem extends Model {
title: DataTypes.STRING,
titleIgnorePrefix: DataTypes.STRING,
authorNamesFirstLast: DataTypes.STRING,
authorNamesLastFirst: DataTypes.STRING
authorNamesLastFirst: DataTypes.STRING,
isNotConsolidated: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
},
{
sequelize,
@ -917,11 +921,17 @@ class LibraryItem extends Model {
}
checkIsNotConsolidated() {
if (this.isFile || this.mediaType !== 'book' || !this.media) return false
const author = this.media.authors?.[0]?.name || 'Unknown Author'
const title = this.media.title || 'Unknown Title'
const folderName = sanitizeFilename(`${author} - ${title}`)
return Path.basename(this.path) !== folderName
if (this.mediaType !== 'book') return false
if (this.isFile) return true
const author = this.authorNamesFirstLast?.split(',')[0]?.trim() || 'Unknown Author'
const title = this.title || 'Unknown Title'
const folderName = this.checkIsNotConsolidated_FolderName(author, title)
const currentFolderName = Path.basename(this.path.replace(/[\/\\]$/, ''))
return currentFolderName !== folderName
}
checkIsNotConsolidated_FolderName(author, title) {
return sanitizeFilename(`${author} - ${title}`)
}
toOldJSON() {
@ -947,7 +957,7 @@ class LibraryItem extends Model {
scanVersion: this.lastScanVersion,
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
isNotConsolidated: this.checkIsNotConsolidated(),
isNotConsolidated: !!this.isNotConsolidated,
mediaType: this.mediaType,
media: this.media.toOldJSON(this.id),
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
@ -976,7 +986,7 @@ class LibraryItem extends Model {
updatedAt: this.updatedAt.valueOf(),
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
isNotConsolidated: this.checkIsNotConsolidated(),
isNotConsolidated: !!this.isNotConsolidated,
mediaType: this.mediaType,
media: this.media.toOldJSONMinified(),
numFiles: this.libraryFiles.length,
@ -1003,7 +1013,7 @@ class LibraryItem extends Model {
scanVersion: this.lastScanVersion,
isMissing: !!this.isMissing,
isInvalid: !!this.isInvalid,
isNotConsolidated: this.checkIsNotConsolidated(),
isNotConsolidated: !!this.isNotConsolidated,
mediaType: this.mediaType,
media: this.media.toOldJSONExpanded(this.id),
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database

View file

@ -382,8 +382,12 @@ class BookScanner {
}
existingLibraryItem.media = media
let libraryItemUpdated = false
const isNotConsolidated = existingLibraryItem.checkIsNotConsolidated()
if (existingLibraryItem.isNotConsolidated !== isNotConsolidated) {
existingLibraryItem.isNotConsolidated = isNotConsolidated
libraryItemUpdated = true
}
// Save Book changes to db
if (hasMediaChanges) {
@ -560,6 +564,7 @@ class BookScanner {
}
libraryItemObj.book = bookObject
libraryItemObj.isNotConsolidated = Database.libraryItemModel.prototype.checkIsNotConsolidated.call(libraryItemObj)
const libraryItem = await Database.libraryItemModel.create(libraryItemObj, {
include: {
model: Database.bookModel,

View file

@ -232,8 +232,12 @@ class PodcastScanner {
}
existingLibraryItem.media = media
let libraryItemUpdated = false
const isNotConsolidated = existingLibraryItem.checkIsNotConsolidated()
if (existingLibraryItem.isNotConsolidated !== isNotConsolidated) {
existingLibraryItem.isNotConsolidated = isNotConsolidated
libraryItemUpdated = true
}
// Save Podcast changes to db
if (hasMediaChanges) {
@ -337,6 +341,7 @@ class PodcastScanner {
}
libraryItemObj.podcast = podcastObject
libraryItemObj.isNotConsolidated = Database.libraryItemModel.prototype.checkIsNotConsolidated.call(libraryItemObj)
const libraryItem = await Database.libraryItemModel.create(libraryItemObj, {
include: {
model: Database.podcastModel,

View file

@ -27,7 +27,7 @@ module.exports = {
let filterValue = null
let filterGroup = null
if (filterBy) {
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'publishedDecades', 'missing', 'languages', 'tracks', 'ebooks']
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'publishedDecades', 'missing', 'languages', 'tracks', 'ebooks', 'consolidated']
const group = searchGroups.find((_group) => filterBy.startsWith(_group + '.'))
filterGroup = group || filterBy
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null

View file

@ -239,6 +239,8 @@ module.exports = {
mediaWhere = Sequelize.where(Sequelize.literal('CAST(publishedYear AS INTEGER)'), {
[Sequelize.Op.between]: [startYear, endYear]
})
} else if (group === 'consolidated') {
// This is handled in libraryItemWhere in getFilteredLibraryItems
}
return { mediaWhere, replacements }
@ -531,6 +533,8 @@ module.exports = {
libraryItemWhere['createdAt'] = {
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
}
} else if (filterGroup === 'consolidated') {
libraryItemWhere['isNotConsolidated'] = filterValue === 'not-consolidated'
}
// When sorting by progress but not filtering by progress, include media progresses

View file

@ -172,6 +172,8 @@ module.exports = {
libraryItemWhere['createdAt'] = {
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
}
} else if (filterGroup === 'consolidated') {
libraryItemWhere['isNotConsolidated'] = filterValue === 'not-consolidated'
}
const podcastIncludes = []