mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-04 06:59:41 +00:00
Fix library consolidation filter and implement podcast support
This commit is contained in:
parent
b3cdd880e1
commit
23034e6672
12 changed files with 153 additions and 14 deletions
|
|
@ -258,6 +258,11 @@ export default {
|
||||||
sublist: false
|
sublist: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
items.push({
|
||||||
|
text: 'Consolidated',
|
||||||
|
value: 'consolidated',
|
||||||
|
sublist: true
|
||||||
|
})
|
||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
podcastItems() {
|
podcastItems() {
|
||||||
|
|
@ -303,7 +308,11 @@ export default {
|
||||||
sublist: false
|
sublist: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
items.push({
|
||||||
|
text: 'Consolidated',
|
||||||
|
value: 'consolidated',
|
||||||
|
sublist: true
|
||||||
|
})
|
||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
|
|
@ -350,6 +359,9 @@ export default {
|
||||||
} else if (parts[0] === 'missing') {
|
} else if (parts[0] === 'missing') {
|
||||||
const item = this.missing.find((m) => m.id == decoded)
|
const item = this.missing.find((m) => m.id == decoded)
|
||||||
if (item) filterValue = item.name
|
if (item) filterValue = item.name
|
||||||
|
} else if (parts[0] === 'consolidated') {
|
||||||
|
const item = this.consolidated.find((c) => c.id == decoded)
|
||||||
|
if (item) filterValue = item.name
|
||||||
} else {
|
} else {
|
||||||
filterValue = decoded
|
filterValue = decoded
|
||||||
}
|
}
|
||||||
|
|
@ -504,6 +516,18 @@ export default {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
consolidated() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'consolidated',
|
||||||
|
name: 'Consolidated'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'not-consolidated',
|
||||||
|
name: 'Not Consolidated'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
const sublistItems = (this[this.sublist] || []).map((item) => {
|
const sublistItems = (this[this.sublist] || []).map((item) => {
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ components:
|
||||||
isInvalid:
|
isInvalid:
|
||||||
description: Whether the library item was scanned and no longer has media files.
|
description: Whether the library item was scanned and no longer has media files.
|
||||||
type: boolean
|
type: boolean
|
||||||
|
isNotConsolidated:
|
||||||
|
description: Whether the library item folder name does not match the "Author - Title" format.
|
||||||
|
type: boolean
|
||||||
mediaType:
|
mediaType:
|
||||||
$ref: './mediaTypes/media.yaml#/components/schemas/mediaType'
|
$ref: './mediaTypes/media.yaml#/components/schemas/mediaType'
|
||||||
libraryItemMinified:
|
libraryItemMinified:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.32.1",
|
"version": "2.32.7",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
|
||||||
|
|
@ -401,6 +401,7 @@ class Database {
|
||||||
|
|
||||||
// Update server settings with version/build
|
// Update server settings with version/build
|
||||||
let updateServerSettings = false
|
let updateServerSettings = false
|
||||||
|
const oldVersion = this.serverSettings.version
|
||||||
if (packageJson.version !== this.serverSettings.version) {
|
if (packageJson.version !== this.serverSettings.version) {
|
||||||
Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`)
|
Logger.info(`[Database] Server upgrade detected from ${this.serverSettings.version} to ${packageJson.version}`)
|
||||||
this.serverSettings.version = packageJson.version
|
this.serverSettings.version = packageJson.version
|
||||||
|
|
@ -413,9 +414,48 @@ class Database {
|
||||||
}
|
}
|
||||||
if (updateServerSettings) {
|
if (updateServerSettings) {
|
||||||
await this.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
|
* Create root user
|
||||||
* @param {string} username
|
* @param {string} username
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ async function handleMoveLibraryItem(libraryItem, targetLibrary, targetFolder, n
|
||||||
libraryItem.relPath = newRelPath
|
libraryItem.relPath = newRelPath
|
||||||
libraryItem.isMissing = false
|
libraryItem.isMissing = false
|
||||||
libraryItem.isInvalid = false
|
libraryItem.isInvalid = false
|
||||||
|
libraryItem.isNotConsolidated = libraryItem.checkIsNotConsolidated()
|
||||||
libraryItem.changed('updatedAt', true)
|
libraryItem.changed('updatedAt', true)
|
||||||
await libraryItem.save({ transaction })
|
await libraryItem.save({ transaction })
|
||||||
|
|
||||||
|
|
|
||||||
45
server/migrations/v2.32.2-add-is-not-consolidated-column.js
Normal file
45
server/migrations/v2.32.2-add-is-not-consolidated-column.js
Normal 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 }
|
||||||
|
|
@ -688,7 +688,11 @@ class LibraryItem extends Model {
|
||||||
title: DataTypes.STRING,
|
title: DataTypes.STRING,
|
||||||
titleIgnorePrefix: DataTypes.STRING,
|
titleIgnorePrefix: DataTypes.STRING,
|
||||||
authorNamesFirstLast: DataTypes.STRING,
|
authorNamesFirstLast: DataTypes.STRING,
|
||||||
authorNamesLastFirst: DataTypes.STRING
|
authorNamesLastFirst: DataTypes.STRING,
|
||||||
|
isNotConsolidated: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|
@ -917,11 +921,17 @@ class LibraryItem extends Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIsNotConsolidated() {
|
checkIsNotConsolidated() {
|
||||||
if (this.isFile || this.mediaType !== 'book' || !this.media) return false
|
if (this.mediaType !== 'book') return false
|
||||||
const author = this.media.authors?.[0]?.name || 'Unknown Author'
|
if (this.isFile) return true
|
||||||
const title = this.media.title || 'Unknown Title'
|
const author = this.authorNamesFirstLast?.split(',')[0]?.trim() || 'Unknown Author'
|
||||||
const folderName = sanitizeFilename(`${author} - ${title}`)
|
const title = this.title || 'Unknown Title'
|
||||||
return Path.basename(this.path) !== folderName
|
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() {
|
toOldJSON() {
|
||||||
|
|
@ -947,7 +957,7 @@ class LibraryItem extends Model {
|
||||||
scanVersion: this.lastScanVersion,
|
scanVersion: this.lastScanVersion,
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
isInvalid: !!this.isInvalid,
|
isInvalid: !!this.isInvalid,
|
||||||
isNotConsolidated: this.checkIsNotConsolidated(),
|
isNotConsolidated: !!this.isNotConsolidated,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toOldJSON(this.id),
|
media: this.media.toOldJSON(this.id),
|
||||||
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
// 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(),
|
updatedAt: this.updatedAt.valueOf(),
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
isInvalid: !!this.isInvalid,
|
isInvalid: !!this.isInvalid,
|
||||||
isNotConsolidated: this.checkIsNotConsolidated(),
|
isNotConsolidated: !!this.isNotConsolidated,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toOldJSONMinified(),
|
media: this.media.toOldJSONMinified(),
|
||||||
numFiles: this.libraryFiles.length,
|
numFiles: this.libraryFiles.length,
|
||||||
|
|
@ -1003,7 +1013,7 @@ class LibraryItem extends Model {
|
||||||
scanVersion: this.lastScanVersion,
|
scanVersion: this.lastScanVersion,
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
isInvalid: !!this.isInvalid,
|
isInvalid: !!this.isInvalid,
|
||||||
isNotConsolidated: this.checkIsNotConsolidated(),
|
isNotConsolidated: !!this.isNotConsolidated,
|
||||||
mediaType: this.mediaType,
|
mediaType: this.mediaType,
|
||||||
media: this.media.toOldJSONExpanded(this.id),
|
media: this.media.toOldJSONExpanded(this.id),
|
||||||
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
||||||
|
|
|
||||||
|
|
@ -382,8 +382,12 @@ class BookScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
existingLibraryItem.media = media
|
existingLibraryItem.media = media
|
||||||
|
|
||||||
let libraryItemUpdated = false
|
let libraryItemUpdated = false
|
||||||
|
const isNotConsolidated = existingLibraryItem.checkIsNotConsolidated()
|
||||||
|
if (existingLibraryItem.isNotConsolidated !== isNotConsolidated) {
|
||||||
|
existingLibraryItem.isNotConsolidated = isNotConsolidated
|
||||||
|
libraryItemUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
// Save Book changes to db
|
// Save Book changes to db
|
||||||
if (hasMediaChanges) {
|
if (hasMediaChanges) {
|
||||||
|
|
@ -560,6 +564,7 @@ class BookScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItemObj.book = bookObject
|
libraryItemObj.book = bookObject
|
||||||
|
libraryItemObj.isNotConsolidated = Database.libraryItemModel.prototype.checkIsNotConsolidated.call(libraryItemObj)
|
||||||
const libraryItem = await Database.libraryItemModel.create(libraryItemObj, {
|
const libraryItem = await Database.libraryItemModel.create(libraryItemObj, {
|
||||||
include: {
|
include: {
|
||||||
model: Database.bookModel,
|
model: Database.bookModel,
|
||||||
|
|
|
||||||
|
|
@ -232,8 +232,12 @@ class PodcastScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
existingLibraryItem.media = media
|
existingLibraryItem.media = media
|
||||||
|
|
||||||
let libraryItemUpdated = false
|
let libraryItemUpdated = false
|
||||||
|
const isNotConsolidated = existingLibraryItem.checkIsNotConsolidated()
|
||||||
|
if (existingLibraryItem.isNotConsolidated !== isNotConsolidated) {
|
||||||
|
existingLibraryItem.isNotConsolidated = isNotConsolidated
|
||||||
|
libraryItemUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
// Save Podcast changes to db
|
// Save Podcast changes to db
|
||||||
if (hasMediaChanges) {
|
if (hasMediaChanges) {
|
||||||
|
|
@ -337,6 +341,7 @@ class PodcastScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItemObj.podcast = podcastObject
|
libraryItemObj.podcast = podcastObject
|
||||||
|
libraryItemObj.isNotConsolidated = Database.libraryItemModel.prototype.checkIsNotConsolidated.call(libraryItemObj)
|
||||||
const libraryItem = await Database.libraryItemModel.create(libraryItemObj, {
|
const libraryItem = await Database.libraryItemModel.create(libraryItemObj, {
|
||||||
include: {
|
include: {
|
||||||
model: Database.podcastModel,
|
model: Database.podcastModel,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ module.exports = {
|
||||||
let filterValue = null
|
let filterValue = null
|
||||||
let filterGroup = null
|
let filterGroup = null
|
||||||
if (filterBy) {
|
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 + '.'))
|
const group = searchGroups.find((_group) => filterBy.startsWith(_group + '.'))
|
||||||
filterGroup = group || filterBy
|
filterGroup = group || filterBy
|
||||||
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
|
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,8 @@ module.exports = {
|
||||||
mediaWhere = Sequelize.where(Sequelize.literal('CAST(publishedYear AS INTEGER)'), {
|
mediaWhere = Sequelize.where(Sequelize.literal('CAST(publishedYear AS INTEGER)'), {
|
||||||
[Sequelize.Op.between]: [startYear, endYear]
|
[Sequelize.Op.between]: [startYear, endYear]
|
||||||
})
|
})
|
||||||
|
} else if (group === 'consolidated') {
|
||||||
|
// This is handled in libraryItemWhere in getFilteredLibraryItems
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mediaWhere, replacements }
|
return { mediaWhere, replacements }
|
||||||
|
|
@ -531,6 +533,8 @@ module.exports = {
|
||||||
libraryItemWhere['createdAt'] = {
|
libraryItemWhere['createdAt'] = {
|
||||||
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
|
[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
|
// When sorting by progress but not filtering by progress, include media progresses
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,8 @@ module.exports = {
|
||||||
libraryItemWhere['createdAt'] = {
|
libraryItemWhere['createdAt'] = {
|
||||||
[Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago
|
[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 = []
|
const podcastIncludes = []
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue