mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-04 20:24:27 +02:00
Update continue series shelf queries
This commit is contained in:
parent
58da095bcf
commit
8edab98163
@ -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 = []
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user