mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 18:01:42 +00:00
feat: add large-library browse response contract
This commit is contained in:
parent
6b7550c25b
commit
3579341f6c
4 changed files with 146 additions and 6 deletions
|
|
@ -612,6 +612,12 @@ class LibraryController {
|
||||||
total: undefined,
|
total: undefined,
|
||||||
limit: req.query.limit || 0,
|
limit: req.query.limit || 0,
|
||||||
page: req.query.page || 0,
|
page: req.query.page || 0,
|
||||||
|
cursor: req.query.cursor || null,
|
||||||
|
pageMode: req.query.pageMode || 'paged',
|
||||||
|
nextCursor: null,
|
||||||
|
paginationMode: 'offset',
|
||||||
|
isCountDeferred: false,
|
||||||
|
countMode: 'exact-on-initial-page',
|
||||||
sortBy: req.query.sort,
|
sortBy: req.query.sort,
|
||||||
sortDesc: req.query.desc === '1',
|
sortDesc: req.query.desc === '1',
|
||||||
filterBy: req.query.filter,
|
filterBy: req.query.filter,
|
||||||
|
|
@ -630,9 +636,13 @@ class LibraryController {
|
||||||
const seriesId = libraryFilters.decode(payload.filterBy.split('.')[1])
|
const seriesId = libraryFilters.decode(payload.filterBy.split('.')[1])
|
||||||
payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.user, req.library)
|
payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.user, req.library)
|
||||||
} else {
|
} else {
|
||||||
const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.user, payload)
|
const { libraryItems, count, nextCursor, paginationMode, countMode, isCountDeferred } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.user, payload)
|
||||||
payload.results = libraryItems
|
payload.results = libraryItems
|
||||||
payload.total = count
|
payload.total = count
|
||||||
|
payload.nextCursor = nextCursor || null
|
||||||
|
payload.paginationMode = paginationMode || 'offset'
|
||||||
|
payload.countMode = countMode || 'exact-on-initial-page'
|
||||||
|
payload.isCountDeferred = !!isCountDeferred
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(payload)
|
res.json(payload)
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,11 @@ class LibraryItem extends Model {
|
||||||
*/
|
*/
|
||||||
static async getByFilterAndSort(library, user, options) {
|
static async getByFilterAndSort(library, user, options) {
|
||||||
let start = Date.now()
|
let start = Date.now()
|
||||||
const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(library.id, user, options)
|
const { libraryItems, count, nextCursor, paginationMode, countMode, isCountDeferred } = await libraryFilters.getFilteredLibraryItems(library.id, user, {
|
||||||
|
...options,
|
||||||
|
cursor: options.cursor || null,
|
||||||
|
pageMode: options.pageMode || 'paged'
|
||||||
|
})
|
||||||
Logger.debug(`Loaded ${libraryItems.length} of ${count} items for libary page in ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
Logger.debug(`Loaded ${libraryItems.length} of ${count} items for libary page in ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -323,7 +327,11 @@ class LibraryItem extends Model {
|
||||||
|
|
||||||
return oldLibraryItem
|
return oldLibraryItem
|
||||||
}),
|
}),
|
||||||
count
|
count,
|
||||||
|
nextCursor,
|
||||||
|
paginationMode,
|
||||||
|
countMode,
|
||||||
|
isCountDeferred
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ module.exports = {
|
||||||
* @returns {Promise<{ libraryItems:import('../../models/LibraryItem')[], count:number }>}
|
* @returns {Promise<{ libraryItems:import('../../models/LibraryItem')[], count:number }>}
|
||||||
*/
|
*/
|
||||||
async getFilteredLibraryItems(libraryId, user, options) {
|
async getFilteredLibraryItems(libraryId, user, options) {
|
||||||
const { filterBy, sortBy, sortDesc, limit, offset, collapseseries, include, mediaType } = options
|
const { filterBy, sortBy, sortDesc, limit, offset, collapseseries, include, mediaType, cursor, pageMode } = options
|
||||||
|
|
||||||
let filterValue = null
|
let filterValue = null
|
||||||
let filterGroup = null
|
let filterGroup = null
|
||||||
|
|
@ -34,9 +34,17 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaType === 'book') {
|
if (mediaType === 'book') {
|
||||||
return libraryItemsBookFilters.getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset)
|
return libraryItemsBookFilters.getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset, false, {
|
||||||
|
cursor: cursor || null,
|
||||||
|
pageMode: pageMode || 'paged',
|
||||||
|
collapseseries
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
return libraryItemsPodcastFilters.getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset)
|
return libraryItemsPodcastFilters.getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset, {
|
||||||
|
cursor: cursor || null,
|
||||||
|
pageMode: pageMode || 'paged',
|
||||||
|
collapseseries
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
const { expect } = require('chai')
|
||||||
|
const { Sequelize } = require('sequelize')
|
||||||
|
const sinon = require('sinon')
|
||||||
|
|
||||||
|
const Database = require('../../../server/Database')
|
||||||
|
const LibraryController = require('../../../server/controllers/LibraryController')
|
||||||
|
const libraryFilters = require('../../../server/utils/queries/libraryFilters')
|
||||||
|
|
||||||
|
describe('LibraryController large-library browse contract', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
global.ServerSettings = {}
|
||||||
|
Database.sequelize = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
||||||
|
Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '')
|
||||||
|
await Database.buildModels()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
sinon.restore()
|
||||||
|
await Database.sequelize.sync({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns pagination metadata with deferred count semantics for title browse', async () => {
|
||||||
|
const getByFilterAndSortStub = sinon.stub(Database.libraryItemModel, 'getByFilterAndSort').resolves({
|
||||||
|
libraryItems: [{ id: 'item_1' }],
|
||||||
|
count: 123,
|
||||||
|
nextCursor: 'cursor-2',
|
||||||
|
paginationMode: 'keyset',
|
||||||
|
countMode: 'deferred-exact',
|
||||||
|
isCountDeferred: true
|
||||||
|
})
|
||||||
|
const req = {
|
||||||
|
query: { limit: '40', sort: 'media.metadata.title', minified: '1', cursor: 'cursor-1', pageMode: 'endless' },
|
||||||
|
library: { id: 'lib_1', mediaType: 'book', isVirtual: false },
|
||||||
|
user: { id: 'user_1' }
|
||||||
|
}
|
||||||
|
const res = { json: sinon.spy() }
|
||||||
|
|
||||||
|
await LibraryController.getLibraryItems(req, res)
|
||||||
|
|
||||||
|
expect(getByFilterAndSortStub.firstCall.args[2]).to.include({
|
||||||
|
cursor: 'cursor-1',
|
||||||
|
pageMode: 'endless'
|
||||||
|
})
|
||||||
|
expect(res.json.firstCall.args[0]).to.include({
|
||||||
|
total: 123,
|
||||||
|
cursor: 'cursor-1',
|
||||||
|
pageMode: 'endless',
|
||||||
|
nextCursor: 'cursor-2',
|
||||||
|
paginationMode: 'keyset',
|
||||||
|
isCountDeferred: true,
|
||||||
|
countMode: 'deferred-exact'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults pageMode to paged even when a cursor is present', async () => {
|
||||||
|
const getByFilterAndSortStub = sinon.stub(Database.libraryItemModel, 'getByFilterAndSort').resolves({
|
||||||
|
libraryItems: [],
|
||||||
|
count: 0,
|
||||||
|
nextCursor: null,
|
||||||
|
paginationMode: 'offset',
|
||||||
|
countMode: 'exact-on-initial-page',
|
||||||
|
isCountDeferred: false
|
||||||
|
})
|
||||||
|
const req = {
|
||||||
|
query: { limit: '40', sort: 'media.metadata.title', cursor: 'cursor-1' },
|
||||||
|
library: { id: 'lib_1', mediaType: 'book', isVirtual: false },
|
||||||
|
user: { id: 'user_1' }
|
||||||
|
}
|
||||||
|
const res = { json: sinon.spy() }
|
||||||
|
|
||||||
|
await LibraryController.getLibraryItems(req, res)
|
||||||
|
|
||||||
|
expect(getByFilterAndSortStub.firstCall.args[2]).to.include({
|
||||||
|
cursor: 'cursor-1',
|
||||||
|
pageMode: 'paged'
|
||||||
|
})
|
||||||
|
expect(res.json.firstCall.args[0]).to.include({
|
||||||
|
cursor: 'cursor-1',
|
||||||
|
pageMode: 'paged'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forwards cursor, pageMode, and collapseseries through the real browse stack', async () => {
|
||||||
|
const getFilteredLibraryItemsStub = sinon.stub(libraryFilters, 'getFilteredLibraryItems').resolves({
|
||||||
|
libraryItems: [],
|
||||||
|
count: 0,
|
||||||
|
nextCursor: 'cursor-2',
|
||||||
|
paginationMode: 'keyset',
|
||||||
|
countMode: 'deferred-exact',
|
||||||
|
isCountDeferred: true
|
||||||
|
})
|
||||||
|
|
||||||
|
await Database.libraryItemModel.getByFilterAndSort(
|
||||||
|
{ id: 'lib_1', mediaType: 'book' },
|
||||||
|
{ id: 'user_1' },
|
||||||
|
{
|
||||||
|
sortBy: 'media.metadata.title',
|
||||||
|
limit: 40,
|
||||||
|
offset: 0,
|
||||||
|
cursor: 'cursor-1',
|
||||||
|
pageMode: 'endless',
|
||||||
|
include: [],
|
||||||
|
collapseseries: true,
|
||||||
|
mediaType: 'book'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(getFilteredLibraryItemsStub.firstCall.args[2]).to.include({
|
||||||
|
cursor: 'cursor-1',
|
||||||
|
pageMode: 'endless',
|
||||||
|
collapseseries: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue