mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			232 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			232 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const { createNewSortInstance } = require('../libs/fastSort')
 | |
| const Database = require('../Database')
 | |
| const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
 | |
| const naturalSort = createNewSortInstance({
 | |
|   comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
 | |
| })
 | |
| 
 | |
| module.exports = {
 | |
|   getSeriesFromBooks(books, filterSeries, hideSingleBookSeries) {
 | |
|     const _series = {}
 | |
|     const seriesToFilterOut = {}
 | |
|     books.forEach((libraryItem) => {
 | |
|       // 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
 | |
| 
 | |
|       bookSeries.forEach((bookSeriesObj) => {
 | |
|         // const series = allSeries.find(se => se.id === bookSeriesObj.id)
 | |
| 
 | |
|         const abJson = libraryItem.toJSONMinified()
 | |
|         abJson.sequence = bookSeriesObj.sequence
 | |
|         if (filterSeries) {
 | |
|           abJson.filterSeriesSequence = libraryItem.media.metadata.getSeries(filterSeries).sequence
 | |
|         }
 | |
|         if (!_series[bookSeriesObj.id]) {
 | |
|           _series[bookSeriesObj.id] = {
 | |
|             id: bookSeriesObj.id,
 | |
|             name: bookSeriesObj.name,
 | |
|             nameIgnorePrefix: getTitlePrefixAtEnd(bookSeriesObj.name),
 | |
|             nameIgnorePrefixSort: getTitleIgnorePrefix(bookSeriesObj.name),
 | |
|             type: 'series',
 | |
|             books: [abJson],
 | |
|             totalDuration: isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration)
 | |
|           }
 | |
|         } else {
 | |
|           _series[bookSeriesObj.id].books.push(abJson)
 | |
|           _series[bookSeriesObj.id].totalDuration += isNullOrNaN(abJson.media.duration) ? 0 : Number(abJson.media.duration)
 | |
|         }
 | |
|       })
 | |
|     })
 | |
| 
 | |
|     let seriesItems = Object.values(_series)
 | |
| 
 | |
|     // Library setting to hide series with only 1 book
 | |
|     if (hideSingleBookSeries) {
 | |
|       seriesItems = seriesItems.filter(se => se.books.length > 1)
 | |
|     }
 | |
| 
 | |
|     return seriesItems.map((series) => {
 | |
|       series.books = naturalSort(series.books).asc(li => li.sequence)
 | |
|       return series
 | |
|     })
 | |
|   },
 | |
| 
 | |
|   collapseBookSeries(libraryItems, filterSeries, hideSingleBookSeries) {
 | |
|     // 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.
 | |
|     const seriesObjects = this
 | |
|       .getSeriesFromBooks(libraryItems, filterSeries, hideSingleBookSeries)
 | |
|       .filter(s => s.id != filterSeries)
 | |
| 
 | |
|     const filteredLibraryItems = []
 | |
| 
 | |
|     libraryItems.forEach((li) => {
 | |
|       if (li.mediaType != 'book') return
 | |
| 
 | |
|       // Handle when this is the first book in a series
 | |
|       seriesObjects.filter(s => s.books[0].id == li.id).forEach(series => {
 | |
|         // 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 }))
 | |
|       })
 | |
| 
 | |
|       // Only included books not contained in series
 | |
|       if (!seriesObjects.some(s => s.books.some(b => b.id == li.id)))
 | |
|         filteredLibraryItems.push(li)
 | |
|     })
 | |
| 
 | |
|     return filteredLibraryItems
 | |
|   },
 | |
| 
 | |
|   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 sortingIgnorePrefix = Database.serverSettings.sortingIgnorePrefix
 | |
| 
 | |
|     let sortArray = []
 | |
|     const direction = payload.sortDesc ? 'desc' : 'asc'
 | |
|     if (!payload.sortBy || payload.sortBy === 'sequence') {
 | |
|       sortArray = [
 | |
|         {
 | |
|           [direction]: (li) => li.media.metadata.getSeries(seriesId).sequence
 | |
|         },
 | |
|         { // If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
 | |
|           [direction]: (li) => {
 | |
|             if (sortingIgnorePrefix) {
 | |
|               return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix
 | |
|             } else {
 | |
|               return li.collapsedSeries?.name || li.media.metadata.title
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       ]
 | |
|     } else {
 | |
|       // If series are collapsed and not sorting by title or sequence, 
 | |
|       // sort all collapsed series to the end in alphabetical order
 | |
|       if (payload.sortBy !== 'media.metadata.title') {
 | |
|         sortArray.push({
 | |
|           asc: (li) => {
 | |
|             if (li.collapsedSeries) {
 | |
|               return sortingIgnorePrefix ? li.collapsedSeries.nameIgnorePrefix : li.collapsedSeries.name
 | |
|             } else {
 | |
|               return ''
 | |
|             }
 | |
|           }
 | |
|         })
 | |
|       }
 | |
|       sortArray.push({
 | |
|         [direction]: (li) => {
 | |
|           if (payload.sortBy === 'media.metadata.title') {
 | |
|             if (sortingIgnorePrefix) {
 | |
|               return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix
 | |
|             } else {
 | |
|               return li.collapsedSeries?.name || li.media.metadata.title
 | |
|             }
 | |
|           } else {
 | |
|             return payload.sortBy.split('.').reduce((a, b) => a[b], li)
 | |
|           }
 | |
|         }
 | |
|       })
 | |
|     }
 | |
| 
 | |
|     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,
 | |
|         name: filteredSeries.name,
 | |
|         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
 | |
|     }))
 | |
|   }
 | |
| }
 |