mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-01 00:18:14 +01:00
Update new library item API endpoint to handle collapse series
This commit is contained in:
parent
11120a3765
commit
eeaf012cdc
@ -313,7 +313,12 @@ export default {
|
||||
this.currentSFQueryString = this.buildSearchParams()
|
||||
}
|
||||
|
||||
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
||||
let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
||||
// TODO: Temp use new library items API for everything except podcasts and collapse sub-series
|
||||
if (entityPath === 'items' && !this.isPodcast && !this.collapseBookSeries && !(this.filterName === 'Series' && this.collapseSeries)) {
|
||||
entityPath += '2'
|
||||
}
|
||||
|
||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete`
|
||||
|
||||
|
@ -208,7 +208,7 @@ class LibraryController {
|
||||
payload.offset = payload.page * payload.limit
|
||||
|
||||
const { libraryItems, count } = await Database.models.libraryItem.getByFilterAndSort(req.library.id, req.user.id, payload)
|
||||
payload.results = libraryItems.map(li => li.toJSONMinified())
|
||||
payload.results = libraryItems
|
||||
payload.total = count
|
||||
|
||||
res.json(payload)
|
||||
|
@ -17,7 +17,7 @@ module.exports = (sequelize) => {
|
||||
}
|
||||
|
||||
static getOldFeed(feedExpanded) {
|
||||
const episodes = feedExpanded.feedEpisodes.map((feedEpisode) => feedEpisode.getOldEpisode())
|
||||
const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode())
|
||||
return new oldFeed({
|
||||
id: feedExpanded.id,
|
||||
slug: feedExpanded.slug,
|
||||
@ -42,7 +42,7 @@ module.exports = (sequelize) => {
|
||||
},
|
||||
serverAddress: feedExpanded.serverAddress,
|
||||
feedUrl: feedExpanded.feedURL,
|
||||
episodes,
|
||||
episodes: episodes || [],
|
||||
createdAt: feedExpanded.createdAt.valueOf(),
|
||||
updatedAt: feedExpanded.updatedAt.valueOf()
|
||||
})
|
||||
|
@ -400,10 +400,22 @@ module.exports = (sequelize) => {
|
||||
})
|
||||
}
|
||||
|
||||
static async getByFilterAndSort(libraryId, userId, { filterBy, sortBy, sortDesc, limit, offset }) {
|
||||
const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(libraryId, filterBy, sortBy, sortDesc, limit, offset, userId)
|
||||
static async getByFilterAndSort(libraryId, userId, options) {
|
||||
const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(libraryId, userId, options)
|
||||
return {
|
||||
libraryItems: libraryItems.map(ti => this.getOldLibraryItem(ti)),
|
||||
libraryItems: libraryItems.map(li => {
|
||||
const oldLibraryItem = this.getOldLibraryItem(li).toJSONMinified()
|
||||
if (li.collapsedSeries) {
|
||||
oldLibraryItem.collapsedSeries = li.collapsedSeries
|
||||
}
|
||||
if (li.series) {
|
||||
oldLibraryItem.media.metadata.series = li.series
|
||||
}
|
||||
if (li.rssFeed) {
|
||||
oldLibraryItem.rssFeed = sequelize.models.feed.getOldFeed(li.rssFeed).toJSONMinified()
|
||||
}
|
||||
return oldLibraryItem
|
||||
}),
|
||||
count
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ module.exports = {
|
||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||
},
|
||||
|
||||
async getFilteredLibraryItems(libraryId, filterBy, sortBy, sortDesc, limit, offset, userId) {
|
||||
async getFilteredLibraryItems(libraryId, userId, options) {
|
||||
const { filterBy, sortBy, sortDesc, limit, offset, collapseseries, include } = options
|
||||
|
||||
let filterValue = null
|
||||
let filterGroup = null
|
||||
if (filterBy) {
|
||||
@ -16,6 +18,6 @@ module.exports = {
|
||||
}
|
||||
|
||||
// TODO: Handle podcast filters
|
||||
return libraryItemsBookFilters.getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset)
|
||||
return libraryItemsBookFilters.getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset)
|
||||
}
|
||||
}
|
@ -3,6 +3,51 @@ const Database = require('../../Database')
|
||||
const Logger = require('../../Logger')
|
||||
|
||||
module.exports = {
|
||||
getCollapseSeriesMediaProgressFilter(value) {
|
||||
const mediaWhere = {}
|
||||
if (value === 'not-finished') {
|
||||
mediaWhere['$books.mediaProgresses.isFinished$'] = {
|
||||
[Sequelize.Op.or]: [null, false]
|
||||
}
|
||||
} else if (value === 'not-started') {
|
||||
mediaWhere[Sequelize.Op.and] = [
|
||||
{
|
||||
'$books.mediaProgresses.currentTime$': {
|
||||
[Sequelize.Op.or]: [null, 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
'$books.mediaProgresses.isFinished$': {
|
||||
[Sequelize.Op.or]: [null, false]
|
||||
}
|
||||
}
|
||||
]
|
||||
} else if (value === 'finished') {
|
||||
mediaWhere['$books.mediaProgresses.isFinished$'] = true
|
||||
} else if (value === 'in-progress') {
|
||||
mediaWhere[Sequelize.Op.and] = [
|
||||
{
|
||||
[Sequelize.Op.or]: [
|
||||
{
|
||||
'$books.mediaProgresses.currentTime$': {
|
||||
[Sequelize.Op.gt]: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
'$books.mediaProgresses.ebookProgress$': {
|
||||
[Sequelize.Op.gt]: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'$books.mediaProgresses.isFinished$': false
|
||||
}
|
||||
]
|
||||
}
|
||||
return mediaWhere
|
||||
},
|
||||
|
||||
/**
|
||||
* Get where options for Book model
|
||||
* @param {string} group
|
||||
@ -104,9 +149,10 @@ module.exports = {
|
||||
* Get sequelize order
|
||||
* @param {string} sortBy
|
||||
* @param {boolean} sortDesc
|
||||
* @param {boolean} collapseseries
|
||||
* @returns {Sequelize.order}
|
||||
*/
|
||||
getOrder(sortBy, sortDesc) {
|
||||
getOrder(sortBy, sortDesc, collapseseries) {
|
||||
const dir = sortDesc ? 'DESC' : 'ASC'
|
||||
if (sortBy === 'addedAt') {
|
||||
return [[Sequelize.literal('libraryItem.createdAt'), dir]]
|
||||
@ -125,15 +171,69 @@ module.exports = {
|
||||
} else if (sortBy === 'media.metadata.authorName') {
|
||||
return [['author_name', dir]]
|
||||
} else if (sortBy === 'media.metadata.title') {
|
||||
if (collapseseries) {
|
||||
return [[Sequelize.literal('display_title COLLATE NOCASE'), dir]]
|
||||
}
|
||||
|
||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||
return [['titleIgnorePrefix', dir]]
|
||||
} else {
|
||||
return [['title', dir]]
|
||||
}
|
||||
} else if (sortBy === 'sequence') {
|
||||
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
|
||||
return [[Sequelize.literal(`\`series.bookSeries.sequence\` COLLATE NOCASE ${nullDir}`)]]
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
async getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere) {
|
||||
const allSeries = await Database.models.series.findAll({
|
||||
attributes: [
|
||||
'id',
|
||||
'name',
|
||||
[Sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 'numBooks']
|
||||
],
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
where: seriesWhere,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.book,
|
||||
attributes: ['id', 'title'],
|
||||
through: {
|
||||
attributes: ['id', 'seriesId', 'bookId', 'sequence']
|
||||
},
|
||||
...bookFindOptions,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
order: [
|
||||
Sequelize.literal('`books.bookSeries.sequence` COLLATE NOCASE ASC NULLS LAST')
|
||||
]
|
||||
})
|
||||
const bookSeriesToInclude = []
|
||||
const booksToInclude = []
|
||||
let booksToExclude = []
|
||||
allSeries.forEach(s => {
|
||||
let found = false
|
||||
for (let book of s.books) {
|
||||
if (!found && !booksToInclude.includes(book.id)) {
|
||||
booksToInclude.push(book.id)
|
||||
bookSeriesToInclude.push({
|
||||
id: book.bookSeries.id,
|
||||
numBooks: s.dataValues.numBooks
|
||||
})
|
||||
booksToExclude = booksToExclude.filter(bid => bid !== book.id)
|
||||
found = true
|
||||
} else if (!booksToExclude.includes(book.id) && !booksToInclude.includes(book.id)) {
|
||||
booksToExclude.push(book.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
return { booksToExclude, bookSeriesToInclude }
|
||||
},
|
||||
|
||||
/**
|
||||
* Get library items for book media type using filter and sort
|
||||
* @param {string} libraryId
|
||||
@ -141,11 +241,22 @@ module.exports = {
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
* @param {string} sortDesc
|
||||
* @param {boolean} collapseseries
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
async getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset) {
|
||||
async getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset) {
|
||||
// TODO: Handle collapse sub-series
|
||||
if (filterGroup === 'series' && collapseseries) {
|
||||
collapseseries = false
|
||||
}
|
||||
if (filterGroup !== 'series' && sortBy === 'sequence') {
|
||||
sortBy = 'media.metadata.title'
|
||||
}
|
||||
const includeRSSFeed = include.includes('rssfeed')
|
||||
|
||||
// For sorting by author name an additional attribute must be added
|
||||
// with author names concatenated
|
||||
let bookAttributes = null
|
||||
@ -169,10 +280,10 @@ module.exports = {
|
||||
|
||||
let seriesInclude = {
|
||||
model: Database.models.bookSeries,
|
||||
attributes: ['seriesId', 'sequence', 'createdAt'],
|
||||
attributes: ['id', 'seriesId', 'sequence', 'createdAt'],
|
||||
include: {
|
||||
model: Database.models.series,
|
||||
attributes: ['id', 'name']
|
||||
attributes: ['id', 'name', 'nameIgnorePrefix']
|
||||
},
|
||||
order: [
|
||||
['createdAt', 'ASC']
|
||||
@ -193,9 +304,17 @@ module.exports = {
|
||||
separate: true
|
||||
}
|
||||
|
||||
const sortOrder = this.getOrder(sortBy, sortDesc, collapseseries)
|
||||
|
||||
const libraryItemIncludes = []
|
||||
const bookIncludes = []
|
||||
if (filterGroup === 'feed-open') {
|
||||
if (includeRSSFeed) {
|
||||
libraryItemIncludes.push({
|
||||
model: Database.models.feed,
|
||||
required: filterGroup === 'feed-open'
|
||||
})
|
||||
}
|
||||
if (filterGroup === 'feed-open' && !includeRSSFeed) {
|
||||
libraryItemIncludes.push({
|
||||
model: Database.models.feed,
|
||||
required: true
|
||||
@ -243,6 +362,10 @@ module.exports = {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
})
|
||||
if (sortBy !== 'sequence') {
|
||||
// Secondary sort by sequence
|
||||
sortOrder.push([Sequelize.literal('`series.bookSeries.sequence` COLLATE NOCASE ASC NULLS LAST')])
|
||||
}
|
||||
} else if (filterGroup === 'issues') {
|
||||
libraryItemWhere[Sequelize.Op.or] = [
|
||||
{
|
||||
@ -263,8 +386,52 @@ module.exports = {
|
||||
})
|
||||
}
|
||||
|
||||
const bookWhere = filterGroup ? this.getMediaGroupQuery(filterGroup, filterValue) : {}
|
||||
|
||||
let collapseSeriesBookSeries = []
|
||||
if (collapseseries) {
|
||||
let seriesBookWhere = null
|
||||
let seriesWhere = null
|
||||
if (filterGroup === 'progress') {
|
||||
seriesWhere = this.getCollapseSeriesMediaProgressFilter(filterValue)
|
||||
} else if (filterGroup === 'missing' && filterValue === 'authors') {
|
||||
seriesWhere = {
|
||||
['$books.authors.id$']: null
|
||||
}
|
||||
} else {
|
||||
seriesBookWhere = bookWhere
|
||||
}
|
||||
|
||||
const bookFindOptions = {
|
||||
where: seriesBookWhere,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.libraryItem,
|
||||
required: true,
|
||||
where: libraryItemWhere,
|
||||
include: libraryItemIncludes
|
||||
},
|
||||
authorInclude,
|
||||
...bookIncludes
|
||||
]
|
||||
}
|
||||
const { booksToExclude, bookSeriesToInclude } = await this.getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere)
|
||||
if (booksToExclude.length) {
|
||||
bookWhere['id'] = {
|
||||
[Sequelize.Op.notIn]: booksToExclude
|
||||
}
|
||||
}
|
||||
collapseSeriesBookSeries = bookSeriesToInclude
|
||||
if (!bookAttributes?.include) bookAttributes = { include: [] }
|
||||
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'])
|
||||
} 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(', ')})), title)`), 'display_title'])
|
||||
}
|
||||
}
|
||||
|
||||
const { rows: books, count } = await Database.models.book.findAndCountAll({
|
||||
where: filterGroup ? this.getMediaGroupQuery(filterGroup, filterValue) : null,
|
||||
where: bookWhere,
|
||||
distinct: true,
|
||||
attributes: bookAttributes,
|
||||
include: [
|
||||
@ -278,7 +445,7 @@ module.exports = {
|
||||
authorInclude,
|
||||
...bookIncludes
|
||||
],
|
||||
order: this.getOrder(sortBy, sortDesc),
|
||||
order: sortOrder,
|
||||
subQuery: false,
|
||||
limit,
|
||||
offset
|
||||
@ -287,9 +454,39 @@ module.exports = {
|
||||
const libraryItems = books.map((bookExpanded) => {
|
||||
const libraryItem = bookExpanded.libraryItem.toJSON()
|
||||
const book = bookExpanded.toJSON()
|
||||
|
||||
if (filterGroup === 'series' && book.series?.length) {
|
||||
// For showing sequence on book cover when filtering for series
|
||||
libraryItem.series = {
|
||||
id: book.series[0].id,
|
||||
name: book.series[0].name,
|
||||
sequence: book.series[0].bookSeries?.sequence || null
|
||||
}
|
||||
}
|
||||
|
||||
delete book.libraryItem
|
||||
delete book.authors
|
||||
delete book.series
|
||||
|
||||
// For showing details of collapsed series
|
||||
if (collapseseries && book.bookSeries?.length) {
|
||||
const collapsedSeries = book.bookSeries.find(bs => collapseSeriesBookSeries.some(cbs => cbs.id === bs.id))
|
||||
if (collapsedSeries) {
|
||||
const collapseSeriesObj = collapseSeriesBookSeries.find(csbs => csbs.id === collapsedSeries.id)
|
||||
libraryItem.collapsedSeries = {
|
||||
id: collapsedSeries.series.id,
|
||||
name: collapsedSeries.series.name,
|
||||
nameIgnorePrefix: collapsedSeries.series.nameIgnorePrefix,
|
||||
sequence: collapsedSeries.sequence,
|
||||
numBooks: collapseSeriesObj?.numBooks || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (libraryItem.feeds?.length) {
|
||||
libraryItem.rssFeed = libraryItem.feeds[0]
|
||||
}
|
||||
|
||||
libraryItem.media = book
|
||||
|
||||
return libraryItem
|
||||
|
Loading…
Reference in New Issue
Block a user