fix: align large-library browse strategy keys

This commit is contained in:
Jonathan Finley 2026-03-13 21:14:59 -04:00
parent 1533d4146a
commit 8585cfff6b
2 changed files with 113 additions and 16 deletions

View file

@ -1,25 +1,45 @@
function getCursorKeys(sortBy) { function getTitleCursorKey() {
return global.ServerSettings && global.ServerSettings.sortingIgnorePrefix ? 'titleIgnorePrefix' : 'title'
}
function getKeysetCursorKeys(sortBy) {
if (sortBy === 'media.metadata.title') {
return [getTitleCursorKey(), 'id']
}
if (sortBy === 'media.metadata.authorName') { if (sortBy === 'media.metadata.authorName') {
return ['authorNamesFirstLast', 'titleIgnorePrefix', 'id'] return ['authorNamesFirstLast', getTitleCursorKey(), 'id']
} }
if (sortBy === 'media.metadata.authorNameLF') { if (sortBy === 'media.metadata.authorNameLF') {
return ['authorNamesLastFirst', 'titleIgnorePrefix', 'id'] return ['authorNamesLastFirst', getTitleCursorKey(), 'id']
}
if (sortBy === 'progress') {
return ['mediaProgress.updatedAt', 'id']
}
if (sortBy === 'updatedAt') {
return ['updatedAt', 'id']
} }
if (sortBy === 'addedAt') { if (sortBy === 'addedAt') {
return ['createdAt', 'id'] return ['createdAt', 'id']
} }
return ['titleIgnorePrefix', 'id'] if (sortBy === 'progress') {
return ['mediaProgresses.updatedAt', 'id']
}
if (sortBy === 'progress.createdAt') {
return ['mediaProgresses.createdAt', 'id']
}
if (sortBy === 'progress.finishedAt') {
return ['mediaProgresses.finishedAt', 'id']
}
return []
}
function getPaginationMode(pageMode, sortBy) {
if (pageMode !== 'endless') {
return 'offset'
}
return getKeysetCursorKeys(sortBy).length ? 'keyset' : 'offset'
} }
function getFamily({ mediaType, sortBy, filterGroup, collapseseries }) { function getFamily({ mediaType, sortBy, filterGroup, collapseseries }) {
@ -50,11 +70,11 @@ function getLibraryBrowseStrategy({ mediaType, sortBy, filterGroup, pageMode, co
return { return {
family: getFamily({ mediaType, sortBy: normalizedSort, filterGroup, collapseseries }), family: getFamily({ mediaType, sortBy: normalizedSort, filterGroup, collapseseries }),
paginationMode: pageMode === 'endless' ? 'keyset' : 'offset', paginationMode: getPaginationMode(pageMode, normalizedSort),
countMode: 'deferred-exact', countMode: 'deferred-exact',
deepScrollAllowed: true, deepScrollAllowed: true,
tieBreaker: 'id', tieBreaker: 'id',
cursorKeys: getCursorKeys(normalizedSort) cursorKeys: getKeysetCursorKeys(normalizedSort)
} }
} }

View file

@ -4,7 +4,13 @@ const expect = chai.expect
const { getLibraryBrowseStrategy } = require('../../../server/utils/queries/libraryBrowseStrategy') const { getLibraryBrowseStrategy } = require('../../../server/utils/queries/libraryBrowseStrategy')
describe('libraryBrowseStrategy', () => { describe('libraryBrowseStrategy', () => {
afterEach(() => {
delete global.ServerSettings
})
it('uses keyset pagination for title browse', () => { it('uses keyset pagination for title browse', () => {
global.ServerSettings = { sortingIgnorePrefix: true }
const strategy = getLibraryBrowseStrategy({ const strategy = getLibraryBrowseStrategy({
mediaType: 'book', mediaType: 'book',
sortBy: 'media.metadata.title', sortBy: 'media.metadata.title',
@ -19,6 +25,20 @@ describe('libraryBrowseStrategy', () => {
expect(strategy.cursorKeys).to.deep.equal(['titleIgnorePrefix', 'id']) expect(strategy.cursorKeys).to.deep.equal(['titleIgnorePrefix', 'id'])
}) })
it('uses raw title cursor keys when prefix ignoring is disabled', () => {
global.ServerSettings = { sortingIgnorePrefix: false }
const strategy = getLibraryBrowseStrategy({
mediaType: 'book',
sortBy: 'media.metadata.title',
filterGroup: null,
pageMode: 'endless'
})
expect(strategy.paginationMode).to.equal('keyset')
expect(strategy.cursorKeys).to.deep.equal(['title', 'id'])
})
it('falls back to offset for random sort', () => { it('falls back to offset for random sort', () => {
const strategy = getLibraryBrowseStrategy({ const strategy = getLibraryBrowseStrategy({
mediaType: 'book', mediaType: 'book',
@ -41,6 +61,7 @@ describe('libraryBrowseStrategy', () => {
expect(strategy.family).to.equal('recent-browse') expect(strategy.family).to.equal('recent-browse')
expect(strategy.paginationMode).to.equal('keyset') expect(strategy.paginationMode).to.equal('keyset')
expect(strategy.cursorKeys).to.deep.equal(['createdAt', 'id'])
}) })
it('uses keyset pagination for progress browse', () => { it('uses keyset pagination for progress browse', () => {
@ -53,6 +74,7 @@ describe('libraryBrowseStrategy', () => {
expect(strategy.family).to.equal('progress-browse') expect(strategy.family).to.equal('progress-browse')
expect(strategy.paginationMode).to.equal('keyset') expect(strategy.paginationMode).to.equal('keyset')
expect(strategy.cursorKeys).to.deep.equal(['mediaProgresses.updatedAt', 'id'])
}) })
it('uses keyset pagination for updated browse', () => { it('uses keyset pagination for updated browse', () => {
@ -64,11 +86,13 @@ describe('libraryBrowseStrategy', () => {
}) })
expect(strategy.family).to.equal('plain-browse') expect(strategy.family).to.equal('plain-browse')
expect(strategy.paginationMode).to.equal('keyset') expect(strategy.paginationMode).to.equal('offset')
expect(strategy.cursorKeys).to.deep.equal(['updatedAt', 'id']) expect(strategy.cursorKeys).to.deep.equal([])
}) })
it('uses keyset pagination for deterministic author browse', () => { it('uses keyset pagination for deterministic author browse', () => {
global.ServerSettings = { sortingIgnorePrefix: true }
const strategy = getLibraryBrowseStrategy({ const strategy = getLibraryBrowseStrategy({
mediaType: 'book', mediaType: 'book',
sortBy: 'media.metadata.authorName', sortBy: 'media.metadata.authorName',
@ -81,6 +105,47 @@ describe('libraryBrowseStrategy', () => {
expect(strategy.cursorKeys).to.deep.equal(['authorNamesFirstLast', 'titleIgnorePrefix', 'id']) expect(strategy.cursorKeys).to.deep.equal(['authorNamesFirstLast', 'titleIgnorePrefix', 'id'])
}) })
it('uses keyset pagination for deterministic author last-first browse', () => {
global.ServerSettings = { sortingIgnorePrefix: false }
const strategy = getLibraryBrowseStrategy({
mediaType: 'book',
sortBy: 'media.metadata.authorNameLF',
filterGroup: null,
pageMode: 'endless'
})
expect(strategy.family).to.equal('author-browse')
expect(strategy.paginationMode).to.equal('keyset')
expect(strategy.cursorKeys).to.deep.equal(['authorNamesLastFirst', 'title', 'id'])
})
it('uses keyset pagination for progress created-at browse', () => {
const strategy = getLibraryBrowseStrategy({
mediaType: 'book',
sortBy: 'progress.createdAt',
filterGroup: 'progress',
pageMode: 'endless'
})
expect(strategy.family).to.equal('progress-browse')
expect(strategy.paginationMode).to.equal('keyset')
expect(strategy.cursorKeys).to.deep.equal(['mediaProgresses.createdAt', 'id'])
})
it('uses keyset pagination for progress finished-at browse', () => {
const strategy = getLibraryBrowseStrategy({
mediaType: 'book',
sortBy: 'progress.finishedAt',
filterGroup: 'progress',
pageMode: 'endless'
})
expect(strategy.family).to.equal('progress-browse')
expect(strategy.paginationMode).to.equal('keyset')
expect(strategy.cursorKeys).to.deep.equal(['mediaProgresses.finishedAt', 'id'])
})
it('defines a filtered browse family for generic library filters', () => { it('defines a filtered browse family for generic library filters', () => {
const strategy = getLibraryBrowseStrategy({ const strategy = getLibraryBrowseStrategy({
mediaType: 'book', mediaType: 'book',
@ -125,4 +190,16 @@ describe('libraryBrowseStrategy', () => {
expect(strategy.family).to.equal('podcast-browse') expect(strategy.family).to.equal('podcast-browse')
}) })
it('falls back to offset for unsupported endless-scroll sorts', () => {
const strategy = getLibraryBrowseStrategy({
mediaType: 'book',
sortBy: 'size',
filterGroup: null,
pageMode: 'endless'
})
expect(strategy.paginationMode).to.equal('offset')
expect(strategy.cursorKeys).to.deep.equal([])
})
}) })