From a668921e29f0948b170b806053702bc94450695e Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 7 May 2024 18:16:32 +0300 Subject: [PATCH 1/9] Prettier-only formatting changes --- .../utils/queries/libraryItemsBookFilters.js | 210 ++++++++---------- 1 file changed, 95 insertions(+), 115 deletions(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 12bc970198..a728aaf5a6 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -7,7 +7,7 @@ const { asciiOnlyToLowerCase } = require('../index') module.exports = { /** * User permissions to restrict books for explicit content & tags - * @param {import('../../objects/user/User')} user + * @param {import('../../objects/user/User')} user * @returns {{ bookWhere:Sequelize.WhereOptions, replacements:object }} */ getUserPermissionBookWhereQuery(user) { @@ -25,9 +25,11 @@ module.exports = { if (user.permissions.selectedTagsNotAccessible) { bookWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0)) } else { - bookWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), { - [Sequelize.Op.gte]: 1 - })) + bookWhere.push( + Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), { + [Sequelize.Op.gte]: 1 + }) + ) } } return { @@ -39,8 +41,8 @@ module.exports = { /** * When collapsing series and filtering by progress * different where options are required - * - * @param {string} value + * + * @param {string} value * @returns {Sequelize.WhereOptions} */ getCollapseSeriesMediaProgressFilter(value) { @@ -90,8 +92,8 @@ module.exports = { /** * Get where options for Book model - * @param {string} group - * @param {[string]} value + * @param {string} group + * @param {[string]} value * @returns {object} { Sequelize.WhereOptions, string[] } */ getMediaGroupQuery(group, value) { @@ -170,7 +172,7 @@ module.exports = { Sequelize.where(Sequelize.fn('json_array_length', Sequelize.col('audioFiles')), 0), { '$mediaProgresses.isFinished$': true, - 'ebookFile': { + ebookFile: { [Sequelize.Op.not]: null } } @@ -232,8 +234,8 @@ module.exports = { /** * Get sequelize order - * @param {string} sortBy - * @param {boolean} sortDesc + * @param {string} sortBy + * @param {boolean} sortDesc * @param {boolean} collapseseries * @returns {Sequelize.order} */ @@ -278,18 +280,14 @@ module.exports = { * When collapsing series get first book in each series * to know which books to exclude from primary query. * Additionally use this query to get the number of books in each series - * - * @param {Sequelize.ModelStatic} bookFindOptions - * @param {Sequelize.WhereOptions} seriesWhere + * + * @param {Sequelize.ModelStatic} bookFindOptions + * @param {Sequelize.WhereOptions} seriesWhere * @returns {object} { booksToExclude, bookSeriesToInclude } */ async getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere) { const allSeries = await Database.seriesModel.findAll({ - attributes: [ - 'id', - 'name', - [Sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 'numBooks'] - ], + attributes: ['id', 'name', [Sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 'numBooks']], distinct: true, subQuery: false, where: seriesWhere, @@ -304,14 +302,12 @@ module.exports = { required: true } ], - order: [ - Sequelize.literal('CAST(`books.bookSeries.sequence` AS FLOAT) ASC NULLS LAST') - ] + order: [Sequelize.literal('CAST(`books.bookSeries.sequence` AS FLOAT) ASC NULLS LAST')] }) const bookSeriesToInclude = [] const booksToInclude = [] let booksToExclude = [] - allSeries.forEach(s => { + allSeries.forEach((s) => { let found = false for (let book of s.books) { if (!found && !booksToInclude.includes(book.id)) { @@ -320,7 +316,7 @@ module.exports = { id: book.bookSeries.id, numBooks: s.dataValues.numBooks }) - booksToExclude = booksToExclude.filter(bid => bid !== book.id) + booksToExclude = booksToExclude.filter((bid) => bid !== book.id) found = true } else if (!booksToExclude.includes(book.id) && !booksToInclude.includes(book.id)) { booksToExclude.push(book.id) @@ -332,16 +328,16 @@ module.exports = { /** * Get library items for book media type using filter and sort - * @param {string} libraryId + * @param {string} libraryId * @param {[oldUser]} user - * @param {[string]} filterGroup - * @param {[string]} filterValue - * @param {string} sortBy - * @param {string} sortDesc + * @param {[string]} filterGroup + * @param {[string]} filterValue + * @param {string} sortBy + * @param {string} sortDesc * @param {boolean} collapseseries * @param {string[]} include - * @param {number} limit - * @param {number} offset + * @param {number} limit + * @param {number} offset * @param {boolean} isHomePage for home page shelves * @returns {object} { libraryItems:LibraryItem[], count:number } */ @@ -363,15 +359,11 @@ module.exports = { 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'] - ] + 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'] - ] + 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']] } } @@ -386,9 +378,7 @@ module.exports = { model: Database.seriesModel, attributes: ['id', 'name', 'nameIgnorePrefix'] }, - order: [ - ['createdAt', 'ASC'] - ], + order: [['createdAt', 'ASC']], separate: true } @@ -399,9 +389,7 @@ module.exports = { model: Database.authorModel, attributes: ['id', 'name'] }, - order: [ - ['createdAt', 'ASC'] - ], + order: [['createdAt', 'ASC']], separate: true } @@ -427,7 +415,7 @@ module.exports = { } } else if (filterGroup === 'ebooks' && filterValue === 'no-supplementary') { libraryItemWhere['libraryFiles'] = { - [Sequelize.Op.notLike]: Sequelize.literal(`\'%"isSupplementary":true%\'`), + [Sequelize.Op.notLike]: Sequelize.literal(`\'%"isSupplementary":true%\'`) } } else if (filterGroup === 'missing' && filterValue === 'authors') { authorInclude = { @@ -496,7 +484,7 @@ module.exports = { }) } else if (filterGroup === 'recent') { 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 } } @@ -551,9 +539,9 @@ module.exports = { // When collapsing series and sorting by title then use the series name instead of the book title // for this set an attribute "display_title" to use in sorting if (global.ServerSettings.sortingIgnorePrefix) { - bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map(v => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title']) + bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.nameIgnorePrefix FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), titleIgnorePrefix)`), 'display_title']) } else { - bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map(v => `"${v.id}"`).join(', ')})), \`book\`.\`title\`)`), 'display_title']) + bookAttributes.include.push([Sequelize.literal(`IFNULL((SELECT s.name FROM bookSeries AS bs, series AS s WHERE bs.seriesId = s.id AND bs.bookId = book.id AND bs.id IN (${bookSeriesToInclude.map((v) => `"${v.id}"`).join(', ')})), \`book\`.\`title\`)`), 'display_title']) } } @@ -598,9 +586,9 @@ module.exports = { // For showing details of collapsed series if (collapseseries && book.bookSeries?.length) { - const collapsedSeries = book.bookSeries.find(bs => collapseSeriesBookSeries.some(cbs => cbs.id === bs.id)) + const collapsedSeries = book.bookSeries.find((bs) => collapseSeriesBookSeries.some((cbs) => cbs.id === bs.id)) if (collapsedSeries) { - const collapseSeriesObj = collapseSeriesBookSeries.find(csbs => csbs.id === collapsedSeries.id) + const collapseSeriesObj = collapseSeriesBookSeries.find((csbs) => csbs.id === collapsedSeries.id) libraryItem.collapsedSeries = { id: collapsedSeries.series.id, name: collapsedSeries.series.name, @@ -633,11 +621,11 @@ module.exports = { * 2. Has no books in progress * 3. Has at least 1 unfinished book * TODO: Reduce queries - * @param {import('../../objects/Library')} library - * @param {import('../../objects/user/User')} user - * @param {string[]} include - * @param {number} limit - * @param {number} offset + * @param {import('../../objects/Library')} library + * @param {import('../../objects/user/User')} user + * @param {string[]} include + * @param {number} limit + * @param {number} offset * @returns {{ libraryItems:import('../../models/LibraryItem')[], count:number }} */ async getContinueSeriesLibraryItems(library, user, include, limit, offset) { @@ -655,15 +643,13 @@ module.exports = { const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user) bookWhere.push(...userPermissionBookWhere.bookWhere) - let includeAttributes = [ - [Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress'], - ] + let includeAttributes = [[Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress']] let booksNotFinishedQuery = `SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId AND mp.userId = :userId WHERE bs.seriesId = series.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL)` if (library.settings.onlyShowLaterBooksInContinueSeries) { const maxSequenceQuery = `(SELECT CAST(max(bs.sequence) as FLOAT) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.isFinished = 1 AND mp.userId = :userId AND bs.seriesId = series.id)` includeAttributes.push([Sequelize.literal(`${maxSequenceQuery}`), 'maxSequence']) - + booksNotFinishedQuery = booksNotFinishedQuery + ` AND CAST(bs.sequence as FLOAT) > ${maxSequenceQuery}` } @@ -699,9 +685,7 @@ module.exports = { attributes: ['bookId', 'sequence'], separate: true, subQuery: false, - order: [ - [Sequelize.literal('CAST(sequence AS FLOAT) ASC NULLS LAST')] - ], + order: [[Sequelize.literal('CAST(sequence AS FLOAT) ASC NULLS LAST')]], where: { '$book.mediaProgresses.isFinished$': { [Sequelize.Op.or]: [null, 0] @@ -731,44 +715,44 @@ module.exports = { ] } }, - order: [ - [Sequelize.literal('recent_progress DESC')] - ], + order: [[Sequelize.literal('recent_progress DESC')]], distinct: true, subQuery: false, limit, offset }) - const libraryItems = series.map(s => { - if (!s.bookSeries.length) return null // this is only possible if user has restricted books in series + const libraryItems = series + .map((s) => { + if (!s.bookSeries.length) return null // this is only possible if user has restricted books in series - let bookIndex = 0 - // if the library setting is toggled, only show later entries in series, otherwise skip - if (library.settings.onlyShowLaterBooksInContinueSeries) { - bookIndex = s.bookSeries.findIndex(function (b) { - return parseFloat(b.dataValues.sequence) > s.dataValues.maxSequence - }) - if (bookIndex === -1) { - // no later books than maxSequence - return null + let bookIndex = 0 + // if the library setting is toggled, only show later entries in series, otherwise skip + if (library.settings.onlyShowLaterBooksInContinueSeries) { + bookIndex = s.bookSeries.findIndex(function (b) { + return parseFloat(b.dataValues.sequence) > s.dataValues.maxSequence + }) + if (bookIndex === -1) { + // no later books than maxSequence + return null + } } - } - const libraryItem = s.bookSeries[bookIndex].book.libraryItem.toJSON() - const book = s.bookSeries[bookIndex].book.toJSON() - delete book.libraryItem - libraryItem.series = { - id: s.id, - name: s.name, - sequence: s.bookSeries[bookIndex].sequence - } - if (libraryItem.feeds?.length) { - libraryItem.rssFeed = libraryItem.feeds[0] - } - libraryItem.media = book - return libraryItem - }).filter(s => s) + const libraryItem = s.bookSeries[bookIndex].book.libraryItem.toJSON() + const book = s.bookSeries[bookIndex].book.toJSON() + delete book.libraryItem + libraryItem.series = { + id: s.id, + name: s.name, + sequence: s.bookSeries[bookIndex].sequence + } + if (libraryItem.feeds?.length) { + libraryItem.rssFeed = libraryItem.feeds[0] + } + libraryItem.media = book + return libraryItem + }) + .filter((s) => s) return { libraryItems, @@ -780,10 +764,10 @@ module.exports = { * Get book library items for the "Discover" shelf * Random selection of books that are not started * - only includes the first book of a not-started series - * @param {string} libraryId - * @param {oldUser} user - * @param {string[]} include - * @param {number} limit + * @param {string} libraryId + * @param {oldUser} user + * @param {string[]} include + * @param {number} limit * @returns {object} {libraryItems:LibraryItem, count:number} */ async getDiscoverLibraryItems(libraryId, user, include, limit) { @@ -811,9 +795,7 @@ module.exports = { model: Database.bookModel, where: userPermissionBookWhere.bookWhere }, - order: [ - [Sequelize.literal('CAST(sequence AS FLOAT) ASC NULLS LAST')] - ], + order: [[Sequelize.literal('CAST(sequence AS FLOAT) ASC NULLS LAST')]], limit: 1 }, subQuery: false, @@ -821,7 +803,7 @@ module.exports = { order: Database.sequelize.random() }) - const booksFromSeriesToInclude = seriesNotStarted.map(se => se.bookSeries?.[0]?.bookId).filter(bid => bid) + const booksFromSeriesToInclude = seriesNotStarted.map((se) => se.bookSeries?.[0]?.bookId).filter((bid) => bid) // optional include rssFeed const libraryItemIncludes = [] @@ -913,7 +895,7 @@ module.exports = { /** * Get book library items in a collection - * @param {oldCollection} collection + * @param {oldCollection} collection * @returns {Promise} */ async getLibraryItemsForCollection(collection) { @@ -957,22 +939,22 @@ module.exports = { /** * Get library items for series - * @param {import('../../objects/entities/Series')} oldSeries - * @param {import('../../objects/user/User')} [oldUser] + * @param {import('../../objects/entities/Series')} oldSeries + * @param {import('../../objects/user/User')} [oldUser] * @returns {Promise} */ async getLibraryItemsForSeries(oldSeries, oldUser) { const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, oldUser, 'series', oldSeries.id, null, null, false, [], null, null) - return libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li)) + return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li)) }, /** * Search books, authors, series * @param {import('../../objects/user/User')} oldUser - * @param {import('../../objects/Library')} oldLibrary - * @param {string} query - * @param {number} limit - * @param {number} offset + * @param {import('../../objects/Library')} oldLibrary + * @param {string} query + * @param {number} limit + * @param {number} offset * @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[]}} */ async search(oldUser, oldLibrary, query, limit, offset) { @@ -1151,7 +1133,7 @@ module.exports = { /** * Genres with num books - * @param {string} libraryId + * @param {string} libraryId * @returns {{genre:string, count:number}[]} */ async getGenresWithCount(libraryId) { @@ -1173,7 +1155,7 @@ module.exports = { /** * Get stats for book library - * @param {string} libraryId + * @param {string} libraryId * @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>} */ async getBookLibraryStats(libraryId) { @@ -1187,8 +1169,8 @@ module.exports = { /** * Get longest books in library - * @param {string} libraryId - * @param {number} limit + * @param {string} libraryId + * @param {number} limit * @returns {Promise<{ id:string, title:string, duration:number }[]>} */ async getLongestBooks(libraryId, limit) { @@ -1201,12 +1183,10 @@ module.exports = { libraryId } }, - order: [ - ['duration', 'DESC'] - ], + order: [['duration', 'DESC']], limit }) - return books.map(book => { + return books.map((book) => { return { id: book.libraryItem.id, title: book.title, @@ -1214,4 +1194,4 @@ module.exports = { } }) } -} \ No newline at end of file +} From c6740423198c6d845b356f270e84fd9f828ddd81 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 7 May 2024 18:19:55 +0300 Subject: [PATCH 2/9] Add libraryItemIds to collapsedSeries objects --- server/utils/queries/libraryItemsBookFilters.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index a728aaf5a6..35eb2de273 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -314,7 +314,8 @@ module.exports = { booksToInclude.push(book.id) bookSeriesToInclude.push({ id: book.bookSeries.id, - numBooks: s.dataValues.numBooks + numBooks: s.dataValues.numBooks, + libraryItemIds: s.books?.map((b) => b.libraryItem.id) || [] }) booksToExclude = booksToExclude.filter((bid) => bid !== book.id) found = true @@ -594,7 +595,8 @@ module.exports = { name: collapsedSeries.series.name, nameIgnorePrefix: collapsedSeries.series.nameIgnorePrefix, sequence: collapsedSeries.sequence, - numBooks: collapseSeriesObj?.numBooks || 0 + numBooks: collapseSeriesObj?.numBooks || 0, + libraryItemIds: collapseSeriesObj?.libraryItemIds || [] } } } From e384527b672f8ce5d1c31b7f95488c75baadf99d Mon Sep 17 00:00:00 2001 From: mikiher Date: Wed, 8 May 2024 08:44:55 +0300 Subject: [PATCH 3/9] Simplify progress bar and show correct collapsed series progress --- client/components/cards/LazyBookCard.vue | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index faa93997ff..d89316900b 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -45,10 +45,8 @@ - -
- -
+ +