diff --git a/server/models/BookAuthor.js b/server/models/BookAuthor.js index 7ee596d3..c425d2fd 100644 --- a/server/models/BookAuthor.js +++ b/server/models/BookAuthor.js @@ -21,7 +21,8 @@ module.exports = (sequelize) => { }, { sequelize, modelName: 'bookAuthor', - timestamps: false + timestamps: true, + updatedAt: false }) // Super Many-to-Many diff --git a/server/models/BookSeries.js b/server/models/BookSeries.js index 5406d2c1..ba6581f2 100644 --- a/server/models/BookSeries.js +++ b/server/models/BookSeries.js @@ -22,7 +22,8 @@ module.exports = (sequelize) => { }, { sequelize, modelName: 'bookSeries', - timestamps: false + timestamps: true, + updatedAt: false }) // Super Many-to-Many diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index bbe4877f..a0a066a7 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -1,4 +1,4 @@ -const { DataTypes, Model } = require('sequelize') +const { DataTypes, Model, literal } = require('sequelize') const Logger = require('../Logger') const oldLibraryItem = require('../objects/LibraryItem') const libraryFilters = require('../utils/queries/libraryFilters') @@ -70,13 +70,13 @@ module.exports = (sequelize) => { { model: sequelize.models.author, through: { - attributes: [] + attributes: ['createdAt'] } }, { model: sequelize.models.series, through: { - attributes: ['sequence'] + attributes: ['sequence', 'createdAt'] } } ] @@ -85,6 +85,12 @@ module.exports = (sequelize) => { model: sequelize.models.podcast } ], + order: [ + ['createdAt', 'ASC'], + // Ensure author & series stay in the same order + [sequelize.models.book, sequelize.models.author, sequelize.models.bookAuthor, 'createdAt', 'ASC'], + [sequelize.models.book, sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'], + ], offset, limit }) diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index e9c8ebed..931682d8 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -73,7 +73,6 @@ module.exports = { } else if (filterBy === 'feed-open') { const libraryItemIdsWithFeed = await Database.models.feed.findAllLibraryItemIds() filtered = filtered.filter(li => libraryItemIdsWithFeed.includes(li.id)) - // filtered = filtered.filter(li => feedsArray.some(feed => feed.entityId === li.id)) } else if (filterBy === 'abridged') { filtered = filtered.filter(li => !!li.media.metadata?.abridged) } else if (filterBy === 'ebook') { diff --git a/server/utils/migrations/dbMigration.js b/server/utils/migrations/dbMigration.js index f7db216e..28c9f2b0 100644 --- a/server/utils/migrations/dbMigration.js +++ b/server/utils/migrations/dbMigration.js @@ -1543,6 +1543,76 @@ async function migrationPatch2Authors(ctx, offset = 0) { return migrationPatch2Authors(ctx, offset + authors.length) } +/** + * Migration from 2.3.3 to 2.3.4 + * Populating the createdAt column on bookAuthor + * @param {/src/Database} ctx + * @param {number} offset + */ +async function migrationPatch2BookAuthors(ctx, offset = 0) { + const bookAuthors = await ctx.models.bookAuthor.findAll({ + include: { + model: ctx.models.author + }, + limit: 500, + offset + }) + if (!bookAuthors.length) return + + const bulkUpdateItems = [] + for (const bookAuthor of bookAuthors) { + if (bookAuthor.author?.createdAt) { + const dateString = bookAuthor.author.createdAt.toISOString().replace('T', ' ').replace('Z', '') + bulkUpdateItems.push(`("${bookAuthor.id}","${dateString}")`) + } + } + + if (bulkUpdateItems.length) { + Logger.info(`[dbMigration] Migration patch 2.3.3+ - patching ${bulkUpdateItems.length} bookAuthors`) + await ctx.sequelize.query(`INSERT INTO bookAuthors ('id','createdAt') VALUES ${bulkUpdateItems.join(',')} ON CONFLICT(id) DO UPDATE SET 'createdAt' = EXCLUDED.createdAt;`) + } + + if (bookAuthors.length < 500) { + return + } + return migrationPatch2BookAuthors(ctx, offset + bookAuthors.length) +} + +/** + * Migration from 2.3.3 to 2.3.4 + * Populating the createdAt column on bookSeries + * @param {/src/Database} ctx + * @param {number} offset + */ +async function migrationPatch2BookSeries(ctx, offset = 0) { + const allBookSeries = await ctx.models.bookSeries.findAll({ + include: { + model: ctx.models.series + }, + limit: 500, + offset + }) + if (!allBookSeries.length) return + + const bulkUpdateItems = [] + for (const bookSeries of allBookSeries) { + if (bookSeries.series?.createdAt) { + const dateString = bookSeries.series.createdAt.toISOString().replace('T', ' ').replace('Z', '') + bulkUpdateItems.push(`("${bookSeries.id}","${dateString}")`) + } + } + + if (bulkUpdateItems.length) { + Logger.info(`[dbMigration] Migration patch 2.3.3+ - patching ${bulkUpdateItems.length} bookSeries`) + await ctx.sequelize.query(`INSERT INTO bookSeries ('id','createdAt') VALUES ${bulkUpdateItems.join(',')} ON CONFLICT(id) DO UPDATE SET 'createdAt' = EXCLUDED.createdAt;`) + } + + if (allBookSeries.length < 500) { + return + } + return migrationPatch2BookSeries(ctx, offset + allBookSeries.length) +} + /** * Migration from 2.3.3 to 2.3.4 * Adding coverPath column to Feed model @@ -1552,8 +1622,9 @@ module.exports.migrationPatch2 = async (ctx) => { const queryInterface = ctx.sequelize.getQueryInterface() const feedTableDescription = await queryInterface.describeTable('feeds') const authorsTableDescription = await queryInterface.describeTable('authors') + const bookAuthorsTableDescription = await queryInterface.describeTable('bookAuthors') - if (feedTableDescription?.coverPath && authorsTableDescription?.lastFirst) { + if (feedTableDescription?.coverPath && authorsTableDescription?.lastFirst && bookAuthorsTableDescription?.createdAt) { Logger.info(`[dbMigration] Migration patch 2.3.3+ - columns already on model`) return false } @@ -1562,25 +1633,35 @@ module.exports.migrationPatch2 = async (ctx) => { try { await queryInterface.sequelize.transaction(t => { const queries = [ - queryInterface.addColumn('authors', 'lastFirst', { - type: DataTypes.STRING + queryInterface.addColumn('bookAuthors', 'createdAt', { + type: DataTypes.DATE }, { transaction: t }), - queryInterface.addColumn('libraryItems', 'size', { - type: DataTypes.BIGINT - }, { transaction: t }), - queryInterface.addColumn('books', 'duration', { - type: DataTypes.FLOAT - }, { transaction: t }), - queryInterface.addColumn('books', 'titleIgnorePrefix', { - type: DataTypes.STRING - }, { transaction: t }), - queryInterface.addColumn('podcasts', 'titleIgnorePrefix', { - type: DataTypes.STRING - }, { transaction: t }), - queryInterface.addColumn('series', 'nameIgnorePrefix', { - type: DataTypes.STRING + queryInterface.addColumn('bookSeries', 'createdAt', { + type: DataTypes.DATE }, { transaction: t }), ] + if (!authorsTableDescription?.lastFirst) { + queries.push(...[ + queryInterface.addColumn('authors', 'lastFirst', { + type: DataTypes.STRING + }, { transaction: t }), + queryInterface.addColumn('libraryItems', 'size', { + type: DataTypes.BIGINT + }, { transaction: t }), + queryInterface.addColumn('books', 'duration', { + type: DataTypes.FLOAT + }, { transaction: t }), + queryInterface.addColumn('books', 'titleIgnorePrefix', { + type: DataTypes.STRING + }, { transaction: t }), + queryInterface.addColumn('podcasts', 'titleIgnorePrefix', { + type: DataTypes.STRING + }, { transaction: t }), + queryInterface.addColumn('series', 'nameIgnorePrefix', { + type: DataTypes.STRING + }, { transaction: t }), + ]) + } if (!feedTableDescription?.coverPath) { queries.push(queryInterface.addColumn('feeds', 'coverPath', { type: DataTypes.STRING @@ -1589,24 +1670,32 @@ module.exports.migrationPatch2 = async (ctx) => { return Promise.all(queries) }) - if (global.ServerSettings.sortingPrefixes?.length) { - prefixesToIgnore = global.ServerSettings.sortingPrefixes + if (!authorsTableDescription?.lastFirst) { + if (global.ServerSettings.sortingPrefixes?.length) { + prefixesToIgnore = global.ServerSettings.sortingPrefixes + } + + // Patch library items size column + await migrationPatch2LibraryItems(ctx, 0) + + // Patch books duration & titleIgnorePrefix column + await migrationPatch2Books(ctx, 0) + + // Patch podcasts titleIgnorePrefix column + await migrationPatch2Podcasts(ctx, 0) + + // Patch authors lastFirst column + await migrationPatch2Authors(ctx, 0) + + // Patch series nameIgnorePrefix column + await migrationPatch2Series(ctx, 0) } - // Patch library items size column - await migrationPatch2LibraryItems(ctx, 0) + // Patch bookAuthors createdAt column + await migrationPatch2BookAuthors(ctx, 0) - // Patch books duration & titleIgnorePrefix column - await migrationPatch2Books(ctx, 0) - - // Patch podcasts titleIgnorePrefix column - await migrationPatch2Podcasts(ctx, 0) - - // Patch authors lastFirst column - await migrationPatch2Authors(ctx, 0) - - // Patch series nameIgnorePrefix column - await migrationPatch2Series(ctx, 0) + // Patch bookSeries createdAt column + await migrationPatch2BookSeries(ctx, 0) Logger.info(`[dbMigration] Migration patch 2.3.3+ finished`) return true diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 2fd58687..2362eea8 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -1,154 +1,21 @@ -const { Op, literal, col, fn, where } = require('sequelize') -const Database = require('../../Database') -const libraryItemsSeriesFilters = require('./libraryItemsSeriesFilters') -const libraryItemsProgressFilters = require('./libraryItemsProgressFilters') -const Logger = require('../../Logger') +const libraryItemsBookFilters = require('./libraryItemsBookFilters') module.exports = { decode(text) { return Buffer.from(decodeURIComponent(text), 'base64').toString() }, - getMediaGroupQuery(group, value) { - let mediaWhere = {} - - if (['genres', 'tags', 'narrators'].includes(group)) { - mediaWhere[group] = { - [Op.substring]: `"${value}"` - } - } else if (group === 'publishers') { - mediaWhere['publisher'] = { - [Op.substring]: `"${value}"` - } - } else if (group === 'languages') { - mediaWhere['language'] = { - [Op.substring]: `"${value}"` - } - } else if (group === 'tracks') { - if (value === 'multi') { - mediaWhere = where(fn('json_array_length', col('audioFiles')), { - [Op.gt]: 1 - }) - } else { - mediaWhere = where(fn('json_array_length', col('audioFiles')), 1) - } - } else if (group === 'ebooks') { - if (value === 'ebook') { - mediaWhere['ebookFile'] = { - [Op.not]: null - } - } - } - - return mediaWhere - }, - - getOrder(sortBy, sortDesc) { - const dir = sortDesc ? 'DESC' : 'ASC' - if (sortBy === 'addedAt') { - return [['createdAt', dir]] - } else if (sortBy === 'size') { - return [['size', dir]] - } else if (sortBy === 'birthtimeMs') { - return [['birthtime', dir]] - } else if (sortBy === 'mtimeMs') { - return [['mtime', dir]] - } else if (sortBy === 'media.duration') { - return [[literal('book.duration'), dir]] - } else if (sortBy === 'media.metadata.publishedYear') { - return [[literal('book.publishedYear'), dir]] - } else if (sortBy === 'media.metadata.authorNameLF') { - return [[literal('book.authors.lastFirst'), dir]] - } else if (sortBy === 'media.metadata.authorName') { - return [[literal('book.authors.name'), dir]] - } else if (sortBy === 'media.metadata.title') { - if (global.ServerSettings.sortingIgnorePrefix) { - return [[literal('book.titleIgnorePrefix'), dir]] - } else { - return [[literal('book.title'), dir]] - } - } - return [] - }, - async getFilteredLibraryItems(libraryId, filterBy, sortBy, sortDesc, limit, offset, userId) { - const libraryItemModel = Database.models.libraryItem - - let mediaWhereQuery = null - let mediaAttributes = null - let itemWhereQuery = { - libraryId + let filterValue = null + let filterGroup = null + if (filterBy) { + const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks'] + const group = searchGroups.find(_group => filterBy.startsWith(_group + '.')) + filterGroup = group || filterBy + filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null } - const itemIncludes = [] - - let authorInclude = { - model: Database.models.author, - through: { - attributes: [] - } - } - let seriesInclude = { - model: Database.models.series, - through: { - attributes: ['sequence'] - } - } - - const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks'] - const group = searchGroups.find(_group => filterBy.startsWith(_group + '.')) - if (group) { - // e.g. genre id - const value = this.decode(filterBy.replace(`${group}.`, '')) - - if (group === 'series' && value === 'no-series') { - return libraryItemsSeriesFilters.getLibraryItemsWithNoSeries(libraryId, sortBy, sortDesc, limit, offset) - } else if (group === 'progress') { - return libraryItemsProgressFilters.getLibraryItemsWithProgressFilter(value, libraryId, userId, sortBy, sortDesc, limit, offset) - } - - if (group === 'authors') { - authorInclude.where = { - id: value - } - authorInclude.required = true - } else if (group === 'series') { - seriesInclude.where = { - id: value - } - seriesInclude.required = true - } else { - mediaWhereQuery = this.getMediaGroupQuery(group, value) - } - } else if (filterBy === 'abridged') { - mediaWhereQuery = { - abridged: true - } - } - - const { rows: libraryItems, count } = await libraryItemModel.findAndCountAll({ - where: itemWhereQuery, - attributes: { - include: [ - [fn('group_concat', col('book.author.name'), ', '), 'author_name'] - ] - }, - distinct: true, - subQuery: false, - include: [ - { - model: Database.models.book, - attributes: mediaAttributes, - where: mediaWhereQuery, - required: true, - include: [authorInclude, seriesInclude, ...itemIncludes] - } - ], - order: this.getOrder(sortBy, sortDesc), - limit, - offset - }) - Logger.debug('Found', libraryItems.length, 'library items', 'total=', count) - return { libraryItems, count } + // TODO: Handle podcast filters + return libraryItemsBookFilters.getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset) } } \ No newline at end of file diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js new file mode 100644 index 00000000..98e7c4bc --- /dev/null +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -0,0 +1,303 @@ +const Sequelize = require('sequelize') +const Database = require('../../Database') +const Logger = require('../../Logger') + +module.exports = { + /** + * Get where options for Book model + * @param {string} group + * @param {[string]} value + * @returns {Sequelize.WhereOptions} + */ + getMediaGroupQuery(group, value) { + let mediaWhere = {} + + if (group === 'progress') { + if (value === 'not-finished') { + mediaWhere['$mediaProgresses.isFinished$'] = { + [Sequelize.Op.or]: [null, false] + } + } else if (value === 'not-started') { + mediaWhere[Sequelize.Op.and] = [ + { + '$mediaProgresses.currentTime$': { + [Sequelize.Op.or]: [null, 0] + } + }, + { + '$mediaProgresses.isFinished$': { + [Sequelize.Op.or]: [null, false] + } + } + ] + } else if (value === 'finished') { + mediaWhere['$mediaProgresses.isFinished$'] = true + } else if (value === 'in-progress') { + mediaWhere[Sequelize.Op.and] = [ + { + [Sequelize.Op.or]: [ + { + '$mediaProgresses.currentTime$': { + [Sequelize.Op.gt]: 0 + } + }, + { + '$mediaProgresses.ebookProgress$': { + [Sequelize.Op.gt]: 0 + } + } + ] + }, + { + '$mediaProgresses.isFinished$': false + } + ] + } + } else if (group === 'series' && value === 'no-series') { + mediaWhere['$series.id$'] = null + } else if (group === 'abridged') { + mediaWhere['abridged'] = true + } else if (['genres', 'tags', 'narrators'].includes(group)) { + mediaWhere[group] = Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(${group}) WHERE json_valid(${group}) AND json_each.value = "${value}")`), { + [Sequelize.Op.gte]: 1 + }) + } else if (group === 'publishers') { + mediaWhere['publisher'] = value + } else if (group === 'languages') { + mediaWhere['language'] = value + } else if (group === 'tracks') { + if (value === 'multi') { + mediaWhere = Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), { + [Sequelize.Op.gt]: 1 + }) + } else { + mediaWhere = Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), 1) + } + } else if (group === 'ebooks') { + if (value === 'ebook') { + mediaWhere['ebookFile'] = { + [Sequelize.Op.not]: null + } + } + } else if (group === 'missing') { + if (['asin', 'isbn', 'subtitle', 'publishedYear', 'description', 'publisher', 'language', 'cover'].includes(value)) { + let key = value + if (value === 'cover') key = 'coverPath' + mediaWhere[key] = { + [Sequelize.Op.or]: [null, ''] + } + } else if (['genres', 'tags', 'narrator'].includes(value)) { + mediaWhere[value] = { + [Sequelize.Op.or]: [null, Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col(value)), 0)] + } + } else if (value === 'authors') { + mediaWhere['$authors.id$'] = null + } else if (value === 'series') { + mediaWhere['$series.id$'] = null + } + } + + return mediaWhere + }, + + /** + * Get sequelize order + * @param {string} sortBy + * @param {boolean} sortDesc + * @returns {Sequelize.order} + */ + getOrder(sortBy, sortDesc) { + const dir = sortDesc ? 'DESC' : 'ASC' + if (sortBy === 'addedAt') { + return [[Sequelize.literal('libraryItem.createdAt'), dir]] + } else if (sortBy === 'size') { + return [[Sequelize.literal('libraryItem.size'), dir]] + } else if (sortBy === 'birthtimeMs') { + return [[Sequelize.literal('libraryItem.birthtime'), dir]] + } else if (sortBy === 'mtimeMs') { + return [[Sequelize.literal('libraryItem.mtime'), dir]] + } else if (sortBy === 'media.duration') { + return [['duration', dir]] + } else if (sortBy === 'media.metadata.publishedYear') { + return [['publishedYear', dir]] + } else if (sortBy === 'media.metadata.authorNameLF') { + return [['author_name', dir]] + } else if (sortBy === 'media.metadata.authorName') { + return [['author_name', dir]] + } else if (sortBy === 'media.metadata.title') { + if (global.ServerSettings.sortingIgnorePrefix) { + return [['titleIgnorePrefix', dir]] + } else { + return [['title', dir]] + } + } + return [] + }, + + /** + * Get library items for book media type using filter and sort + * @param {string} libraryId + * @param {[string]} filterGroup + * @param {[string]} filterValue + * @param {string} sortBy + * @param {string} sortDesc + * @param {number} limit + * @param {number} offset + * @returns {object} { libraryItems:LibraryItem[], count:number } + */ + async getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset) { + // For sorting by author name an additional attribute must be added + // with author names concatenated + let bookAttributes = null + if (sortBy === 'media.metadata.authorNameLF') { + bookAttributes = { + include: [ + [Sequelize.literal(`(SELECT group_concat(a.lastFirst, ", ") FROM authors AS a, bookAuthors as ba WHERE ba.authorId = a.id AND ba.bookId = book.id)`), 'author_name'] + ] + } + } else if (sortBy === 'media.metadata.authorName') { + bookAttributes = { + include: [ + [Sequelize.literal(`(SELECT group_concat(a.name, ", ") FROM authors AS a, bookAuthors as ba WHERE ba.authorId = a.id AND ba.bookId = book.id)`), 'author_name'] + ] + } + } + + const libraryItemWhere = { + libraryId + } + + let seriesInclude = { + model: Database.models.bookSeries, + attributes: ['seriesId', 'sequence', 'createdAt'], + include: { + model: Database.models.series, + attributes: ['id', 'name'] + }, + order: [ + ['createdAt', 'ASC'] + ], + separate: true + } + + let authorInclude = { + model: Database.models.bookAuthor, + attributes: ['authorId', 'createdAt'], + include: { + model: Database.models.author, + attributes: ['id', 'name'] + }, + order: [ + ['createdAt', 'ASC'] + ], + separate: true + } + + const libraryItemIncludes = [] + const bookIncludes = [] + if (filterGroup === 'feed-open') { + libraryItemIncludes.push({ + model: Database.models.feed, + required: true + }) + } else if (filterGroup === 'ebooks' && filterValue === 'supplementary') { + // TODO: Temp workaround for filtering supplementary ebook + libraryItemWhere['libraryFiles'] = { + [Sequelize.Op.substring]: `"isSupplementary":true` + } + } else if (filterGroup === 'missing' && filterValue === 'authors') { + authorInclude = { + model: Database.models.author, + attributes: ['id'], + through: { + attributes: [] + } + } + } else if ((filterGroup === 'series' && filterValue === 'no-series') || (filterGroup === 'missing' && filterValue === 'series')) { + seriesInclude = { + model: Database.models.series, + attributes: ['id'], + through: { + attributes: [] + } + } + } else if (filterGroup === 'authors') { + bookIncludes.push({ + model: Database.models.author, + attributes: ['id', 'name'], + where: { + id: filterValue + }, + through: { + attributes: [] + } + }) + } else if (filterGroup === 'series') { + bookIncludes.push({ + model: Database.models.series, + attributes: ['id', 'name'], + where: { + id: filterValue + }, + through: { + attributes: ['sequence'] + } + }) + } else if (filterGroup === 'issues') { + libraryItemWhere[Sequelize.Op.or] = [ + { + isMissing: true + }, + { + isInvalid: true + } + ] + } else if (filterGroup === 'progress') { + bookIncludes.push({ + model: Database.models.mediaProgress, + attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress'], + where: { + userId + }, + required: false + }) + } + + const { rows: books, count } = await Database.models.book.findAndCountAll({ + where: filterGroup ? this.getMediaGroupQuery(filterGroup, filterValue) : null, + distinct: true, + attributes: bookAttributes, + include: [ + { + model: Database.models.libraryItem, + required: true, + where: libraryItemWhere, + include: libraryItemIncludes + }, + seriesInclude, + authorInclude, + ...bookIncludes + ], + order: this.getOrder(sortBy, sortDesc), + subQuery: false, + limit, + offset + }) + + const libraryItems = books.map((bookExpanded) => { + const libraryItem = bookExpanded.libraryItem.toJSON() + const book = bookExpanded.toJSON() + delete book.libraryItem + delete book.authors + delete book.series + libraryItem.media = book + + return libraryItem + }) + Logger.debug('Found', libraryItems.length, 'library items', 'total=', count) + return { + libraryItems, + count + } + } +} \ No newline at end of file diff --git a/server/utils/queries/libraryItemsProgressFilters.js b/server/utils/queries/libraryItemsProgressFilters.js deleted file mode 100644 index 8eee4175..00000000 --- a/server/utils/queries/libraryItemsProgressFilters.js +++ /dev/null @@ -1,130 +0,0 @@ -const Sequelize = require('sequelize') -const Database = require('../../Database') -const Logger = require('../../Logger') - -module.exports = { - getOrder(sortBy, sortDesc) { - const dir = sortDesc ? 'DESC' : 'ASC' - if (sortBy === 'addedAt') { - return [[Sequelize.literal('libraryItem.createdAt'), dir]] - } else if (sortBy === 'size') { - return [[Sequelize.literal('libraryItem.size'), dir]] - } else if (sortBy === 'birthtimeMs') { - return [[Sequelize.literal('libraryItem.birthtime'), dir]] - } else if (sortBy === 'mtimeMs') { - return [[Sequelize.literal('libraryItem.mtime'), dir]] - } else if (sortBy === 'media.duration') { - return [['duration', dir]] - } else if (sortBy === 'media.metadata.publishedYear') { - return [['publishedYear', dir]] - } else if (sortBy === 'media.metadata.authorNameLF') { - return [] // TODO: Handle author filter - } else if (sortBy === 'media.metadata.authorName') { - return [] // TODO: Handle author filter - } else if (sortBy === 'media.metadata.title') { - if (global.ServerSettings.sortingIgnorePrefix) { - return [['titleIgnorePrefix', dir]] - } else { - return [['title', dir]] - } - } - return [] - }, - - async getLibraryItemsWithProgressFilter(filterValue, libraryId, userId, sortBy, sortDesc, limit, offset) { - - const bookWhere = {} - if (filterValue === 'not-finished') { - bookWhere['$mediaProgresses.isFinished$'] = { - [Sequelize.Op.or]: [null, false] - } - } else if (filterValue === 'not-started') { - bookWhere['$mediaProgresses.currentTime$'] = { - [Sequelize.Op.or]: [null, 0] - } - } else if (filterValue === 'finished') { - bookWhere['$mediaProgresses.isFinished$'] = true - } else { // in-progress - bookWhere[Sequelize.Op.and] = [ - { - '$book.mediaProgresses.currentTime$': { - [Sequelize.Op.gt]: 0 - } - }, - { - '$book.mediaProgresses.isFinished$': false - } - ] - } - - - const { rows: books, count } = await Database.models.book.findAndCountAll({ - where: bookWhere, - distinct: true, - include: [ - { - model: Database.models.libraryItem, - required: true, - where: { - libraryId - } - }, - { - model: Database.models.bookSeries, - attributes: ['seriesId', 'sequence'], - include: { - model: Database.models.series, - attributes: ['id', 'name'] - }, - separate: true - }, - { - model: Database.models.bookAuthor, - attributes: ['authorId'], - include: { - model: Database.models.author, - attributes: ['id', 'name'] - }, - separate: true - }, - { - model: Database.models.mediaProgress, - attributes: ['id', 'isFinished'], - where: { - userId - }, - required: false - } - ], - order: this.getOrder(sortBy, sortDesc), - subQuery: false, - limit, - offset - }) - - const libraryItems = books.map((bookExpanded) => { - const libraryItem = bookExpanded.libraryItem.toJSON() - const book = bookExpanded.toJSON() - delete book.libraryItem - - book.authors = [] - if (book.bookAuthors?.length) { - book.bookAuthors.forEach((ba) => { - if (ba.author) { - book.authors.push(ba.author) - } - }) - } - delete book.bookAuthors - - libraryItem.media = book - - return libraryItem - }) - Logger.debug('Found', libraryItems.length, 'library items', 'total=', count) - return { - libraryItems, - count - } - } -} \ No newline at end of file diff --git a/server/utils/queries/libraryItemsSeriesFilters.js b/server/utils/queries/libraryItemsSeriesFilters.js deleted file mode 100644 index 6a4b44c4..00000000 --- a/server/utils/queries/libraryItemsSeriesFilters.js +++ /dev/null @@ -1,96 +0,0 @@ -const Sequelize = require('sequelize') -const Database = require('../../Database') -const Logger = require('../../Logger') - -module.exports = { - getOrder(sortBy, sortDesc) { - const dir = sortDesc ? 'DESC' : 'ASC' - if (sortBy === 'addedAt') { - return [[Sequelize.literal('libraryItem.createdAt'), dir]] - } else if (sortBy === 'size') { - return [[Sequelize.literal('libraryItem.size'), dir]] - } else if (sortBy === 'birthtimeMs') { - return [[Sequelize.literal('libraryItem.birthtime'), dir]] - } else if (sortBy === 'mtimeMs') { - return [[Sequelize.literal('libraryItem.mtime'), dir]] - } else if (sortBy === 'media.duration') { - return [['duration', dir]] - } else if (sortBy === 'media.metadata.publishedYear') { - return [['publishedYear', dir]] - } else if (sortBy === 'media.metadata.authorNameLF') { - return [] // TODO: Handle author filter - } else if (sortBy === 'media.metadata.authorName') { - return [] // TODO: Handle author filter - } else if (sortBy === 'media.metadata.title') { - if (global.ServerSettings.sortingIgnorePrefix) { - return [['titleIgnorePrefix', dir]] - } else { - return [['title', dir]] - } - } - return [] - }, - - async getLibraryItemsWithNoSeries(libraryId, sortBy, sortDesc, limit, offset) { - const { rows: books, count } = await Database.models.book.findAndCountAll({ - where: { - '$series.id$': null - }, - distinct: true, - include: [ - { - model: Database.models.libraryItem, - required: true, - where: { - libraryId - } - }, - { - model: Database.models.series, - attributes: ['id', 'name'], - through: { - attributes: ['sequence'] - }, - }, - { - model: Database.models.bookAuthor, - attributes: ['authorId'], - include: { - model: Database.models.author, - attributes: ['id', 'name'] - }, - separate: true - } - ], - order: this.getOrder(sortBy, sortDesc), - subQuery: false, - limit, - offset - }) - - const libraryItems = books.map((bookExpanded) => { - const libraryItem = bookExpanded.libraryItem.toJSON() - const book = bookExpanded.toJSON() - delete book.libraryItem - - book.authors = [] - if (book.bookAuthors?.length) { - book.bookAuthors.forEach((ba) => { - if (ba.author) { - book.authors.push(ba.author) - } - }) - } - delete book.bookAuthors - - libraryItem.media = book - - return libraryItem - }) - Logger.debug('Found', libraryItems.length, 'library items', 'total=', count) - return { - libraryItems, - count - } - } -} \ No newline at end of file