diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 33f80bc74..29864463c 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -634,6 +634,7 @@ class LibraryController { const filterByValue = filterByGroup ? libraryFilters.decode(payload.filterBy.replace(`${filterByGroup}.`, '')) : null if (filterByGroup === 'series' && filterByValue !== 'no-series' && payload.collapseseries) { const seriesId = libraryFilters.decode(payload.filterBy.split('.')[1]) + payload.sortBy = libraryItemsBookFilters.getCollapsedSeriesBrowseSort(payload.sortBy) const collapsedSeriesWindow = { limit: Number(payload.limit) || 0, offset: Number(payload.offset) || 0 diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index fa6b62f63..c0ec1e277 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -172,7 +172,7 @@ async function loadCollapsedSeriesWindow({ findOptions, countOptions, limit, off } function getCollapsedSeriesSortContext(payload = {}) { - const collapsedSortBy = payload.sortBy || 'sequence' + const collapsedSortBy = module.exports.getCollapsedSeriesBrowseSort(payload.sortBy) const sortDesc = !!payload.sortDesc const direction = sortDesc ? 'DESC' : 'ASC' const titleColumn = Database.serverSettings.sortingIgnorePrefix ? 'titleIgnorePrefix' : 'title' @@ -454,6 +454,13 @@ function getNextBrowseCursor(books, limit, sortBy, sortDesc, cursorKeys) { } module.exports = { + getCollapsedSeriesBrowseSort(sortBy) { + if (!sortBy || sortBy === 'sequence' || sortBy === 'media.metadata.title') { + return sortBy || 'sequence' + } + return 'sequence' + }, + /** * User permissions to restrict books for explicit content & tags * @param {import('../../models/User')} user diff --git a/test/server/controllers/LibraryController.largeLibraryBrowse.test.js b/test/server/controllers/LibraryController.largeLibraryBrowse.test.js index 307437252..968d651ff 100644 --- a/test/server/controllers/LibraryController.largeLibraryBrowse.test.js +++ b/test/server/controllers/LibraryController.largeLibraryBrowse.test.js @@ -390,6 +390,34 @@ describe('LibraryController large-library browse contract', () => { expect(queryStub.secondCall.args[0]).to.include('filterSequenceSort') }) + it('falls back to sequence-based collapsed browse ordering for unsupported sorts', async () => { + const queryStub = sinon.stub(Database.sequelize, 'query') + queryStub.onCall(0).resolves([{ rawBookCount: 0, plainRowCount: 0, collapsedRowCount: 0 }]) + queryStub.onCall(1).resolves([]) + const findAllStub = sinon.stub(Database.bookModel, 'findAll').resolves([]) + + const req = { + query: { + filter: `series.${Buffer.from('series_1').toString('base64')}`, + collapseseries: '1', + limit: '20', + sort: 'updatedAt', + desc: '1' + }, + library: { id: 'lib_1', mediaType: 'book', isVirtual: false, settings: {} }, + user: { id: 'user_1', checkCanAccessLibraryItem: () => true } + } + const res = { json: sinon.spy() } + + await LibraryController.getLibraryItems(req, res) + + expect(queryStub.calledTwice).to.equal(true) + expect(findAllStub.called).to.equal(false) + expect(queryStub.secondCall.args[0]).to.include('filterSequenceSort') + expect(queryStub.secondCall.args[0]).to.not.include('updatedAt') + expect(res.json.firstCall.args[0].sortBy).to.equal('sequence') + }) + it('paginates collapsed-series browse by visible rows instead of raw books', async () => { const makeBook = ({ libraryItemId, filterSequence, subseriesId = null, subseriesName = null }) => ({ libraryItem: {