mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 16:44:16 +01:00
Update get library series api endpoint to load from db
This commit is contained in:
parent
9d7d4c6902
commit
4e4a976050
@ -317,6 +317,8 @@ export default {
|
||||
// TODO: Temp use new library items API for everything except collapse sub-series
|
||||
if (entityPath === 'items' && !this.collapseBookSeries && !(this.filterName === 'Series' && this.collapseSeries)) {
|
||||
entityPath += '2'
|
||||
} else if (entityPath === 'series') {
|
||||
entityPath += '2'
|
||||
}
|
||||
|
||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
@ -628,6 +630,11 @@ export default {
|
||||
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
|
||||
},
|
||||
async init(bookshelf) {
|
||||
if (this.entityName === 'series') {
|
||||
this.booksPerFetch = 50
|
||||
} else {
|
||||
this.booksPerFetch = 100
|
||||
}
|
||||
this.checkUpdateSearchParams()
|
||||
this.initSizeData(bookshelf)
|
||||
|
||||
|
@ -8,6 +8,7 @@ const Library = require('../objects/Library')
|
||||
const libraryHelpers = require('../utils/libraryHelpers')
|
||||
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
||||
const seriesFilters = require('../utils/queries/seriesFilters')
|
||||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
@ -519,12 +520,42 @@ class LibraryController {
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/libraries/:id/series2
|
||||
* Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async getAllSeriesForLibraryNew(req, res) {
|
||||
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||
|
||||
const payload = {
|
||||
results: [],
|
||||
total: 0,
|
||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
|
||||
sortBy: req.query.sort,
|
||||
sortDesc: req.query.desc === '1',
|
||||
filterBy: req.query.filter,
|
||||
minified: req.query.minified === '1',
|
||||
include: include.join(',')
|
||||
}
|
||||
|
||||
const offset = payload.page * payload.limit
|
||||
const { series, count } = await seriesFilters.getFilteredSeries(req.library, req.user, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset)
|
||||
|
||||
payload.total = count
|
||||
payload.results = series
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/libraries/:id/series
|
||||
* Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async getAllSeriesForLibrary(req, res) {
|
||||
const libraryItems = req.libraryItems
|
||||
|
@ -100,7 +100,24 @@ class Series extends Model {
|
||||
description: DataTypes.TEXT
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'series'
|
||||
modelName: 'series',
|
||||
indexes: [
|
||||
{
|
||||
fields: [{
|
||||
name: 'name',
|
||||
collate: 'NOCASE'
|
||||
}]
|
||||
},
|
||||
{
|
||||
fields: [{
|
||||
name: 'nameIgnorePrefix',
|
||||
collate: 'NOCASE'
|
||||
}]
|
||||
},
|
||||
{
|
||||
fields: ['libraryId']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const { library } = sequelize.models
|
||||
|
@ -1,5 +1,5 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { getTitleIgnorePrefix } = require('../../utils/index')
|
||||
const { getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
|
||||
|
||||
class Series {
|
||||
constructor(series) {
|
||||
@ -33,6 +33,7 @@ class Series {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
|
||||
description: this.description,
|
||||
addedAt: this.addedAt,
|
||||
updatedAt: this.updatedAt,
|
||||
|
@ -78,6 +78,7 @@ class ApiRouter {
|
||||
this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this))
|
||||
this.router.delete('/libraries/:id/issues', LibraryController.middlewareNew.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
|
||||
this.router.get('/libraries/:id/episode-downloads', LibraryController.middlewareNew.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this))
|
||||
this.router.get('/libraries/:id/series2', LibraryController.middlewareNew.bind(this), LibraryController.getAllSeriesForLibraryNew.bind(this))
|
||||
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
||||
this.router.get('/libraries/:id/series/:seriesId', LibraryController.middlewareNew.bind(this), LibraryController.getSeriesForLibrary.bind(this))
|
||||
this.router.get('/libraries/:id/collections', LibraryController.middlewareNew.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
||||
|
@ -41,11 +41,11 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get library items for continue listening & continue reading shelves
|
||||
* @param {oldLibrary} library
|
||||
* @param {oldUser} user
|
||||
* @param {import('../../objects/Library')} library
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object} { items:LibraryItem[], count:number }
|
||||
* @returns {Promise<{ items:import('../../models/LibraryItem')[], count:number }>}
|
||||
*/
|
||||
async getMediaItemsInProgress(library, user, include, limit) {
|
||||
if (library.mediaType === 'book') {
|
||||
@ -176,14 +176,14 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get series for recent series shelf
|
||||
* @param {oldLibrary} library
|
||||
* @param {oldUser} user
|
||||
* @param {import('../../objects/Library')} library
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object} { series:oldSeries[], count:number}
|
||||
* @returns {{ series:import('../../objects/entities/Series')[], count:number}}
|
||||
*/
|
||||
async getSeriesMostRecentlyAdded(library, user, include, limit) {
|
||||
if (library.mediaType !== 'book') return { series: [], count: 0 }
|
||||
if (!library.isBook) return { series: [], count: 0 }
|
||||
|
||||
const seriesIncludes = []
|
||||
if (include.includes('rssfeed')) {
|
||||
@ -390,7 +390,7 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get filter data used in filter menus
|
||||
* @param {oldLibrary} oldLibrary
|
||||
* @param {import('../../objects/Library')} oldLibrary
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
async getFilterData(oldLibrary) {
|
||||
|
@ -6,7 +6,7 @@ module.exports = {
|
||||
/**
|
||||
* User permissions to restrict books for explicit content & tags
|
||||
* @param {oldUser} user
|
||||
* @returns {object} { bookWhere:Sequelize.WhereOptions, replacements:string[] }
|
||||
* @returns {{ bookWhere:Sequelize.WhereOptions, replacements:object }}
|
||||
*/
|
||||
getUserPermissionBookWhereQuery(user) {
|
||||
const bookWhere = []
|
||||
|
206
server/utils/queries/seriesFilters.js
Normal file
206
server/utils/queries/seriesFilters.js
Normal file
@ -0,0 +1,206 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const Logger = require('../../Logger')
|
||||
const Database = require('../../Database')
|
||||
const libraryItemsBookFilters = require('./libraryItemsBookFilters')
|
||||
|
||||
module.exports = {
|
||||
decode(text) {
|
||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||
},
|
||||
|
||||
/**
|
||||
* Get series filtered and sorted
|
||||
*
|
||||
* @param {import('../../objects/Library')} library
|
||||
* @param {import('../../objects/user/User')} user
|
||||
* @param {string} filterBy
|
||||
* @param {string} sortBy
|
||||
* @param {boolean} sortDesc
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {Promise<{ series:object[], count:number }>}
|
||||
*/
|
||||
async getFilteredSeries(library, user, filterBy, sortBy, sortDesc, include, limit, offset) {
|
||||
let filterValue = null
|
||||
let filterGroup = null
|
||||
if (filterBy) {
|
||||
const searchGroups = ['genres', 'tags', 'authors', 'progress', 'narrators', 'publishers', 'languages']
|
||||
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
filterGroup = group || filterBy
|
||||
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
|
||||
}
|
||||
|
||||
const seriesIncludes = []
|
||||
if (include.includes('rssfeed')) {
|
||||
seriesIncludes.push({
|
||||
model: Database.models.feed
|
||||
})
|
||||
}
|
||||
|
||||
const userPermissionBookWhere = libraryItemsBookFilters.getUserPermissionBookWhereQuery(user)
|
||||
|
||||
const seriesWhere = [
|
||||
{
|
||||
libraryId: library.id
|
||||
}
|
||||
]
|
||||
|
||||
// Handle library setting to hide single book series
|
||||
// TODO: Merge with existing query
|
||||
if (library.settings.hideSingleBookSeries) {
|
||||
seriesWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id)`), {
|
||||
[Sequelize.Op.gt]: 1
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle filters
|
||||
// TODO: Simplify and break-out
|
||||
let attrQuery = null
|
||||
if (['genres', 'tags', 'narrators'].includes(filterGroup)) {
|
||||
attrQuery = `SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (SELECT count(*) FROM json_each(b.${filterGroup}) WHERE json_valid(b.${filterGroup}) AND json_each.value = :filterValue) > 0`
|
||||
userPermissionBookWhere.replacements.filterValue = filterValue
|
||||
} else if (filterGroup === 'authors') {
|
||||
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs, bookAuthors ba WHERE bs.seriesId = series.id AND bs.bookId = b.id AND ba.bookId = b.id AND ba.authorId = :filterValue'
|
||||
userPermissionBookWhere.replacements.filterValue = filterValue
|
||||
} else if (filterGroup === 'publishers') {
|
||||
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id AND b.publisher = :filterValue'
|
||||
userPermissionBookWhere.replacements.filterValue = filterValue
|
||||
} else if (filterGroup === 'languages') {
|
||||
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id AND b.language = :filterValue'
|
||||
userPermissionBookWhere.replacements.filterValue = filterValue
|
||||
} else if (filterGroup === 'progress') {
|
||||
if (filterValue === 'not-finished') {
|
||||
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished IS NULL OR mp.isFinished = 0)'
|
||||
} else if (filterValue === 'finished') {
|
||||
const progQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished IS NULL OR mp.isFinished = 0)'
|
||||
seriesWhere.push(Sequelize.where(Sequelize.literal(`(${progQuery})`), 0))
|
||||
} else if (filterValue === 'not-started') {
|
||||
const progQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.isFinished = 1 OR mp.currentTime > 0)'
|
||||
seriesWhere.push(Sequelize.where(Sequelize.literal(`(${progQuery})`), 0))
|
||||
} else if (filterValue === 'in-progress') {
|
||||
attrQuery = 'SELECT count(*) FROM books b, bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = b.id WHERE bs.seriesId = series.id AND bs.bookId = b.id AND (mp.currentTime > 0 OR mp.ebookProgress > 0) AND mp.isFinished = 0'
|
||||
}
|
||||
}
|
||||
|
||||
// Handle user permissions to only include series with at least 1 book
|
||||
// TODO: Simplify to a single query
|
||||
if (userPermissionBookWhere.bookWhere.length) {
|
||||
if (!attrQuery) attrQuery = 'SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id'
|
||||
|
||||
if (!user.canAccessExplicitContent) {
|
||||
attrQuery += ' AND b.explicit = 0'
|
||||
}
|
||||
if (!user.permissions.accessAllTags && user.itemTagsSelected.length) {
|
||||
if (user.permissions.selectedTagsNotAccessible) {
|
||||
attrQuery += ' AND (SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected)) = 0'
|
||||
} else {
|
||||
attrQuery += ' AND (SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected)) > 0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attrQuery) {
|
||||
seriesWhere.push(Sequelize.where(Sequelize.literal(`(${attrQuery})`), {
|
||||
[Sequelize.Op.gt]: 0
|
||||
}))
|
||||
}
|
||||
|
||||
const order = []
|
||||
let seriesAttributes = {
|
||||
include: []
|
||||
}
|
||||
|
||||
// Handle sort order
|
||||
const dir = sortDesc ? 'DESC' : 'ASC'
|
||||
if (sortBy === 'numBooks') {
|
||||
seriesAttributes.include.push([Sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 'numBooks'])
|
||||
order.push(['numBooks', dir])
|
||||
} else if (sortBy === 'addedAt') {
|
||||
order.push(['createdAt', dir])
|
||||
} else if (sortBy === 'name') {
|
||||
if (global.ServerSettings.sortingIgnorePrefix) {
|
||||
order.push([Sequelize.literal('nameIgnorePrefix COLLATE NOCASE'), dir])
|
||||
} else {
|
||||
order.push([Sequelize.literal('`series`.`name` COLLATE NOCASE'), dir])
|
||||
}
|
||||
} else if (sortBy === 'totalDuration') {
|
||||
seriesAttributes.include.push([Sequelize.literal('(SELECT SUM(b.duration) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND b.id = bs.bookId)'), 'totalDuration'])
|
||||
order.push(['totalDuration', dir])
|
||||
} else if (sortBy === 'lastBookAdded') {
|
||||
seriesAttributes.include.push([Sequelize.literal('(SELECT MAX(b.createdAt) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND b.id = bs.bookId)'), 'mostRecentBookAdded'])
|
||||
order.push(['mostRecentBookAdded', dir])
|
||||
} else if (sortBy === 'lastBookUpdated') {
|
||||
seriesAttributes.include.push([Sequelize.literal('(SELECT MAX(b.updatedAt) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND b.id = bs.bookId)'), 'mostRecentBookUpdated'])
|
||||
order.push(['mostRecentBookUpdated', dir])
|
||||
}
|
||||
|
||||
const { rows: series, count } = await Database.seriesModel.findAndCountAll({
|
||||
where: seriesWhere,
|
||||
limit,
|
||||
offset,
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
benchmark: true,
|
||||
logging: (sql, timeMs) => {
|
||||
console.log(`[Query] Series filter/sort. Elapsed ${timeMs}ms`)
|
||||
},
|
||||
attributes: seriesAttributes,
|
||||
replacements: userPermissionBookWhere.replacements,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.bookSeries,
|
||||
include: {
|
||||
model: Database.models.book,
|
||||
where: userPermissionBookWhere.bookWhere,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.libraryItem
|
||||
}
|
||||
]
|
||||
},
|
||||
separate: true
|
||||
},
|
||||
...seriesIncludes
|
||||
],
|
||||
order
|
||||
})
|
||||
|
||||
// Map series to old series
|
||||
const allOldSeries = []
|
||||
for (const s of series) {
|
||||
const oldSeries = s.getOldSeries().toJSON()
|
||||
|
||||
if (s.dataValues.totalDuration) {
|
||||
oldSeries.totalDuration = s.dataValues.totalDuration
|
||||
}
|
||||
|
||||
if (s.feeds?.length) {
|
||||
oldSeries.rssFeed = Database.models.feed.getOldFeed(s.feeds[0]).toJSONMinified()
|
||||
}
|
||||
|
||||
// TODO: Sort books by sequence in query
|
||||
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'
|
||||
})
|
||||
})
|
||||
oldSeries.books = s.bookSeries.map(bs => {
|
||||
const libraryItem = bs.book.libraryItem.toJSON()
|
||||
delete bs.book.libraryItem
|
||||
libraryItem.media = bs.book
|
||||
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(libraryItem).toJSONMinified()
|
||||
return oldLibraryItem
|
||||
})
|
||||
allOldSeries.push(oldSeries)
|
||||
}
|
||||
|
||||
return {
|
||||
series: allOldSeries,
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user