2022-06-08 03:22:23 +02:00
const { sort , createNewSortInstance } = require ( '../libs/fastSort' )
2023-01-03 01:02:04 +01:00
const Logger = require ( '../Logger' )
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 = {
decode ( text ) {
return Buffer . from ( decodeURIComponent ( text ) , 'base64' ) . toString ( )
} ,
2022-08-06 14:58:19 +02:00
getFilteredLibraryItems ( libraryItems , filterBy , user , feedsArray ) {
2022-11-28 00:42:02 +01:00
let filtered = libraryItems
2022-03-11 01:45:02 +01:00
2022-11-28 00:42:02 +01:00
const searchGroups = [ 'genres' , 'tags' , 'series' , 'authors' , 'progress' , 'narrators' , 'missing' , 'languages' , 'tracks' ]
const group = searchGroups . find ( _group => filterBy . startsWith ( _group + '.' ) )
2022-03-11 01:45:02 +01:00
if ( group ) {
2022-11-28 00:42:02 +01:00
const filterVal = filterBy . replace ( ` ${ group } . ` , '' )
const filter = this . decode ( filterVal )
2022-03-11 01:45:02 +01:00
if ( group === 'genres' ) filtered = filtered . filter ( li => li . media . metadata && li . media . metadata . genres . includes ( filter ) )
else if ( group === 'tags' ) filtered = filtered . filter ( li => li . media . tags . includes ( filter ) )
else if ( group === 'series' ) {
2022-11-28 00:54:40 +01:00
if ( filter === 'no-series' ) filtered = filtered . filter ( li => li . isBook && ! li . media . metadata . series . length )
2022-03-13 01:50:31 +01:00
else {
2022-11-28 00:42:02 +01:00
filtered = filtered . filter ( li => li . isBook && li . media . metadata . hasSeries ( filter ) )
2022-03-13 01:50:31 +01:00
}
2022-03-11 01:45:02 +01:00
}
2022-11-28 00:42:02 +01:00
else if ( group === 'authors' ) filtered = filtered . filter ( li => li . isBook && li . media . metadata . hasAuthor ( filter ) )
else if ( group === 'narrators' ) filtered = filtered . filter ( li => li . isBook && li . media . metadata . hasNarrator ( filter ) )
2022-03-11 01:45:02 +01:00
else if ( group === 'progress' ) {
filtered = filtered . filter ( li => {
2022-11-28 00:42:02 +01:00
const itemProgress = user . getMediaProgress ( li . id )
2022-11-28 00:54:40 +01:00
if ( filter === 'finished' && ( itemProgress && itemProgress . isFinished ) ) return true
if ( filter === 'not-started' && ! itemProgress ) return true
if ( filter === 'not-finished' && ( ! itemProgress || ! itemProgress . isFinished ) ) return true
if ( filter === 'in-progress' && ( itemProgress && itemProgress . inProgress ) ) return true
2022-03-11 01:45:02 +01:00
return false
} )
2022-04-18 08:31:39 +02:00
} else if ( group == 'missing' ) {
filtered = filtered . filter ( li => {
2022-11-28 00:42:02 +01:00
if ( li . isBook ) {
2022-12-16 00:46:27 +01:00
if ( filter === 'asin' && ! li . media . metadata . asin ) return true
if ( filter === 'isbn' && ! li . media . metadata . isbn ) return true
if ( filter === 'subtitle' && ! li . media . metadata . subtitle ) return true
if ( filter === 'authors' && ! li . media . metadata . authors . length ) return true
if ( filter === 'publishedYear' && ! li . media . metadata . publishedYear ) return true
if ( filter === 'series' && ! li . media . metadata . series . length ) return true
if ( filter === 'description' && ! li . media . metadata . description ) return true
if ( filter === 'genres' && ! li . media . metadata . genres . length ) return true
if ( filter === 'tags' && ! li . media . tags . length ) return true
if ( filter === 'narrators' && ! li . media . metadata . narrators . length ) return true
if ( filter === 'publisher' && ! li . media . metadata . publisher ) return true
if ( filter === 'language' && ! li . media . metadata . language ) return true
if ( filter === 'cover' && ! li . media . coverPath ) return true
2022-04-26 00:36:18 +02:00
} else {
return false
}
2022-04-18 08:31:39 +02:00
} )
2022-03-11 01:45:02 +01:00
} else if ( group === 'languages' ) {
filtered = filtered . filter ( li => li . media . metadata && li . media . metadata . language === filter )
2022-11-28 00:42:02 +01:00
} else if ( group === 'tracks' ) {
if ( filter === 'single' ) filtered = filtered . filter ( li => li . isBook && li . media . numTracks === 1 )
else if ( filter === 'multi' ) filtered = filtered . filter ( li => li . isBook && li . media . numTracks > 1 )
2022-03-11 01:45:02 +01:00
}
} else if ( filterBy === 'issues' ) {
2022-04-25 01:05:15 +02:00
filtered = filtered . filter ( li => li . hasIssues )
2022-08-06 14:58:19 +02:00
} else if ( filterBy === 'feed-open' ) {
filtered = filtered . filter ( li => feedsArray . some ( feed => feed . entityId === li . id ) )
2022-03-11 01:45:02 +01:00
}
return filtered
} ,
2022-10-29 22:33:38 +02:00
// Returns false if should be filtered out
checkFilterForSeriesLibraryItem ( libraryItem , filterBy ) {
var searchGroups = [ 'genres' , 'tags' , 'authors' , 'progress' , 'narrators' , 'languages' ]
var group = searchGroups . find ( _group => filterBy . startsWith ( _group + '.' ) )
if ( group ) {
var filterVal = filterBy . replace ( ` ${ group } . ` , '' )
var filter = this . decode ( filterVal )
if ( group === 'genres' ) return libraryItem . media . metadata && libraryItem . media . metadata . genres . includes ( filter )
else if ( group === 'tags' ) return libraryItem . media . tags . includes ( filter )
else if ( group === 'authors' ) return libraryItem . mediaType === 'book' && libraryItem . media . metadata . hasAuthor ( filter )
else if ( group === 'narrators' ) return libraryItem . mediaType === 'book' && libraryItem . media . metadata . hasNarrator ( filter )
else if ( group === 'languages' ) {
return libraryItem . media . metadata && libraryItem . media . metadata . language === filter
}
}
return true
} ,
// Return false to filter out series
checkSeriesProgressFilter ( series , filterBy , user ) {
const filter = this . decode ( filterBy . split ( '.' ) [ 1 ] )
var numBooksStartedOrFinished = 0
for ( const libraryItem of series . books ) {
const itemProgress = user . getMediaProgress ( libraryItem . id )
if ( filter === 'Finished' && ( ! itemProgress || ! itemProgress . isFinished ) ) return false
if ( filter === 'Not Started' && itemProgress ) return false
if ( itemProgress ) numBooksStartedOrFinished ++
}
if ( numBooksStartedOrFinished === series . books . length ) { // Completely finished series
if ( filter === 'Not Finished' ) return false
} else if ( numBooksStartedOrFinished === 0 && filter === 'In Progress' ) { // Series not started
return false
}
return true
} ,
2022-03-11 01:45:02 +01:00
getDistinctFilterDataNew ( libraryItems ) {
var data = {
authors : [ ] ,
genres : [ ] ,
tags : [ ] ,
series : [ ] ,
narrators : [ ] ,
languages : [ ]
}
libraryItems . forEach ( ( li ) => {
var mediaMetadata = li . media . metadata
2022-03-22 01:24:38 +01:00
if ( mediaMetadata . authors && mediaMetadata . authors . length ) {
2022-03-11 01:45:02 +01:00
mediaMetadata . authors . forEach ( ( author ) => {
2022-03-12 02:46:32 +01:00
if ( author && ! data . authors . find ( au => au . id === author . id ) ) data . authors . push ( { id : author . id , name : author . name } )
2022-03-11 01:45:02 +01:00
} )
}
2022-03-22 01:24:38 +01:00
if ( mediaMetadata . series && mediaMetadata . series . length ) {
2022-03-11 01:45:02 +01:00
mediaMetadata . series . forEach ( ( series ) => {
2022-03-12 02:46:32 +01:00
if ( series && ! data . series . find ( se => se . id === series . id ) ) data . series . push ( { id : series . id , name : series . name } )
2022-03-11 01:45:02 +01:00
} )
}
2022-05-31 02:26:53 +02:00
if ( mediaMetadata . genres && mediaMetadata . genres . length ) {
2022-03-11 01:45:02 +01:00
mediaMetadata . genres . forEach ( ( genre ) => {
if ( genre && ! data . genres . includes ( genre ) ) data . genres . push ( genre )
} )
}
if ( li . media . tags . length ) {
li . media . tags . forEach ( ( tag ) => {
if ( tag && ! data . tags . includes ( tag ) ) data . tags . push ( tag )
} )
}
2022-03-22 01:24:38 +01:00
if ( mediaMetadata . narrators && mediaMetadata . narrators . length ) {
2022-03-11 01:45:02 +01:00
mediaMetadata . narrators . forEach ( ( narrator ) => {
if ( narrator && ! data . narrators . includes ( narrator ) ) data . narrators . push ( narrator )
} )
}
if ( mediaMetadata . language && ! data . languages . includes ( mediaMetadata . language ) ) data . languages . push ( mediaMetadata . language )
} )
2022-04-25 00:15:41 +02:00
data . authors = naturalSort ( data . authors ) . asc ( au => au . name )
2022-03-11 01:45:02 +01:00
data . genres = naturalSort ( data . genres ) . asc ( )
data . tags = naturalSort ( data . tags ) . asc ( )
2022-04-25 00:15:41 +02:00
data . series = naturalSort ( data . series ) . asc ( se => se . name )
2022-03-11 01:45:02 +01:00
data . narrators = naturalSort ( data . narrators ) . asc ( )
data . languages = naturalSort ( data . languages ) . asc ( )
return data
} ,
2022-10-30 16:38:00 +01:00
getSeriesFromBooks ( books , allSeries , filterSeries , filterBy , user , minified = false ) {
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
if ( filterBy && user && ! filterBy . startsWith ( 'progress.' ) ) { // Series progress filters are evaluated after grouping
// If a single book in a series is filtered out then filter out the entire series
if ( ! this . checkFilterForSeriesLibraryItem ( libraryItem , filterBy ) ) {
// filter out this library item
bookSeries . forEach ( ( bookSeriesObj ) => {
// flag series to filter it out
seriesToFilterOut [ bookSeriesObj . id ] = true
delete _series [ bookSeriesObj . id ]
} )
return
}
}
2022-10-29 18:17:51 +02:00
bookSeries . forEach ( ( bookSeriesObj ) => {
const series = allSeries . find ( se => se . id === bookSeriesObj . id )
const abJson = minified ? libraryItem . toJSONMinified ( ) : libraryItem . toJSONExpanded ( )
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 ] ,
addedAt : series ? series . addedAt : 0 ,
totalDuration : isNullOrNaN ( abJson . media . duration ) ? 0 : Number ( abJson . media . duration )
2021-12-01 03:02:40 +01:00
}
2022-10-29 18:17:51 +02: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
// check progress filter
if ( filterBy && filterBy . startsWith ( 'progress.' ) && user ) {
seriesItems = seriesItems . filter ( se => this . checkSeriesProgressFilter ( se , filterBy , user ) )
}
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
} ,
2022-01-25 11:05:39 +01:00
getBooksNextInSeries ( seriesWithUserAb , limit , minified = false ) {
var incompleteSeires = seriesWithUserAb . filter ( ( series ) => series . books . some ( ( book ) => ! book . userAudiobook || ( ! book . userAudiobook . isRead && book . userAudiobook . progress == 0 ) ) )
var booksNextInSeries = [ ]
incompleteSeires . forEach ( ( series ) => {
2022-01-27 23:53:18 +01:00
var dateLastRead = series . books . filter ( ( data ) => data . userAudiobook && data . userAudiobook . isRead ) . sort ( ( a , b ) => { return b . userAudiobook . finishedAt - a . userAudiobook . finishedAt } ) [ 0 ] . userAudiobook . finishedAt
var nextUnreadBook = series . books . filter ( ( data ) => ! data . userAudiobook || ( ! data . userAudiobook . isRead && data . userAudiobook . progress == 0 ) ) [ 0 ]
2022-01-25 11:05:39 +01:00
nextUnreadBook . DateLastReadSeries = dateLastRead
booksNextInSeries . push ( nextUnreadBook )
} )
2022-01-27 23:53:18 +01:00
return booksNextInSeries . sort ( ( a , b ) => { return b . DateLastReadSeries - a . DateLastReadSeries } ) . map ( b => minified ? b . book . toJSONMinified ( ) : b . book . toJSONExpanded ( ) ) . slice ( 0 , limit )
2022-01-25 11:05:39 +01:00
} ,
2022-03-13 23:33:50 +01:00
getGenresWithCount ( libraryItems ) {
2021-12-02 02:07:03 +01:00
var genresMap = { }
2022-03-13 23:33:50 +01:00
libraryItems . forEach ( ( li ) => {
var genres = li . media . metadata . genres || [ ]
2021-12-02 02:07:03 +01:00
genres . forEach ( ( genre ) => {
if ( genresMap [ genre ] ) genresMap [ genre ] . count ++
else
genresMap [ genre ] = {
genre ,
count : 1
}
} )
} )
return Object . values ( genresMap ) . sort ( ( a , b ) => b . count - a . count )
} ,
2022-03-13 23:33:50 +01:00
getAuthorsWithCount ( libraryItems ) {
2021-12-02 02:07:03 +01:00
var authorsMap = { }
2022-03-13 23:33:50 +01:00
libraryItems . forEach ( ( li ) => {
var authors = li . media . metadata . authors || [ ]
2021-12-02 02:07:03 +01:00
authors . forEach ( ( author ) => {
2022-03-13 23:33:50 +01:00
if ( authorsMap [ author . id ] ) authorsMap [ author . id ] . count ++
2021-12-02 02:07:03 +01:00
else
2022-03-13 23:33:50 +01:00
authorsMap [ author . id ] = {
2022-04-21 01:43:39 +02:00
id : author . id ,
name : author . name ,
2021-12-02 02:07:03 +01:00
count : 1
}
} )
} )
return Object . values ( authorsMap ) . sort ( ( a , b ) => b . count - a . count )
} ,
2022-03-13 23:33:50 +01:00
getItemDurationStats ( libraryItems ) {
2022-03-26 17:59:34 +01:00
var sorted = sort ( libraryItems ) . desc ( li => li . media . duration )
2022-04-21 01:43:39 +02:00
var top10 = sorted . slice ( 0 , 10 ) . map ( li => ( { id : li . id , title : li . media . metadata . title , duration : li . media . duration } ) ) . filter ( i => i . duration > 0 )
2021-12-02 02:07:03 +01:00
var totalDuration = 0
2021-12-29 22:53:19 +01:00
var numAudioTracks = 0
2022-03-13 23:33:50 +01:00
libraryItems . forEach ( ( li ) => {
2022-03-26 17:59:34 +01:00
totalDuration += li . media . duration
numAudioTracks += li . media . numTracks
2021-12-02 02:07:03 +01:00
} )
2021-12-29 22:53:19 +01:00
return {
totalDuration ,
numAudioTracks ,
2022-03-13 23:33:50 +01:00
longestItems : top10
2021-12-29 22:53:19 +01:00
}
2021-12-02 02:07:03 +01:00
} ,
2023-02-19 22:39:28 +01:00
getItemSizeStats ( libraryItems ) {
var sorted = sort ( libraryItems ) . desc ( li => li . media . size )
var top10 = sorted . slice ( 0 , 10 ) . map ( li => ( { id : li . id , title : li . media . metadata . title , size : li . media . size } ) ) . filter ( i => i . size > 0 )
var totalSize = 0
libraryItems . forEach ( ( li ) => {
totalSize += li . media . size
} )
return {
totalSize ,
largestItems : top10
}
} ,
2022-03-13 23:33:50 +01:00
getLibraryItemsTotalSize ( libraryItems ) {
2021-12-02 02:07:03 +01:00
var totalSize = 0
2022-03-13 23:33:50 +01:00
libraryItems . forEach ( ( li ) => {
totalSize += li . media . size
2021-12-02 02:07:03 +01:00
} )
return totalSize
} ,
2022-04-10 02:44:46 +02:00
2022-10-30 15:21:12 +01:00
collapseBookSeries ( libraryItems , series , filterSeries ) {
// 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
2022-10-30 16:38:00 +01:00
. getSeriesFromBooks ( libraryItems , series , filterSeries , null , null , true )
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
2022-04-24 23:56:30 +02:00
} ,
2022-12-31 21:31:38 +01:00
buildPersonalizedShelves ( ctx , user , libraryItems , mediaType , maxEntitiesPerShelf , include ) {
2022-04-24 23:56:30 +02:00
const isPodcastLibrary = mediaType === 'podcast'
2022-12-31 21:31:38 +01:00
const includeRssFeed = include . includes ( 'rssfeed' )
2022-04-24 23:56:30 +02:00
const shelves = [
{
id : 'continue-listening' ,
label : 'Continue Listening' ,
2022-11-09 00:10:08 +01:00
labelStringKey : 'LabelContinueListening' ,
2022-04-24 23:56:30 +02:00
type : isPodcastLibrary ? 'episode' : mediaType ,
entities : [ ] ,
category : 'recentlyListened'
} ,
2022-04-30 19:24:48 +02:00
{
id : 'continue-series' ,
label : 'Continue Series' ,
2022-11-09 00:10:08 +01:00
labelStringKey : 'LabelContinueSeries' ,
2022-04-30 19:24:48 +02:00
type : mediaType ,
entities : [ ] ,
category : 'continueSeries'
} ,
2023-01-28 00:59:06 +01:00
{
id : 'episodes-recently-added' ,
label : 'Newest Episodes' ,
labelStringKey : 'LabelNewestEpisodes' ,
type : 'episode' ,
entities : [ ] ,
category : 'newestEpisodes'
} ,
2022-04-24 23:56:30 +02:00
{
id : 'recently-added' ,
label : 'Recently Added' ,
2022-11-09 00:10:08 +01:00
labelStringKey : 'LabelRecentlyAdded' ,
2022-04-24 23:56:30 +02:00
type : mediaType ,
entities : [ ] ,
category : 'newestItems'
} ,
{
id : 'recent-series' ,
label : 'Recent Series' ,
2022-11-09 00:10:08 +01:00
labelStringKey : 'LabelRecentSeries' ,
2022-04-24 23:56:30 +02:00
type : 'series' ,
entities : [ ] ,
category : 'newestSeries'
} ,
2023-01-28 00:59:06 +01:00
{
id : 'recommended' ,
label : 'Recommended' ,
labelStringKey : 'LabelRecommended' ,
type : mediaType ,
entities : [ ] ,
category : 'recommended'
} ,
{
id : 'listen-again' ,
label : 'Listen Again' ,
labelStringKey : 'LabelListenAgain' ,
type : isPodcastLibrary ? 'episode' : mediaType ,
entities : [ ] ,
category : 'recentlyFinished'
} ,
2022-04-24 23:56:30 +02:00
{
id : 'newest-authors' ,
label : 'Newest Authors' ,
2022-11-09 00:10:08 +01:00
labelStringKey : 'LabelNewestAuthors' ,
2022-04-24 23:56:30 +02:00
type : 'authors' ,
entities : [ ] ,
category : 'newestAuthors'
}
]
const categoryMap = { }
2023-01-28 00:59:06 +01:00
shelves . forEach ( ( shelf ) => {
categoryMap [ shelf . category ] = {
category : shelf . category ,
2022-04-24 23:56:30 +02:00
biggest : 0 ,
smallest : 0 ,
items : [ ]
}
} )
const seriesMap = { }
const authorMap = { }
2023-01-28 00:59:06 +01:00
// For use with recommended
const topGenresListened = { }
const topAuthorsListened = { }
const topTagsListened = { }
const notStartedBooks = [ ]
2022-04-24 23:56:30 +02:00
for ( const libraryItem of libraryItems ) {
if ( libraryItem . addedAt > categoryMap . newestItems . smallest ) {
2022-12-31 21:31:38 +01:00
const indexToPut = categoryMap . newestItems . items . findIndex ( i => libraryItem . addedAt > i . addedAt )
2022-04-24 23:56:30 +02:00
if ( indexToPut >= 0 ) {
categoryMap . newestItems . items . splice ( indexToPut , 0 , libraryItem . toJSONMinified ( ) )
} else {
categoryMap . newestItems . items . push ( libraryItem . toJSONMinified ( ) )
}
if ( categoryMap . newestItems . items . length > maxEntitiesPerShelf ) {
// Remove last item
categoryMap . newestItems . items . pop ( )
categoryMap . newestItems . smallest = categoryMap . newestItems . items [ categoryMap . newestItems . items . length - 1 ] . addedAt
}
categoryMap . newestItems . biggest = categoryMap . newestItems . items [ 0 ] . addedAt
}
2022-12-31 21:31:38 +01:00
const allItemProgress = user . getAllMediaProgressForLibraryItem ( libraryItem . id )
2022-04-24 23:56:30 +02:00
if ( libraryItem . isPodcast ) {
// Podcast categories
const podcastEpisodes = libraryItem . media . episodes || [ ]
for ( const episode of podcastEpisodes ) {
// Newest episodes
if ( episode . addedAt > categoryMap . newestEpisodes . smallest ) {
const libraryItemWithEpisode = {
... libraryItem . toJSONMinified ( ) ,
recentEpisode : episode . toJSON ( )
}
2022-12-31 21:31:38 +01:00
const indexToPut = categoryMap . newestEpisodes . items . findIndex ( i => episode . addedAt > i . recentEpisode . addedAt )
2022-04-24 23:56:30 +02:00
if ( indexToPut >= 0 ) {
categoryMap . newestEpisodes . items . splice ( indexToPut , 0 , libraryItemWithEpisode )
} else {
categoryMap . newestEpisodes . items . push ( libraryItemWithEpisode )
}
if ( categoryMap . newestEpisodes . items . length > maxEntitiesPerShelf ) {
// Remove last item
categoryMap . newestEpisodes . items . pop ( )
categoryMap . newestEpisodes . smallest = categoryMap . newestEpisodes . items [ categoryMap . newestEpisodes . items . length - 1 ] . recentEpisode . addedAt
}
categoryMap . newestEpisodes . biggest = categoryMap . newestEpisodes . items [ 0 ] . recentEpisode . addedAt
}
// Episode recently listened and finished
2022-12-31 21:31:38 +01:00
const mediaProgress = allItemProgress . find ( mp => mp . episodeId === episode . id )
2022-04-24 23:56:30 +02:00
if ( mediaProgress ) {
if ( mediaProgress . isFinished ) {
if ( mediaProgress . finishedAt > categoryMap . recentlyFinished . smallest ) { // Item belongs on shelf
const libraryItemWithEpisode = {
... libraryItem . toJSONMinified ( ) ,
recentEpisode : episode . toJSON ( ) ,
finishedAt : mediaProgress . finishedAt
}
2022-12-31 21:31:38 +01:00
const indexToPut = categoryMap . recentlyFinished . items . findIndex ( i => mediaProgress . finishedAt > i . finishedAt )
2022-04-24 23:56:30 +02:00
if ( indexToPut >= 0 ) {
categoryMap . recentlyFinished . items . splice ( indexToPut , 0 , libraryItemWithEpisode )
} else {
categoryMap . recentlyFinished . items . push ( libraryItemWithEpisode )
}
if ( categoryMap . recentlyFinished . items . length > maxEntitiesPerShelf ) {
// Remove last item
categoryMap . recentlyFinished . items . pop ( )
categoryMap . recentlyFinished . smallest = categoryMap . recentlyFinished . items [ categoryMap . recentlyFinished . items . length - 1 ] . finishedAt
}
categoryMap . recentlyFinished . biggest = categoryMap . recentlyFinished . items [ 0 ] . finishedAt
}
2022-09-29 00:57:27 +02:00
} else if ( mediaProgress . inProgress && ! mediaProgress . hideFromContinueListening ) { // Handle most recently listened
2022-04-24 23:56:30 +02:00
if ( mediaProgress . lastUpdate > categoryMap . recentlyListened . smallest ) { // Item belongs on shelf
const libraryItemWithEpisode = {
... libraryItem . toJSONMinified ( ) ,
recentEpisode : episode . toJSON ( ) ,
progressLastUpdate : mediaProgress . lastUpdate
}
2022-12-31 21:31:38 +01:00
const indexToPut = categoryMap . recentlyListened . items . findIndex ( i => mediaProgress . lastUpdate > i . progressLastUpdate )
2022-04-24 23:56:30 +02:00
if ( indexToPut >= 0 ) {
categoryMap . recentlyListened . items . splice ( indexToPut , 0 , libraryItemWithEpisode )
} else {
categoryMap . recentlyListened . items . push ( libraryItemWithEpisode )
}
if ( categoryMap . recentlyListened . items . length > maxEntitiesPerShelf ) {
// Remove last item
categoryMap . recentlyListened . items . pop ( )
categoryMap . recentlyListened . smallest = categoryMap . recentlyListened . items [ categoryMap . recentlyListened . items . length - 1 ] . progressLastUpdate
}
categoryMap . recentlyListened . biggest = categoryMap . recentlyListened . items [ 0 ] . progressLastUpdate
}
}
}
}
2022-05-31 02:26:53 +02:00
} else if ( libraryItem . isBook ) {
2022-04-24 23:56:30 +02:00
// Book categories
2023-01-28 00:59:06 +01:00
const mediaProgress = allItemProgress . length ? allItemProgress [ 0 ] : null
// Used for recommended. Tally up most listened to authors/genres/tags
if ( mediaProgress && ( mediaProgress . inProgress || mediaProgress . isFinished ) ) {
libraryItem . media . metadata . authors . forEach ( ( author ) => {
topAuthorsListened [ author . id ] = ( topAuthorsListened [ author . id ] || 0 ) + 1
} )
libraryItem . media . metadata . genres . forEach ( ( genre ) => {
topGenresListened [ genre ] = ( topGenresListened [ genre ] || 0 ) + 1
} )
libraryItem . media . tags . forEach ( ( tag ) => {
topTagsListened [ tag ] = ( topTagsListened [ tag ] || 0 ) + 1
} )
} else {
// Insert in random position to add randomization to equal weighted items
notStartedBooks . splice ( Math . floor ( Math . random ( ) * ( notStartedBooks . length + 1 ) ) , 0 , libraryItem )
}
2022-04-24 23:56:30 +02:00
// Newest series
if ( libraryItem . media . metadata . series . length ) {
for ( const librarySeries of libraryItem . media . metadata . series ) {
2023-01-28 00:59:06 +01:00
2022-05-01 22:31:07 +02:00
const bookInProgress = mediaProgress && ( mediaProgress . inProgress || mediaProgress . isFinished )
2023-01-10 21:50:33 +01:00
const bookActive = mediaProgress && mediaProgress . inProgress && ! mediaProgress . isFinished
2022-04-30 19:24:48 +02:00
const libraryItemJson = libraryItem . toJSONMinified ( )
libraryItemJson . seriesSequence = librarySeries . sequence
2022-04-24 23:56:30 +02:00
2022-09-29 00:12:27 +02:00
const hideFromContinueListening = user . checkShouldHideSeriesFromContinueListening ( librarySeries . id )
2022-04-24 23:56:30 +02:00
if ( ! seriesMap [ librarySeries . id ] ) {
2022-12-31 21:31:38 +01:00
const seriesObj = ctx . db . series . find ( se => se . id === librarySeries . id )
2022-09-29 00:12:27 +02:00
if ( seriesObj ) {
2022-12-31 21:31:38 +01:00
const series = {
2022-04-24 23:56:30 +02:00
... seriesObj . toJSON ( ) ,
2022-04-30 19:24:48 +02:00
books : [ libraryItemJson ] ,
inProgress : bookInProgress ,
2023-01-10 21:50:33 +01:00
hasActiveBook : bookActive ,
2022-09-29 00:12:27 +02:00
hideFromContinueListening ,
2022-04-30 19:24:48 +02:00
bookInProgressLastUpdate : bookInProgress ? mediaProgress . lastUpdate : null ,
2022-05-20 22:55:03 +02:00
firstBookUnread : bookInProgress ? null : libraryItemJson
2022-04-24 23:56:30 +02:00
}
2022-04-30 19:24:48 +02:00
seriesMap [ librarySeries . id ] = series
2022-04-24 23:56:30 +02:00
if ( series . addedAt > categoryMap . newestSeries . smallest ) {
2022-12-31 21:31:38 +01:00
const indexToPut = categoryMap . newestSeries . items . findIndex ( i => series . addedAt > i . addedAt )
2022-04-24 23:56:30 +02:00
if ( indexToPut >= 0 ) {
categoryMap . newestSeries . items . splice ( indexToPut , 0 , series )
} else {
categoryMap . newestSeries . items . push ( series )
}
// Max series is 5
if ( categoryMap . newestSeries . items . length > 5 ) {
categoryMap . newestSeries . items . pop ( )
categoryMap . newestSeries . smallest = categoryMap . newestSeries . items [ categoryMap . newestSeries . items . length - 1 ] . addedAt
}
categoryMap . newestSeries . biggest = categoryMap . newestSeries . items [ 0 ] . addedAt
}
}
} else {
// series already in map - add book
seriesMap [ librarySeries . id ] . books . push ( libraryItemJson )
2022-04-30 19:24:48 +02:00
if ( bookInProgress ) { // Update if this series is in progress
seriesMap [ librarySeries . id ] . inProgress = true
2022-05-20 01:09:26 +02:00
2022-08-02 23:41:52 +02:00
if ( seriesMap [ librarySeries . id ] . bookInProgressLastUpdate < mediaProgress . lastUpdate ) {
2022-04-30 19:24:48 +02:00
seriesMap [ librarySeries . id ] . bookInProgressLastUpdate = mediaProgress . lastUpdate
}
2022-05-20 01:09:26 +02:00
} else if ( ! seriesMap [ librarySeries . id ] . firstBookUnread ) {
seriesMap [ librarySeries . id ] . firstBookUnread = libraryItemJson
} else if ( libraryItemJson . seriesSequence ) {
// If current firstBookUnread has a series sequence greater than this series sequence, then update firstBookUnread
const firstBookUnreadSequence = seriesMap [ librarySeries . id ] . firstBookUnread . seriesSequence
if ( ! firstBookUnreadSequence || String ( firstBookUnreadSequence ) . localeCompare ( String ( librarySeries . sequence ) , undefined , { sensitivity : 'base' , numeric : true } ) > 0 ) {
seriesMap [ librarySeries . id ] . firstBookUnread = libraryItemJson
}
2022-04-30 19:24:48 +02:00
}
2023-01-10 21:50:33 +01:00
// Update if series has an active (progress < 100%) book
if ( bookActive ) {
seriesMap [ librarySeries . id ] . hasActiveBook = true
}
2022-04-24 23:56:30 +02:00
}
}
}
// Newest authors
if ( libraryItem . media . metadata . authors . length ) {
for ( const libraryAuthor of libraryItem . media . metadata . authors ) {
if ( ! authorMap [ libraryAuthor . id ] ) {
2022-12-31 21:31:38 +01:00
const authorObj = ctx . db . authors . find ( au => au . id === libraryAuthor . id )
2022-04-24 23:56:30 +02:00
if ( authorObj ) {
2022-12-31 21:31:38 +01:00
const author = {
2022-04-24 23:56:30 +02:00
... authorObj . toJSON ( ) ,
numBooks : 1
}
if ( author . addedAt > categoryMap . newestAuthors . smallest ) {
2022-12-31 21:31:38 +01:00
const indexToPut = categoryMap . newestAuthors . items . findIndex ( i => author . addedAt > i . addedAt )
2022-04-24 23:56:30 +02:00
if ( indexToPut >= 0 ) {
categoryMap . newestAuthors . items . splice ( indexToPut , 0 , author )
} else {
categoryMap . newestAuthors . items . push ( author )
}
// Max authors is 10
if ( categoryMap . newestAuthors . items . length > 10 ) {
categoryMap . newestAuthors . items . pop ( )
categoryMap . newestAuthors . smallest = categoryMap . newestAuthors . items [ categoryMap . newestAuthors . items . length - 1 ] . addedAt
}
categoryMap . newestAuthors . biggest = categoryMap . newestAuthors . items [ 0 ] . addedAt
}
authorMap [ libraryAuthor . id ] = author
}
} else {
authorMap [ libraryAuthor . id ] . numBooks ++
}
}
}
// Book listening and finished
if ( mediaProgress ) {
// Handle most recently finished
if ( mediaProgress . isFinished ) {
if ( mediaProgress . finishedAt > categoryMap . recentlyFinished . smallest ) { // Item belongs on shelf
const libraryItemObj = {
... libraryItem . toJSONMinified ( ) ,
finishedAt : mediaProgress . finishedAt
}
2023-01-28 00:59:06 +01:00
const indexToPut = categoryMap . recentlyFinished . items . findIndex ( i => mediaProgress . finishedAt > i . finishedAt )
2022-04-24 23:56:30 +02:00
if ( indexToPut >= 0 ) {
categoryMap . recentlyFinished . items . splice ( indexToPut , 0 , libraryItemObj )
} else {
categoryMap . recentlyFinished . items . push ( libraryItemObj )
}
if ( categoryMap . recentlyFinished . items . length > maxEntitiesPerShelf ) {
// Remove last item
categoryMap . recentlyFinished . items . pop ( )
categoryMap . recentlyFinished . smallest = categoryMap . recentlyFinished . items [ categoryMap . recentlyFinished . items . length - 1 ] . finishedAt
}
categoryMap . recentlyFinished . biggest = categoryMap . recentlyFinished . items [ 0 ] . finishedAt
}
2022-09-29 00:45:39 +02:00
} else if ( mediaProgress . inProgress && ! mediaProgress . hideFromContinueListening ) { // Handle most recently listened
2022-04-24 23:56:30 +02:00
if ( mediaProgress . lastUpdate > categoryMap . recentlyListened . smallest ) { // Item belongs on shelf
const libraryItemObj = {
... libraryItem . toJSONMinified ( ) ,
progressLastUpdate : mediaProgress . lastUpdate
}
2023-01-28 00:59:06 +01:00
const indexToPut = categoryMap . recentlyListened . items . findIndex ( i => mediaProgress . lastUpdate > i . progressLastUpdate )
2022-04-24 23:56:30 +02:00
if ( indexToPut >= 0 ) {
categoryMap . recentlyListened . items . splice ( indexToPut , 0 , libraryItemObj )
} else { // Should only happen when array is < max
categoryMap . recentlyListened . items . push ( libraryItemObj )
}
if ( categoryMap . recentlyListened . items . length > maxEntitiesPerShelf ) {
// Remove last item
categoryMap . recentlyListened . items . pop ( )
categoryMap . recentlyListened . smallest = categoryMap . recentlyListened . items [ categoryMap . recentlyListened . items . length - 1 ] . progressLastUpdate
}
categoryMap . recentlyListened . biggest = categoryMap . recentlyListened . items [ 0 ] . progressLastUpdate
}
}
}
}
}
2022-04-30 19:24:48 +02:00
// For Continue Series - Find next book in series for series that are in progress
for ( const seriesId in seriesMap ) {
2023-01-28 00:59:06 +01:00
seriesMap [ seriesId ] . books = naturalSort ( seriesMap [ seriesId ] . books ) . asc ( li => li . seriesSequence )
2022-04-30 19:24:48 +02:00
2023-01-28 00:59:06 +01:00
if ( seriesMap [ seriesId ] . inProgress && ! seriesMap [ seriesId ] . hideFromContinueListening ) {
2023-01-10 21:50:33 +01:00
// take the first book unread with the smallest series sequence
// unless the user is already listening to a book from this series
const hasActiveBook = seriesMap [ seriesId ] . hasActiveBook
2022-05-20 01:09:26 +02:00
const nextBookInSeries = seriesMap [ seriesId ] . firstBookUnread
2022-04-30 19:24:48 +02:00
2023-01-10 21:50:33 +01:00
if ( ! hasActiveBook && nextBookInSeries ) {
2022-04-30 19:24:48 +02:00
const bookForContinueSeries = {
... nextBookInSeries ,
prevBookInProgressLastUpdate : seriesMap [ seriesId ] . bookInProgressLastUpdate
}
bookForContinueSeries . media . metadata . series = {
id : seriesId ,
name : seriesMap [ seriesId ] . name ,
sequence : nextBookInSeries . seriesSequence
}
const indexToPut = categoryMap . continueSeries . items . findIndex ( i => i . prevBookInProgressLastUpdate < bookForContinueSeries . prevBookInProgressLastUpdate )
2023-01-13 00:50:04 +01:00
if ( ! categoryMap . continueSeries . items . find ( book => book . id === bookForContinueSeries . id ) ) {
if ( indexToPut >= 0 ) {
categoryMap . continueSeries . items . splice ( indexToPut , 0 , bookForContinueSeries )
} else if ( categoryMap . continueSeries . items . length < 10 ) { // Max 10 books
categoryMap . continueSeries . items . push ( bookForContinueSeries )
}
2022-04-30 19:24:48 +02:00
}
}
}
}
2023-01-28 00:59:06 +01:00
// For recommended
if ( ! isPodcastLibrary && notStartedBooks . length ) {
const genresCount = Object . values ( topGenresListened ) . reduce ( ( a , b ) => a + b , 0 )
const authorsCount = Object . values ( topAuthorsListened ) . reduce ( ( a , b ) => a + b , 0 )
const tagsCount = Object . values ( topTagsListened ) . reduce ( ( a , b ) => a + b , 0 )
for ( const libraryItem of notStartedBooks ) {
// dont include books in an unfinished series and books that are not first in an unstarted series
let shouldContinue = ! libraryItem . media . metadata . series . length
libraryItem . media . metadata . series . forEach ( ( se ) => {
if ( seriesMap [ se . id ] ) {
if ( seriesMap [ se . id ] . inProgress ) {
shouldContinue = false
return
} else if ( seriesMap [ se . id ] . books [ 0 ] . id === libraryItem . id ) {
shouldContinue = true
}
}
} )
if ( ! shouldContinue ) {
continue ;
}
let totalWeight = 0
if ( authorsCount > 0 ) {
libraryItem . media . metadata . authors . forEach ( ( author ) => {
if ( topAuthorsListened [ author . id ] ) {
totalWeight += topAuthorsListened [ author . id ] / authorsCount
}
} )
}
if ( genresCount > 0 ) {
libraryItem . media . metadata . genres . forEach ( ( genre ) => {
if ( topGenresListened [ genre ] ) {
totalWeight += topGenresListened [ genre ] / genresCount
}
} )
}
if ( tagsCount > 0 ) {
libraryItem . media . tags . forEach ( ( tag ) => {
if ( topTagsListened [ tag ] ) {
totalWeight += topTagsListened [ tag ] / tagsCount
}
} )
}
if ( ! categoryMap . recommended . smallest || totalWeight > categoryMap . recommended . smallest ) {
const libraryItemObj = {
... libraryItem . toJSONMinified ( ) ,
weight : totalWeight
}
const indexToPut = categoryMap . recommended . items . findIndex ( i => totalWeight > i . weight )
if ( indexToPut >= 0 ) {
categoryMap . recommended . items . splice ( indexToPut , 0 , libraryItemObj )
} else {
categoryMap . recommended . items . push ( libraryItemObj )
}
if ( categoryMap . recommended . items . length > maxEntitiesPerShelf ) {
categoryMap . recommended . items . pop ( )
categoryMap . recommended . smallest = categoryMap . recommended . items [ categoryMap . recommended . items . length - 1 ] . weight
}
}
}
}
2022-04-24 23:56:30 +02:00
// Sort series books by sequence
if ( categoryMap . newestSeries . items . length ) {
for ( const seriesItem of categoryMap . newestSeries . items ) {
seriesItem . books = naturalSort ( seriesItem . books ) . asc ( li => li . seriesSequence )
}
}
2022-12-31 21:31:38 +01:00
const categoriesWithItems = Object . values ( categoryMap ) . filter ( cat => cat . items . length )
2022-04-24 23:56:30 +02:00
return categoriesWithItems . map ( cat => {
2022-12-31 21:31:38 +01:00
const shelf = shelves . find ( s => s . category === cat . category )
2022-04-24 23:56:30 +02:00
shelf . entities = cat . items
2022-12-31 21:31:38 +01:00
// Add rssFeed to entities if query string "include=rssfeed" was on request
if ( includeRssFeed ) {
if ( shelf . type === 'book' || shelf . type === 'podcast' ) {
shelf . entities = shelf . entities . map ( ( item ) => {
item . rssFeed = ctx . rssFeedManager . findFeedForEntityId ( item . id ) ? . toJSONMinified ( ) || null
return item
} )
2022-12-31 23:58:19 +01:00
} else if ( shelf . type === 'series' ) {
shelf . entities = shelf . entities . map ( ( series ) => {
series . rssFeed = ctx . rssFeedManager . findFeedForEntityId ( series . id ) ? . toJSONMinified ( ) || null
return series
} )
2022-12-31 21:31:38 +01:00
}
}
2022-04-24 23:56:30 +02:00
return shelf
} )
2023-01-03 01:02:04 +01:00
} ,
groupMusicLibraryItemsIntoAlbums ( libraryItems ) {
const albums = { }
libraryItems . forEach ( ( li ) => {
const albumTitle = li . media . metadata . album
const albumArtist = li . media . metadata . albumArtist
if ( albumTitle && ! albums [ albumTitle ] ) {
albums [ albumTitle ] = {
title : albumTitle ,
artist : albumArtist ,
libraryItemId : li . media . coverPath ? li . id : null ,
numTracks : 1
}
} else if ( albumTitle && albums [ albumTitle ] . artist === albumArtist ) {
if ( ! albums [ albumTitle ] . libraryItemId && li . media . coverPath ) albums [ albumTitle ] . libraryItemId = li . id
albums [ albumTitle ] . numTracks ++
} else {
if ( albumTitle ) {
Logger . warn ( ` Music track " ${ li . media . metadata . title } " with album " ${ albumTitle } " has a different album artist then another track in the same album. This track album artist is " ${ albumArtist } " but the album artist is already set to " ${ albums [ albumTitle ] . artist } " ` )
}
if ( ! albums [ '_none_' ] ) albums [ '_none_' ] = { title : 'No Album' , artist : 'Various Artists' , libraryItemId : null , numTracks : 0 }
albums [ '_none_' ] . numTracks ++
}
} )
return Object . values ( albums )
2022-04-10 02:44:46 +02:00
}
2023-02-19 22:39:28 +01:00
}