mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-01 00:18:14 +01:00
Add new personalized home page shelves API endpoint
This commit is contained in:
parent
7ec1d8ee5f
commit
b9633691f4
@ -631,8 +631,24 @@ class LibraryController {
|
|||||||
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/personalized
|
/**
|
||||||
// New and improved personalized call only loops through library items once
|
* GET: /api/libraries/:id/personalized2
|
||||||
|
* TODO: new endpoint
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
|
async getUserPersonalizedShelves(req, res) {
|
||||||
|
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
||||||
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
|
const shelves = await Database.models.libraryItem.getPersonalizedShelves(req.library, req.user.id, include, limitPerShelf)
|
||||||
|
res.json(shelves)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/libraries/:id/personalized
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
async getLibraryUserPersonalizedOptimal(req, res) {
|
async getLibraryUserPersonalizedOptimal(req, res) {
|
||||||
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
||||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
|
@ -437,6 +437,53 @@ module.exports = (sequelize) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getPersonalizedShelves(library, userId, include, limit) {
|
||||||
|
const isPodcastLibrary = library.mediaType === 'podcast'
|
||||||
|
const shelves = []
|
||||||
|
const itemsInProgressPayload = await libraryFilters.getLibraryItemsInProgress(library, userId, include, limit, false)
|
||||||
|
if (itemsInProgressPayload.libraryItems.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'continue-listening',
|
||||||
|
label: 'Continue Listening',
|
||||||
|
labelStringKey: 'LabelContinueListening',
|
||||||
|
type: isPodcastLibrary ? 'episode' : 'book',
|
||||||
|
entities: itemsInProgressPayload.libraryItems,
|
||||||
|
total: itemsInProgressPayload.count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (library.mediaType === 'book') {
|
||||||
|
const ebooksInProgressPayload = await libraryFilters.getLibraryItemsInProgress(library, userId, include, limit, true)
|
||||||
|
if (ebooksInProgressPayload.libraryItems.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'continue-reading',
|
||||||
|
label: 'Continue Reading',
|
||||||
|
labelStringKey: 'LabelContinueReading',
|
||||||
|
type: 'book',
|
||||||
|
entities: ebooksInProgressPayload.libraryItems,
|
||||||
|
total: ebooksInProgressPayload.count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, userId, include, limit)
|
||||||
|
if (mostRecentPayload.libraryItems.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'recently-added',
|
||||||
|
label: 'Recently Added',
|
||||||
|
labelStringKey: 'LabelRecentlyAdded',
|
||||||
|
type: library.mediaType,
|
||||||
|
entities: mostRecentPayload.libraryItems,
|
||||||
|
total: mostRecentPayload.count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle continue series library items
|
||||||
|
const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, userId, include, limit)
|
||||||
|
|
||||||
|
return shelves
|
||||||
|
}
|
||||||
|
|
||||||
getMedia(options) {
|
getMedia(options) {
|
||||||
if (!this.mediaType) return Promise.resolve(null)
|
if (!this.mediaType) return Promise.resolve(null)
|
||||||
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}`
|
const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}`
|
||||||
|
@ -83,6 +83,7 @@ class ApiRouter {
|
|||||||
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this))
|
this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/albums', LibraryController.middleware.bind(this), LibraryController.getAlbumsForLibrary.bind(this))
|
this.router.get('/libraries/:id/albums', LibraryController.middleware.bind(this), LibraryController.getAlbumsForLibrary.bind(this))
|
||||||
|
this.router.get('/libraries/:id/personalized2', LibraryController.middleware.bind(this), LibraryController.getUserPersonalizedShelves.bind(this))
|
||||||
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
|
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
|
||||||
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
|
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
|
||||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
const Database = require('../../Database')
|
||||||
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||||
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
|
||||||
|
|
||||||
@ -30,6 +31,65 @@ module.exports = {
|
|||||||
} else {
|
} else {
|
||||||
return libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, userId, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset)
|
return libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, userId, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getLibraryItemsInProgress(library, userId, include, limit, ebook = false) {
|
||||||
|
if (library.mediaType === 'book') {
|
||||||
|
const filterValue = ebook ? 'ebook-in-progress' : 'audio-in-progress'
|
||||||
|
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, 'progress', filterValue, 'progress', true, false, include, limit, 0)
|
||||||
|
return {
|
||||||
|
libraryItems: libraryItems.map(li => {
|
||||||
|
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||||
|
if (li.rssFeed) {
|
||||||
|
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified()
|
||||||
|
}
|
||||||
|
return oldLibraryItem
|
||||||
|
}),
|
||||||
|
count
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
count: 0,
|
||||||
|
libraryItems: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getLibraryItemsMostRecentlyAdded(library, userId, include, limit) {
|
||||||
|
if (library.mediaType === 'book') {
|
||||||
|
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, null, null, 'addedAt', true, false, include, limit, 0)
|
||||||
|
return {
|
||||||
|
libraryItems: libraryItems.map(li => {
|
||||||
|
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||||
|
if (li.rssFeed) {
|
||||||
|
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified()
|
||||||
|
}
|
||||||
|
if (li.size && !oldLibraryItem.media.size) {
|
||||||
|
oldLibraryItem.media.size = li.size
|
||||||
|
}
|
||||||
|
return oldLibraryItem
|
||||||
|
}),
|
||||||
|
count
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, userId, null, null, 'addedAt', true, include, limit, 0)
|
||||||
|
return {
|
||||||
|
libraryItems: libraryItems.map(li => {
|
||||||
|
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||||
|
if (li.rssFeed) {
|
||||||
|
oldLibraryItem.rssFeed = Database.models.feed.getOldFeed(li.rssFeed).toJSONMinified()
|
||||||
|
}
|
||||||
|
if (li.size && !oldLibraryItem.media.size) {
|
||||||
|
oldLibraryItem.media.size = li.size
|
||||||
|
}
|
||||||
|
return oldLibraryItem
|
||||||
|
}),
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getLibraryItemsContinueSeries(library, userId, include, limit) {
|
||||||
|
await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, userId, limit, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -107,6 +107,28 @@ module.exports = {
|
|||||||
'$mediaProgresses.isFinished$': false
|
'$mediaProgresses.isFinished$': false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
} else if (value === 'audio-in-progress') {
|
||||||
|
mediaWhere[Sequelize.Op.and] = [
|
||||||
|
{
|
||||||
|
'$mediaProgresses.currentTime$': {
|
||||||
|
[Sequelize.Op.gt]: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'$mediaProgresses.isFinished$': false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} else if (value === 'ebook-in-progress') {
|
||||||
|
mediaWhere[Sequelize.Op.and] = [
|
||||||
|
{
|
||||||
|
'$mediaProgresses.ebookProgress$': {
|
||||||
|
[Sequelize.Op.gt]: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'$mediaProgresses.isFinished$': false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
} else if (group === 'series' && value === 'no-series') {
|
} else if (group === 'series' && value === 'no-series') {
|
||||||
mediaWhere['$series.id$'] = null
|
mediaWhere['$series.id$'] = null
|
||||||
@ -194,6 +216,8 @@ 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(`\`series.bookSeries.sequence\` COLLATE NOCASE ${nullDir}`)]]
|
||||||
|
} else if (sortBy === 'progress') {
|
||||||
|
return [[Sequelize.literal('mediaProgresses.updatedAt'), dir]]
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
@ -275,6 +299,9 @@ module.exports = {
|
|||||||
if (filterGroup !== 'series' && sortBy === 'sequence') {
|
if (filterGroup !== 'series' && sortBy === 'sequence') {
|
||||||
sortBy = 'media.metadata.title'
|
sortBy = 'media.metadata.title'
|
||||||
}
|
}
|
||||||
|
if (filterGroup !== 'progress' && sortBy === 'progress') {
|
||||||
|
sortBy = 'media.metadata.title'
|
||||||
|
}
|
||||||
const includeRSSFeed = include.includes('rssfeed')
|
const includeRSSFeed = include.includes('rssfeed')
|
||||||
|
|
||||||
// For sorting by author name an additional attribute must be added
|
// For sorting by author name an additional attribute must be added
|
||||||
@ -398,7 +425,7 @@ module.exports = {
|
|||||||
} else if (filterGroup === 'progress') {
|
} else if (filterGroup === 'progress') {
|
||||||
bookIncludes.push({
|
bookIncludes.push({
|
||||||
model: Database.models.mediaProgress,
|
model: Database.models.mediaProgress,
|
||||||
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress'],
|
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'],
|
||||||
where: {
|
where: {
|
||||||
userId
|
userId
|
||||||
},
|
},
|
||||||
@ -520,5 +547,52 @@ module.exports = {
|
|||||||
libraryItems,
|
libraryItems,
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getContinueSeriesLibraryItems(libraryId, userId, limit, offset) {
|
||||||
|
const { rows: series, count } = await Database.models.series.findAndCountAll({
|
||||||
|
where: [
|
||||||
|
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM books b, bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = b.id AND mp.userId = :userId AND bs.bookId = b.id AND bs.seriesId = series.id AND mp.isFinished = 1)`), {
|
||||||
|
[Sequelize.Op.gt]: 0
|
||||||
|
}),
|
||||||
|
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM books b, bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = b.id AND mp.userId = :userId AND bs.bookId = b.id AND bs.seriesId = series.id AND mp.isFinished = 0 AND mp.currentTime > 0)`), 0),
|
||||||
|
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.userId = :userId AND mp.mediaItemId = bs.bookId WHERE bs.seriesId = series.id AND (mp.currentTime = 0 OR mp.currentTime IS NULL) AND (mp.isFinished = 0 OR mp.isFinished IS NULL))`), {
|
||||||
|
[Sequelize.Op.gt]: 0
|
||||||
|
})
|
||||||
|
],
|
||||||
|
replacements: {
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
distinct: true,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.book,
|
||||||
|
through: {
|
||||||
|
attributes: ['sequence']
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Database.models.libraryItem,
|
||||||
|
where: {
|
||||||
|
libraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Database.models.author,
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
through: {
|
||||||
|
attributes: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [[Sequelize.literal(`\`books.bookSeries.sequence\` COLLATE NOCASE ASC NULLS LAST`)]],
|
||||||
|
subQuery: false,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
})
|
||||||
|
|
||||||
|
Logger.debug('Found', series.length, 'series to continue', 'total=', count)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user