fix: restore collapsed-series browse payloads

This commit is contained in:
Jonathan Finley 2026-03-13 22:40:38 -04:00
parent 394f2e2254
commit 168844e7a8
4 changed files with 208 additions and 16 deletions

View file

@ -1,7 +1,101 @@
const { createNewSortInstance } = require('../libs/fastSort')
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
})
function getSeriesFromBooks(libraryItems, filterSeries, hideSingleBookSeries) {
const seriesById = {}
libraryItems.forEach((libraryItem) => {
const allBookSeries = libraryItem.media.series || []
if (!allBookSeries.length) return
allBookSeries.forEach((bookSeries) => {
const itemJson = libraryItem.toOldJSONMinified()
itemJson.sequence = bookSeries.bookSeries.sequence
if (filterSeries) {
const filteredSeries = libraryItem.media.series.find((series) => series.id === filterSeries)
itemJson.filterSeriesSequence = filteredSeries?.bookSeries?.sequence
}
if (!seriesById[bookSeries.id]) {
seriesById[bookSeries.id] = {
id: bookSeries.id,
name: bookSeries.name,
nameIgnorePrefix: getTitlePrefixAtEnd(bookSeries.name),
nameIgnorePrefixSort: getTitleIgnorePrefix(bookSeries.name),
type: 'series',
books: [itemJson],
totalDuration: isNullOrNaN(itemJson.media.duration) ? 0 : Number(itemJson.media.duration)
}
} else {
seriesById[bookSeries.id].books.push(itemJson)
seriesById[bookSeries.id].totalDuration += isNullOrNaN(itemJson.media.duration) ? 0 : Number(itemJson.media.duration)
}
})
})
let seriesItems = Object.values(seriesById)
if (hideSingleBookSeries) {
seriesItems = seriesItems.filter((series) => series.books.length > 1)
}
return seriesItems.map((series) => {
series.books = naturalSort(series.books).asc((libraryItem) => libraryItem.sequence)
return series
})
}
function collapseBookSeries(libraryItems, filterSeries, hideSingleBookSeries) {
const seriesObjects = getSeriesFromBooks(libraryItems, filterSeries, hideSingleBookSeries).filter((series) => series.id !== filterSeries)
const filteredLibraryItems = []
libraryItems.forEach((libraryItem) => {
if (libraryItem.mediaType && libraryItem.mediaType !== 'book') return
seriesObjects
.filter((series) => series.books[0].id === libraryItem.id)
.forEach((series) => {
filteredLibraryItems.push(Object.assign(Object.create(Object.getPrototypeOf(libraryItem)), libraryItem, { collapsedSeries: series }))
})
if (!seriesObjects.some((series) => series.books.some((book) => book.id === libraryItem.id))) {
filteredLibraryItems.push(libraryItem)
}
})
return filteredLibraryItems
}
function getSeriesSequenceList(collapsedSeries) {
return naturalSort(collapsedSeries.books.filter((book) => book.filterSeriesSequence).map((book) => book.filterSeriesSequence))
.asc()
.reduce((ranges, currentSequence) => {
const lastRange = ranges.at(-1)
const isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence)
const normalizedSequence = isNumber ? parseFloat(currentSequence) : currentSequence
if (lastRange && isNumber && lastRange.isNumber && lastRange.end + 1 === normalizedSequence) {
lastRange.end = normalizedSequence
} else {
ranges.push({ start: normalizedSequence, end: normalizedSequence, isNumber })
}
return ranges
}, [])
.map((range) => (range.start === range.end ? range.start : `${range.start}-${range.end}`))
.join(', ')
}
module.exports = {
async toCollapsedSeriesPayload(libraryItems, seriesId) {
async toCollapsedSeriesPayload(libraryItems, seriesId, hideSingleBookSeries = false) {
const collapsedItems = collapseBookSeries(libraryItems, seriesId, hideSingleBookSeries)
const shapedItems = !(collapsedItems.length === 1 && collapsedItems[0].collapsedSeries) ? collapsedItems : libraryItems
return Promise.all(
libraryItems.map(async (libraryItem) => {
shapedItems.map(async (libraryItem) => {
const filteredSeries = libraryItem.media.series.find((series) => series.id === seriesId)
const json = libraryItem.toOldJSONMinified()
@ -13,6 +107,21 @@ module.exports = {
}
}
if (libraryItem.collapsedSeries) {
json.collapsedSeries = {
id: libraryItem.collapsedSeries.id,
name: libraryItem.collapsedSeries.name,
nameIgnorePrefix: libraryItem.collapsedSeries.nameIgnorePrefix,
libraryItemIds: libraryItem.collapsedSeries.books.map((book) => book.id),
numBooks: libraryItem.collapsedSeries.books.length,
seriesSequenceList: getSeriesSequenceList(libraryItem.collapsedSeries)
}
}
if (libraryItem.rssFeed?.toOldJSONMinified) {
json.rssFeed = libraryItem.rssFeed.toOldJSONMinified()
}
return json
})
)

View file

@ -917,13 +917,14 @@ module.exports = {
}
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
const collapsedSortBy = payload.sortBy || 'sequence'
const direction = payload.sortDesc ? 'DESC' : 'ASC'
const sortOrder = payload.sortBy === 'sequence'
const sortOrder = collapsedSortBy === 'sequence'
? [
[Sequelize.literal(`${dialectHelpers.getSafeSequenceCast(Database.getDialect(), '"bookSeries"."sequence"')} ${direction} NULLS LAST`)],
[Sequelize.literal('"libraryItem"."id"'), 'ASC']
...this.getOrder('media.metadata.title', payload.sortDesc, false)
]
: this.getOrder(payload.sortBy, payload.sortDesc, false)
: this.getOrder(collapsedSortBy, payload.sortDesc, false)
const findOptions = {
where: userPermissionBookWhere.bookWhere,
@ -949,6 +950,13 @@ module.exports = {
attributes: ['id', 'name', 'nameIgnorePrefix']
}
},
{
model: Database.seriesModel,
attributes: ['id', 'name', 'nameIgnorePrefix'],
through: {
attributes: ['sequence']
}
},
{
model: Database.bookAuthorModel,
attributes: ['authorId', 'createdAt'],
@ -966,10 +974,28 @@ module.exports = {
}
const countOptions = {
...findOptions,
distinct: true
where: userPermissionBookWhere.bookWhere,
replacements: userPermissionBookWhere.replacements,
include: [
{
model: Database.libraryItemModel,
required: true,
where: {
libraryId
},
include: libraryItemIncludes
},
{
model: Database.bookSeriesModel,
required: true,
where: {
seriesId
}
}
],
distinct: true,
subQuery: false
}
delete countOptions.order
const { books, count } = await loadCollapsedSeriesWindow({
findOptions,
@ -985,13 +1011,7 @@ module.exports = {
delete book.libraryItem
book.series =
book.bookSeries?.map((bs) => {
const series = bs.series
delete bs.series
series.bookSeries = bs
return series
}) || []
book.series = book.series || []
delete book.bookSeries
book.authors = book.bookAuthors?.map((ba) => ba.author) || []