2023-12-22 17:01:07 -06:00
const Sequelize = require ( 'sequelize' )
const Database = require ( '../../Database' )
const PlaybackSession = require ( '../../models/PlaybackSession' )
const fsExtra = require ( '../../libs/fsExtra' )
module . exports = {
/ * *
2024-12-02 17:23:25 -06:00
*
2023-12-22 17:01:07 -06:00
* @ param { number } year YYYY
* @ returns { Promise < PlaybackSession [ ] > }
* /
async getListeningSessionsForYear ( year ) {
const sessions = await Database . playbackSessionModel . findAll ( {
where : {
createdAt : {
[ Sequelize . Op . gte ] : ` ${ year } -01-01 ` ,
[ Sequelize . Op . lt ] : ` ${ year + 1 } -01-01 `
}
}
} )
return sessions
} ,
/ * *
2024-12-02 17:23:25 -06:00
*
2023-12-22 17:01:07 -06:00
* @ param { number } year YYYY
* @ returns { Promise < number > }
* /
async getNumAuthorsAddedForYear ( year ) {
const count = await Database . authorModel . count ( {
where : {
createdAt : {
[ Sequelize . Op . gte ] : ` ${ year } -01-01 ` ,
[ Sequelize . Op . lt ] : ` ${ year + 1 } -01-01 `
}
}
} )
return count
} ,
/ * *
2024-12-02 17:23:25 -06:00
*
2023-12-22 17:01:07 -06:00
* @ param { number } year YYYY
* @ returns { Promise < import ( '../../models/Book' ) [ ] > }
* /
async getBooksAddedForYear ( year ) {
const books = await Database . bookModel . findAll ( {
attributes : [ 'id' , 'title' , 'coverPath' , 'duration' , 'createdAt' ] ,
where : {
createdAt : {
[ Sequelize . Op . gte ] : ` ${ year } -01-01 ` ,
[ Sequelize . Op . lt ] : ` ${ year + 1 } -01-01 `
}
} ,
include : {
model : Database . libraryItemModel ,
attributes : [ 'id' , 'mediaId' , 'mediaType' , 'size' ] ,
required : true
} ,
order : Database . sequelize . random ( )
} )
return books
} ,
/ * *
2024-12-02 17:23:25 -06:00
*
2023-12-22 17:01:07 -06:00
* @ param { number } year YYYY
* /
async getStatsForYear ( year ) {
const booksAdded = await this . getBooksAddedForYear ( year )
let totalBooksAddedSize = 0
let totalBooksAddedDuration = 0
const booksWithCovers = [ ]
for ( const book of booksAdded ) {
// Grab first 25 that have a cover
2024-12-02 17:23:25 -06:00
if ( book . coverPath && ! booksWithCovers . includes ( book . libraryItem . id ) && booksWithCovers . length < 25 && ( await fsExtra . pathExists ( book . coverPath ) ) ) {
2023-12-22 17:01:07 -06:00
booksWithCovers . push ( book . libraryItem . id )
}
if ( book . duration && ! isNaN ( book . duration ) ) {
totalBooksAddedDuration += book . duration
}
if ( book . libraryItem . size && ! isNaN ( book . libraryItem . size ) ) {
totalBooksAddedSize += book . libraryItem . size
}
}
const numAuthorsAdded = await this . getNumAuthorsAddedForYear ( year )
2023-12-23 15:29:34 -06:00
let authorListeningMap = { }
let narratorListeningMap = { }
let genreListeningMap = { }
2023-12-22 17:01:07 -06:00
const listeningSessions = await this . getListeningSessionsForYear ( year )
let totalListeningTime = 0
2023-12-23 15:29:34 -06:00
for ( const ls of listeningSessions ) {
2024-12-02 17:23:25 -06:00
totalListeningTime += ls . timeListening || 0
2023-12-23 15:29:34 -06:00
2024-12-02 17:23:25 -06:00
const authors = ls . mediaMetadata ? . authors || [ ]
2023-12-23 15:29:34 -06:00
authors . forEach ( ( au ) => {
if ( ! authorListeningMap [ au . name ] ) authorListeningMap [ au . name ] = 0
2024-12-02 17:23:25 -06:00
authorListeningMap [ au . name ] += ls . timeListening || 0
2023-12-23 15:29:34 -06:00
} )
2024-12-02 17:23:25 -06:00
const narrators = ls . mediaMetadata ? . narrators || [ ]
2023-12-23 15:29:34 -06:00
narrators . forEach ( ( narrator ) => {
if ( ! narratorListeningMap [ narrator ] ) narratorListeningMap [ narrator ] = 0
2024-12-02 17:23:25 -06:00
narratorListeningMap [ narrator ] += ls . timeListening || 0
2023-12-23 15:29:34 -06:00
} )
// Filter out bad genres like "audiobook" and "audio book"
2024-12-02 17:23:25 -06:00
const genres = ( ls . mediaMetadata ? . genres || [ ] ) . filter ( ( g ) => g && ! g . toLowerCase ( ) . includes ( 'audiobook' ) && ! g . toLowerCase ( ) . includes ( 'audio book' ) )
2023-12-23 15:29:34 -06:00
genres . forEach ( ( genre ) => {
if ( ! genreListeningMap [ genre ] ) genreListeningMap [ genre ] = 0
2024-12-02 17:23:25 -06:00
genreListeningMap [ genre ] += ls . timeListening || 0
2023-12-23 15:29:34 -06:00
} )
2023-12-22 17:01:07 -06:00
}
2023-12-23 15:29:34 -06:00
let topAuthors = null
2024-12-02 17:23:25 -06:00
topAuthors = Object . keys ( authorListeningMap )
. map ( ( authorName ) => ( {
name : authorName ,
time : Math . round ( authorListeningMap [ authorName ] )
} ) )
. sort ( ( a , b ) => b . time - a . time )
. slice ( 0 , 3 )
2023-12-23 15:29:34 -06:00
let topNarrators = null
2024-12-02 17:23:25 -06:00
topNarrators = Object . keys ( narratorListeningMap )
. map ( ( narratorName ) => ( {
name : narratorName ,
time : Math . round ( narratorListeningMap [ narratorName ] )
} ) )
. sort ( ( a , b ) => b . time - a . time )
. slice ( 0 , 3 )
2023-12-23 15:29:34 -06:00
let topGenres = null
2024-12-02 17:23:25 -06:00
topGenres = Object . keys ( genreListeningMap )
. map ( ( genre ) => ( {
genre ,
time : Math . round ( genreListeningMap [ genre ] )
} ) )
. sort ( ( a , b ) => b . time - a . time )
. slice ( 0 , 3 )
2023-12-23 15:29:34 -06:00
2023-12-22 17:01:07 -06:00
// Stats for total books, size and duration for everything added this year or earlier
const [ totalStatResultsRow ] = await Database . sequelize . query ( ` SELECT SUM(li.size) AS totalSize, SUM(b.duration) AS totalDuration, COUNT(*) AS totalItems FROM libraryItems li, books b WHERE b.id = li.mediaId AND li.mediaType = 'book' AND li.createdAt < ":nextYear-01-01"; ` , {
replacements : {
nextYear : year + 1
}
} )
const totalStatResults = totalStatResultsRow [ 0 ]
return {
numListeningSessions : listeningSessions . length ,
numBooksAdded : booksAdded . length ,
numAuthorsAdded ,
totalBooksAddedSize ,
totalBooksAddedDuration : Math . round ( totalBooksAddedDuration ) ,
booksAddedWithCovers : booksWithCovers ,
totalBooksSize : totalStatResults ? . totalSize || 0 ,
totalBooksDuration : totalStatResults ? . totalDuration || 0 ,
totalListeningTime ,
2023-12-23 15:29:34 -06:00
numBooks : totalStatResults ? . totalItems || 0 ,
topAuthors ,
topNarrators ,
topGenres
2023-12-22 17:01:07 -06:00
}
2025-03-29 17:34:17 -05:00
} ,
/ * *
* Get total file size and number of items for books and podcasts
*
* @ typedef { Object } SizeObject
* @ property { number } totalSize
* @ property { number } numItems
*
* @ returns { Promise < { books : SizeObject , podcasts : SizeObject , total : SizeObject } } > }
* /
async getTotalSize ( ) {
const [ mediaTypeStats ] = await Database . sequelize . query ( ` SELECT li.mediaType, SUM(li.size) AS totalSize, COUNT(*) AS numItems FROM libraryItems li group by li.mediaType; ` )
const bookStats = mediaTypeStats . find ( ( m ) => m . mediaType === 'book' )
const podcastStats = mediaTypeStats . find ( ( m ) => m . mediaType === 'podcast' )
return {
books : {
totalSize : bookStats ? . totalSize || 0 ,
numItems : bookStats ? . numItems || 0
} ,
podcasts : {
totalSize : podcastStats ? . totalSize || 0 ,
numItems : podcastStats ? . numItems || 0
} ,
total : {
totalSize : ( bookStats ? . totalSize || 0 ) + ( podcastStats ? . totalSize || 0 ) ,
numItems : ( bookStats ? . numItems || 0 ) + ( podcastStats ? . numItems || 0 )
}
}
} ,
/ * *
* Get total number of audio files for books and podcasts
*
* @ returns { Promise < { numBookAudioFiles : number , numPodcastAudioFiles : number , numAudioFiles : number } > }
* /
async getNumAudioFiles ( ) {
const [ numBookAudioFilesRow ] = await Database . sequelize . query ( ` SELECT SUM(json_array_length(b.audioFiles)) AS numAudioFiles FROM books b; ` )
const numBookAudioFiles = numBookAudioFilesRow [ 0 ] ? . numAudioFiles || 0
const numPodcastAudioFiles = await Database . podcastEpisodeModel . count ( )
return {
numBookAudioFiles ,
numPodcastAudioFiles ,
numAudioFiles : numBookAudioFiles + numPodcastAudioFiles
}
2023-12-22 17:01:07 -06:00
}
}