mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +00:00
fix: normalize book browse cursor values
This commit is contained in:
parent
ed3a2c64b3
commit
6788943087
2 changed files with 74 additions and 13 deletions
|
|
@ -11,20 +11,47 @@ const { loadBrowseCount } = require('./libraryBrowseCount')
|
||||||
const { encodeBrowseCursor, decodeBrowseCursor } = require('./libraryBrowseCursor')
|
const { encodeBrowseCursor, decodeBrowseCursor } = require('./libraryBrowseCursor')
|
||||||
const countCache = new Map()
|
const countCache = new Map()
|
||||||
|
|
||||||
function getCursorFieldPath(key) {
|
function normalizeCursorValue(key, value) {
|
||||||
|
if (value == null) return value
|
||||||
|
|
||||||
|
if (['title', 'titleIgnorePrefix', 'authorNamesFirstLast', 'authorNamesLastFirst'].includes(key) && typeof value === 'string') {
|
||||||
|
return value.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQuotedCursorColumn(key) {
|
||||||
|
const dialect = Database.getDialect()
|
||||||
|
const getColumnRef = (table, column) => {
|
||||||
|
return dialect === 'postgres'
|
||||||
|
? `"${table}"."${column}"`
|
||||||
|
: `\`${table}\`.\`${column}\``
|
||||||
|
}
|
||||||
|
|
||||||
if (['title', 'titleIgnorePrefix', 'authorNamesFirstLast', 'authorNamesLastFirst', 'createdAt', 'updatedAt'].includes(key)) {
|
if (['title', 'titleIgnorePrefix', 'authorNamesFirstLast', 'authorNamesLastFirst', 'createdAt', 'updatedAt'].includes(key)) {
|
||||||
return `$libraryItem.${key}$`
|
return getColumnRef('libraryItem', key)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'id') {
|
if (key === 'id') {
|
||||||
return '$libraryItem.id$'
|
return getColumnRef('libraryItem', 'id')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.startsWith('mediaProgresses.')) {
|
if (key.startsWith('mediaProgresses.')) {
|
||||||
return `$${key}$`
|
return key.split('.').map((segment) => `"${segment}"`).join('.')
|
||||||
}
|
}
|
||||||
|
|
||||||
return `$${key}$`
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCursorComparisonExpression(key) {
|
||||||
|
const quotedColumn = getQuotedCursorColumn(key)
|
||||||
|
|
||||||
|
if (['title', 'titleIgnorePrefix', 'authorNamesFirstLast', 'authorNamesLastFirst'].includes(key)) {
|
||||||
|
return Sequelize.literal(dialectHelpers.getCaseInsensitiveOrder(Database.getDialect(), quotedColumn, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Sequelize.literal(quotedColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCursorValue(book, key) {
|
function getCursorValue(book, key) {
|
||||||
|
|
@ -38,10 +65,10 @@ function getCursorValue(book, key) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (book.libraryItem && Object.prototype.hasOwnProperty.call(book.libraryItem.dataValues || {}, key)) {
|
if (book.libraryItem && Object.prototype.hasOwnProperty.call(book.libraryItem.dataValues || {}, key)) {
|
||||||
return book.libraryItem[key]
|
return normalizeCursorValue(key, book.libraryItem[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
return book[key] ?? null
|
return normalizeCursorValue(key, book[key] ?? null)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildBookCursorClause(cursor, cursorKeys, sortDesc) {
|
function buildBookCursorClause(cursor, cursorKeys, sortDesc) {
|
||||||
|
|
@ -60,17 +87,19 @@ function buildBookCursorClause(cursor, cursorKeys, sortDesc) {
|
||||||
|
|
||||||
const comparisonOperator = sortDesc ? Sequelize.Op.lt : Sequelize.Op.gt
|
const comparisonOperator = sortDesc ? Sequelize.Op.lt : Sequelize.Op.gt
|
||||||
const cursorConditions = decodedCursor.keys.map((key, index) => {
|
const cursorConditions = decodedCursor.keys.map((key, index) => {
|
||||||
const condition = {}
|
const condition = []
|
||||||
|
|
||||||
for (let cursorIndex = 0; cursorIndex < index; cursorIndex++) {
|
for (let cursorIndex = 0; cursorIndex < index; cursorIndex++) {
|
||||||
condition[getCursorFieldPath(decodedCursor.keys[cursorIndex])] = decodedCursor.values[cursorIndex]
|
condition.push(Sequelize.where(getCursorComparisonExpression(decodedCursor.keys[cursorIndex]), normalizeCursorValue(decodedCursor.keys[cursorIndex], decodedCursor.values[cursorIndex])))
|
||||||
}
|
}
|
||||||
|
|
||||||
condition[getCursorFieldPath(key)] = {
|
condition.push(Sequelize.where(getCursorComparisonExpression(key), {
|
||||||
[comparisonOperator]: decodedCursor.values[index]
|
[comparisonOperator]: normalizeCursorValue(key, decodedCursor.values[index])
|
||||||
}
|
}))
|
||||||
|
|
||||||
return condition
|
return {
|
||||||
|
[Sequelize.Op.and]: condition
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const sinon = require('sinon')
|
||||||
const Database = require('../../../server/Database')
|
const Database = require('../../../server/Database')
|
||||||
const LibraryController = require('../../../server/controllers/LibraryController')
|
const LibraryController = require('../../../server/controllers/LibraryController')
|
||||||
const libraryFilters = require('../../../server/utils/queries/libraryFilters')
|
const libraryFilters = require('../../../server/utils/queries/libraryFilters')
|
||||||
|
const { decodeBrowseCursor } = require('../../../server/utils/queries/libraryBrowseCursor')
|
||||||
const { getLibraryBrowseStrategy } = require('../../../server/utils/queries/libraryBrowseStrategy')
|
const { getLibraryBrowseStrategy } = require('../../../server/utils/queries/libraryBrowseStrategy')
|
||||||
const libraryItemsBookFilters = require('../../../server/utils/queries/libraryItemsBookFilters')
|
const libraryItemsBookFilters = require('../../../server/utils/queries/libraryItemsBookFilters')
|
||||||
const libraryItemsPodcastFilters = require('../../../server/utils/queries/libraryItemsPodcastFilters')
|
const libraryItemsPodcastFilters = require('../../../server/utils/queries/libraryItemsPodcastFilters')
|
||||||
|
|
@ -290,6 +291,37 @@ describe('LibraryController large-library browse contract', () => {
|
||||||
expect(findAndCountAllStub.called).to.equal(false)
|
expect(findAndCountAllStub.called).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('encodes case-insensitive title cursor values to match the executed browse order', async () => {
|
||||||
|
global.ServerSettings = { sortingIgnorePrefix: true }
|
||||||
|
|
||||||
|
sinon.stub(Database.bookModel, 'count').resolves(123)
|
||||||
|
sinon.stub(Database.bookModel, 'findAndCountAll').resolves({ rows: [], count: 123 })
|
||||||
|
sinon.stub(Database.bookModel, 'findAll').resolves([
|
||||||
|
{ id: 'book-1', title: 'Alpha', libraryItem: { id: 'item-1', titleIgnorePrefix: 'Alpha', dataValues: { titleIgnorePrefix: 'Alpha' } } },
|
||||||
|
{ id: 'book-2', title: 'aLPHa', libraryItem: { id: 'item-2', titleIgnorePrefix: 'aLPHa', dataValues: { titleIgnorePrefix: 'aLPHa' } } },
|
||||||
|
{ id: 'book-3', title: 'Beta', libraryItem: { id: 'item-3', titleIgnorePrefix: 'Beta', dataValues: { titleIgnorePrefix: 'Beta' } } }
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await libraryItemsBookFilters.getFilteredLibraryItems(
|
||||||
|
'lib_1',
|
||||||
|
{ id: 'user_1', canAccessExplicitContent: true, accessAllTags: true },
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'media.metadata.title',
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
{ pageMode: 'endless' }
|
||||||
|
)
|
||||||
|
|
||||||
|
const decodedCursor = decodeBrowseCursor(result.nextCursor)
|
||||||
|
|
||||||
|
expect(decodedCursor.values).to.deep.equal(['alpha', 'item-2'])
|
||||||
|
})
|
||||||
|
|
||||||
it('defaults omitted sort to deterministic title ordering for endless browse requests', async () => {
|
it('defaults omitted sort to deterministic title ordering for endless browse requests', async () => {
|
||||||
global.ServerSettings = { sortingIgnorePrefix: true }
|
global.ServerSettings = { sortingIgnorePrefix: true }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue