2023-08-20 00:12:24 +02:00
|
|
|
const { createNewSortInstance } = require('../libs/fastSort')
|
2023-07-05 01:14:44 +02:00
|
|
|
const Database = require('../Database')
|
2022-11-03 05:14:07 +01:00
|
|
|
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
|
2021-12-26 18:25:07 +01:00
|
|
|
const naturalSort = createNewSortInstance({
|
|
|
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
|
|
|
})
|
2021-12-01 03:02:40 +01:00
|
|
|
|
|
|
|
module.exports = {
|
2023-09-04 22:26:07 +02:00
|
|
|
getSeriesFromBooks(books, filterSeries, hideSingleBookSeries) {
|
2022-10-29 18:17:51 +02:00
|
|
|
const _series = {}
|
2022-10-29 22:33:38 +02:00
|
|
|
const seriesToFilterOut = {}
|
2022-03-13 01:50:31 +01:00
|
|
|
books.forEach((libraryItem) => {
|
2022-10-29 22:33:38 +02:00
|
|
|
// get all book series for item that is not already filtered out
|
|
|
|
const bookSeries = (libraryItem.media.metadata.series || []).filter(se => !seriesToFilterOut[se.id])
|
|
|
|
if (!bookSeries.length) return
|
|
|
|
|
2022-10-29 18:17:51 +02:00
|
|
|
bookSeries.forEach((bookSeriesObj) => {
|
2023-09-04 22:26:07 +02:00
|
|
|
// const series = allSeries.find(se => se.id === bookSeriesObj.id)
|
2022-10-29 18:17:51 +02:00
|
|
|
|
2023-09-04 22:26:07 +02:00
|
|
|
const abJson = libraryItem.toJSONMinified()
|
2022-10-29 18:17:51 +02:00
|
|
|
abJson.sequence = bookSeriesObj.sequence
|
2022-10-30 16:38:00 +01:00
|
|
|
if (filterSeries) {
|
|
|
|
abJson.filterSeriesSequence = libraryItem.media.metadata.getSeries(filterSeries).sequence
|
|
|
|
}
|
2022-10-29 18:17:51 +02:00
|
|
|
if (!_series[bookSeriesObj.id]) {
|
|
|
|
_series[bookSeriesObj.id] = {
|
|
|
|
id: bookSeriesObj.id,
|
|
|
|
name: bookSeriesObj.name,
|
2022-11-03 05:14:07 +01:00
|
|
|
nameIgnorePrefix: getTitlePrefixAtEnd(bookSeriesObj.name),
|
|
|
|
nameIgnorePrefixSort: getTitleIgnorePrefix(bookSeriesObj.name),
|
2022-03-27 23:16:08 +02:00
|
|
|
type: 'series',
|
2022-10-29 18:17:51 +02:00
|
|
|
books: [abJson],
|
|
|
|
totalDuration: isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration)
|
2021-12-01 03:02:40 +01:00
|
|
|
}
|
2022-03-27 23:16:08 +02:00
|
|
|
} else {
|
2022-10-29 18:17:51 +02:00
|
|
|
_series[bookSeriesObj.id].books.push(abJson)
|
|
|
|
_series[bookSeriesObj.id].totalDuration += isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration)
|
2022-03-27 23:16:08 +02:00
|
|
|
}
|
|
|
|
})
|
2021-12-01 03:02:40 +01:00
|
|
|
})
|
2022-10-29 22:33:38 +02:00
|
|
|
|
2022-12-31 23:58:19 +01:00
|
|
|
let seriesItems = Object.values(_series)
|
2022-10-29 22:33:38 +02:00
|
|
|
|
2023-06-30 00:55:17 +02:00
|
|
|
// Library setting to hide series with only 1 book
|
|
|
|
if (hideSingleBookSeries) {
|
|
|
|
seriesItems = seriesItems.filter(se => se.books.length > 1)
|
|
|
|
}
|
|
|
|
|
2022-10-29 22:33:38 +02:00
|
|
|
return seriesItems.map((series) => {
|
2022-03-13 01:50:31 +01:00
|
|
|
series.books = naturalSort(series.books).asc(li => li.sequence)
|
2021-12-06 23:18:26 +01:00
|
|
|
return series
|
|
|
|
})
|
2021-12-01 03:02:40 +01:00
|
|
|
},
|
|
|
|
|
2023-09-04 22:26:07 +02:00
|
|
|
collapseBookSeries(libraryItems, filterSeries, hideSingleBookSeries) {
|
2022-10-30 15:21:12 +01:00
|
|
|
// Get series from the library items. If this list is being collapsed after filtering for a series,
|
|
|
|
// don't collapse that series, only books that are in other series.
|
2022-12-31 17:59:12 +01:00
|
|
|
const seriesObjects = this
|
2023-09-04 22:26:07 +02:00
|
|
|
.getSeriesFromBooks(libraryItems, filterSeries, hideSingleBookSeries)
|
2022-10-30 15:21:12 +01:00
|
|
|
.filter(s => s.id != filterSeries)
|
|
|
|
|
2022-12-31 17:59:12 +01:00
|
|
|
const filteredLibraryItems = []
|
2022-04-10 02:44:46 +02:00
|
|
|
|
2022-10-30 03:54:31 +01:00
|
|
|
libraryItems.forEach((li) => {
|
2022-04-10 02:44:46 +02:00
|
|
|
if (li.mediaType != 'book') return
|
2022-10-30 03:54:31 +01:00
|
|
|
|
|
|
|
// Handle when this is the first book in a series
|
|
|
|
seriesObjects.filter(s => s.books[0].id == li.id).forEach(series => {
|
2022-10-30 15:21:12 +01:00
|
|
|
// Clone the library item as we need to attach data to it, but don't
|
|
|
|
// want to change the global copy of the library item
|
|
|
|
filteredLibraryItems.push(Object.assign(
|
|
|
|
Object.create(Object.getPrototypeOf(li)),
|
|
|
|
li, { collapsedSeries: series }))
|
2022-12-31 17:59:12 +01:00
|
|
|
})
|
2022-10-30 03:54:31 +01:00
|
|
|
|
2022-10-30 15:21:12 +01:00
|
|
|
// Only included books not contained in series
|
|
|
|
if (!seriesObjects.some(s => s.books.some(b => b.id == li.id)))
|
|
|
|
filteredLibraryItems.push(li)
|
2022-12-31 17:59:12 +01:00
|
|
|
})
|
2022-10-30 03:54:31 +01:00
|
|
|
|
2022-10-30 15:21:12 +01:00
|
|
|
return filteredLibraryItems
|
2023-09-04 22:26:07 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
async handleCollapseSubseries(payload, seriesId, user, library) {
|
|
|
|
const seriesWithBooks = await Database.seriesModel.findByPk(seriesId, {
|
|
|
|
include: {
|
|
|
|
model: Database.bookModel,
|
|
|
|
through: {
|
|
|
|
attributes: ['sequence']
|
|
|
|
},
|
|
|
|
include: [
|
|
|
|
{
|
|
|
|
model: Database.libraryItemModel
|
|
|
|
},
|
|
|
|
{
|
|
|
|
model: Database.authorModel,
|
|
|
|
through: {
|
|
|
|
attributes: []
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
model: Database.seriesModel,
|
|
|
|
through: {
|
|
|
|
attributes: ['sequence']
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (!seriesWithBooks) {
|
|
|
|
payload.total = 0
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const books = seriesWithBooks.books
|
|
|
|
payload.total = books.length
|
|
|
|
|
|
|
|
let libraryItems = books.map((book) => {
|
|
|
|
const libraryItem = book.libraryItem
|
|
|
|
libraryItem.media = book
|
|
|
|
return Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
|
|
|
}).filter(li => {
|
|
|
|
return user.checkCanAccessLibraryItem(li)
|
|
|
|
})
|
|
|
|
|
|
|
|
const collapsedItems = this.collapseBookSeries(libraryItems, seriesId, library.settings.hideSingleBookSeries)
|
|
|
|
if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) {
|
|
|
|
libraryItems = collapsedItems
|
|
|
|
payload.total = libraryItems.length
|
|
|
|
}
|
|
|
|
|
|
|
|
const sortArray = [
|
|
|
|
{
|
|
|
|
asc: (li) => li.media.metadata.getSeries(seriesId).sequence
|
|
|
|
},
|
|
|
|
{ // If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
|
|
|
asc: (li) => {
|
|
|
|
if (Database.serverSettings.sortingIgnorePrefix) {
|
|
|
|
return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix
|
|
|
|
} else {
|
|
|
|
return li.collapsedSeries?.name || li.media.metadata.title
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
libraryItems = naturalSort(libraryItems).by(sortArray)
|
|
|
|
|
|
|
|
if (payload.limit) {
|
|
|
|
const startIndex = payload.page * payload.limit
|
|
|
|
libraryItems = libraryItems.slice(startIndex, startIndex + payload.limit)
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.all(libraryItems.map(async li => {
|
|
|
|
const filteredSeries = li.media.metadata.getSeries(seriesId)
|
|
|
|
const json = li.toJSONMinified()
|
|
|
|
json.media.metadata.series = {
|
|
|
|
id: filteredSeries.id,
|
|
|
|
sequence: filteredSeries.sequence
|
|
|
|
}
|
|
|
|
|
|
|
|
if (li.collapsedSeries) {
|
|
|
|
json.collapsedSeries = {
|
|
|
|
id: li.collapsedSeries.id,
|
|
|
|
name: li.collapsedSeries.name,
|
|
|
|
nameIgnorePrefix: li.collapsedSeries.nameIgnorePrefix,
|
|
|
|
libraryItemIds: li.collapsedSeries.books.map(b => b.id),
|
|
|
|
numBooks: li.collapsedSeries.books.length
|
|
|
|
}
|
|
|
|
|
|
|
|
// If collapsing by series and filtering by a series, generate the list of sequences the collapsed
|
|
|
|
// series represents in the filtered series
|
|
|
|
json.collapsedSeries.seriesSequenceList =
|
|
|
|
naturalSort(li.collapsedSeries.books.filter(b => b.filterSeriesSequence).map(b => b.filterSeriesSequence)).asc()
|
|
|
|
.reduce((ranges, currentSequence) => {
|
|
|
|
let lastRange = ranges.at(-1)
|
|
|
|
let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence)
|
|
|
|
if (isNumber) currentSequence = parseFloat(currentSequence)
|
|
|
|
|
|
|
|
if (lastRange && isNumber && lastRange.isNumber && ((lastRange.end + 1) == currentSequence)) {
|
|
|
|
lastRange.end = currentSequence
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
ranges.push({ start: currentSequence, end: currentSequence, isNumber: isNumber })
|
|
|
|
}
|
|
|
|
|
|
|
|
return ranges
|
|
|
|
}, [])
|
|
|
|
.map(r => r.start == r.end ? r.start : `${r.start}-${r.end}`)
|
|
|
|
.join(', ')
|
|
|
|
}
|
|
|
|
|
|
|
|
return json
|
|
|
|
}))
|
2022-04-10 02:44:46 +02:00
|
|
|
}
|
2023-02-19 22:39:28 +01:00
|
|
|
}
|