Update continue series shelf queries

This commit is contained in:
advplyr 2023-08-04 17:24:06 -05:00
parent 58da095bcf
commit 8edab98163
3 changed files with 114 additions and 47 deletions

View File

@ -437,6 +437,14 @@ module.exports = (sequelize) => {
} }
} }
/**
* Get home page data personalized shelves
* @param {oldLibrary} library
* @param {string} userId
* @param {string[]} include
* @param {number} limit
* @returns {object[]} array of shelf objects
*/
static async getPersonalizedShelves(library, userId, include, limit) { static async getPersonalizedShelves(library, userId, include, limit) {
const isPodcastLibrary = library.mediaType === 'podcast' const isPodcastLibrary = library.mediaType === 'podcast'
const shelves = [] const shelves = []

View File

@ -33,6 +33,15 @@ module.exports = {
} }
}, },
/**
* Get library items for continue listening & continue reading shelves
* @param {oldLibrary} library
* @param {string} userId
* @param {string[]} include
* @param {number} limit
* @param {boolean} ebook true if continue reading shelf
* @returns {object} { libraryItems:LibraryItem[], count:number }
*/
async getLibraryItemsInProgress(library, userId, include, limit, ebook = false) { async getLibraryItemsInProgress(library, userId, include, limit, ebook = false) {
if (library.mediaType === 'book') { if (library.mediaType === 'book') {
const filterValue = ebook ? 'ebook-in-progress' : 'audio-in-progress' const filterValue = ebook ? 'ebook-in-progress' : 'audio-in-progress'
@ -55,6 +64,14 @@ module.exports = {
} }
}, },
/**
* Get library items for most recently added shelf
* @param {oldLibrary} library
* @param {string} userId
* @param {string[]} include
* @param {number} limit
* @returns {object} { libraryItems:LibraryItem[], count:number }
*/
async getLibraryItemsMostRecentlyAdded(library, userId, include, limit) { async getLibraryItemsMostRecentlyAdded(library, userId, include, limit) {
if (library.mediaType === 'book') { if (library.mediaType === 'book') {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, null, null, 'addedAt', true, false, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, null, null, 'addedAt', true, false, include, limit, 0)
@ -89,6 +106,14 @@ module.exports = {
} }
}, },
/**
* Get library items for continue series shelf
* @param {string} library
* @param {string} userId
* @param {string[]} include
* @param {number} limit
* @returns {object} { libraryItems:LibraryItem[], count:number }
*/
async getLibraryItemsContinueSeries(library, userId, include, limit) { async getLibraryItemsContinueSeries(library, userId, include, limit) {
const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, userId, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, userId, include, limit, 0)
return { return {

View File

@ -215,7 +215,7 @@ module.exports = {
} }
} else if (sortBy === 'sequence') { } else if (sortBy === 'sequence') {
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST' const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
return [[Sequelize.literal(`\`series.bookSeries.sequence\` COLLATE NOCASE ${nullDir}`)]] return [[Sequelize.literal(`CAST(\`series.bookSeries.sequence\` AS INTEGER) COLLATE NOCASE ${nullDir}`)]]
} else if (sortBy === 'progress') { } else if (sortBy === 'progress') {
return [[Sequelize.literal('mediaProgresses.updatedAt'), dir]] return [[Sequelize.literal('mediaProgresses.updatedAt'), dir]]
} }
@ -253,7 +253,7 @@ module.exports = {
} }
], ],
order: [ order: [
Sequelize.literal('`books.bookSeries.sequence` COLLATE NOCASE ASC NULLS LAST') Sequelize.literal('CAST(`books.bookSeries.sequence` AS INTEGER) COLLATE NOCASE ASC NULLS LAST')
] ]
}) })
const bookSeriesToInclude = [] const bookSeriesToInclude = []
@ -411,7 +411,7 @@ module.exports = {
}) })
if (sortBy !== 'sequence') { if (sortBy !== 'sequence') {
// Secondary sort by sequence // Secondary sort by sequence
sortOrder.push([Sequelize.literal('`series.bookSeries.sequence` COLLATE NOCASE ASC NULLS LAST')]) sortOrder.push([Sequelize.literal('CAST(`series.bookSeries.sequence` AS INTEGER) COLLATE NOCASE ASC NULLS LAST')])
} }
} else if (filterGroup === 'issues') { } else if (filterGroup === 'issues') {
libraryItemWhere[Sequelize.Op.or] = [ libraryItemWhere[Sequelize.Op.or] = [
@ -549,7 +549,22 @@ module.exports = {
} }
}, },
/**
* Get library items for continue series shelf
* A series is included on the shelf if it meets the following:
* 1. Has at least 1 finished book
* 2. Has no books in progress
* 3. Has at least 1 unfinished book
* TODO: Reduce queries
* @param {string} libraryId
* @param {string} userId
* @param {string[]} include
* @param {number} limit
* @param {number} offset
* @returns {object} { libraryItems:LibraryItem[], count:number }
*/
async getContinueSeriesLibraryItems(libraryId, userId, include, limit, offset) { async getContinueSeriesLibraryItems(libraryId, userId, include, limit, offset) {
// Step 1: Get all media progress for user that belongs to a series book
const mediaProgressForUserForSeries = await Database.models.mediaProgress.findAll({ const mediaProgressForUserForSeries = await Database.models.mediaProgress.findAll({
where: { where: {
userId userId
@ -571,46 +586,52 @@ module.exports = {
] ]
}) })
let seriesToIncludeMap = {} // Step 1.5: Identify the series that have at least 1 finished book and have no books in progress
let seriesToInclude = []
let seriesToExclude = [] let seriesToExclude = []
for (const prog of mediaProgressForUserForSeries) { for (const prog of mediaProgressForUserForSeries) {
const series = prog.mediaItem?.series || [] const series = prog.mediaItem?.series || []
for (const s of series) { for (const s of series) {
if (prog.currentTime > 0 && !prog.isFinished) { // in-progress if (prog.currentTime > 0 && !prog.isFinished) { // in-progress
delete seriesToIncludeMap[s.id] seriesToInclude = seriesToInclude.filter(sid => sid !== s.id)
if (!seriesToExclude.includes(s.id)) seriesToExclude.push(s.id) if (!seriesToExclude.includes(s.id)) seriesToExclude.push(s.id)
} else if (prog.isFinished && !seriesToExclude.includes(s.id)) { // finished } else if (prog.isFinished && !seriesToExclude.includes(s.id) && !seriesToInclude.includes(s.id)) { // finished
const lastUpdate = prog.updatedAt?.valueOf() || 0 seriesToInclude.push(s.id)
if (!seriesToIncludeMap[s.id] || lastUpdate > seriesToIncludeMap[s.id]) {
seriesToIncludeMap[s.id] = lastUpdate
}
} }
} }
} }
// optional include rssFeed with library item
const libraryItemIncludes = []
if (include.includes('rssfeed')) {
libraryItemIncludes.push({
model: Database.models.feed
})
}
// Step 2: Get all series identified in step 1.5 and filter out series where all books are finished
const { rows: series, count } = await Database.models.series.findAndCountAll({ const { rows: series, count } = await Database.models.series.findAndCountAll({
where: { where: {
id: { id: {
[Sequelize.Op.in]: Object.keys(seriesToIncludeMap) [Sequelize.Op.in]: seriesToInclude
}, },
'$books.mediaProgresses.isFinished$': { '$bookSeries.book.mediaProgresses.isFinished$': {
[Sequelize.Op.or]: [false, null] [Sequelize.Op.or]: [false, null]
} }
}, },
distinct: true, distinct: true,
include: [ include: [
{ {
model: Database.models.bookSeries,
include: {
model: Database.models.book, model: Database.models.book,
through: {
attributes: ['sequence']
},
required: true,
include: [ include: [
{ {
model: Database.models.libraryItem, model: Database.models.libraryItem,
where: { where: {
libraryId libraryId
} },
include: libraryItemIncludes
}, },
{ {
model: Database.models.bookAuthor, model: Database.models.bookAuthor,
@ -627,12 +648,15 @@ module.exports = {
}, },
required: false required: false
} }
] ],
required: true
},
required: true
} }
], ],
order: [ order: [
[Sequelize.literal(`CAST(\`books.bookSeries.sequence\` AS INTEGER) COLLATE NOCASE ASC NULLS LAST`)], // Sort by progress most recently updated
[Sequelize.literal(`\`books.mediaProgresses.updatedAt\` DESC`)] [Database.models.bookSeries, Database.models.book, Database.models.mediaProgress, 'updatedAt', 'DESC'],
], ],
subQuery: false, subQuery: false,
limit, limit,
@ -641,20 +665,30 @@ module.exports = {
Logger.debug('Found', series.length, 'series to continue', 'total=', count) Logger.debug('Found', series.length, 'series to continue', 'total=', count)
// Step 3: Map series to library items by selecting the first unfinished book in the series
const libraryItems = series.map(s => { const libraryItems = series.map(s => {
const book = s.books.find(book => { // Natural sort sequence, nulls last
return !book.mediaProgresses?.[0]?.isFinished // TODO: sort in query. was unable to sort nested association with sequelize
s.bookSeries.sort((a, b) => {
if (!a.sequence) return 1
if (!b.sequence) return -1
return a.sequence.localeCompare(b.sequence, undefined, {
numeric: true,
sensitivity: 'base'
}) })
const libraryItem = book.libraryItem.toJSON() })
// Get first unfinished book to use
const bookSeries = s.bookSeries.find(bs => !bs.book.mediaProgresses?.[0]?.isFinished)
const libraryItem = bookSeries.book.libraryItem.toJSON()
libraryItem.series = { libraryItem.series = {
id: s.id, id: s.id,
name: s.name, name: s.name,
sequence: book.bookSeries.sequence sequence: bookSeries.sequence
} }
delete book.bookSeries
libraryItem.media = book libraryItem.media = bookSeries.book
return libraryItem return libraryItem
}) })
return { return {