mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-28 09:12:25 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			212 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			212 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const Sequelize = require('sequelize')
 | |
| const Database = require('../../Database')
 | |
| const PlaybackSession = require('../../models/PlaybackSession')
 | |
| const MediaProgress = require('../../models/MediaProgress')
 | |
| const fsExtra = require('../../libs/fsExtra')
 | |
| 
 | |
| module.exports = {
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} userId
 | |
|    * @param {number} year YYYY
 | |
|    * @returns {Promise<PlaybackSession[]>}
 | |
|    */
 | |
|   async getUserListeningSessionsForYear(userId, year) {
 | |
|     const sessions = await Database.playbackSessionModel.findAll({
 | |
|       where: {
 | |
|         userId,
 | |
|         createdAt: {
 | |
|           [Sequelize.Op.gte]: `${year}-01-01`,
 | |
|           [Sequelize.Op.lt]: `${year + 1}-01-01`
 | |
|         }
 | |
|       },
 | |
|       include: {
 | |
|         model: Database.bookModel,
 | |
|         attributes: ['id', 'coverPath'],
 | |
|         include: {
 | |
|           model: Database.libraryItemModel,
 | |
|           attributes: ['id', 'mediaId', 'mediaType']
 | |
|         },
 | |
|         required: false
 | |
|       },
 | |
|       order: Database.sequelize.random()
 | |
|     })
 | |
|     return sessions
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    *
 | |
|    * @param {string} userId
 | |
|    * @param {number} year YYYY
 | |
|    * @returns {Promise<MediaProgress[]>}
 | |
|    */
 | |
|   async getBookMediaProgressFinishedForYear(userId, year) {
 | |
|     const progresses = await Database.mediaProgressModel.findAll({
 | |
|       where: {
 | |
|         userId,
 | |
|         mediaItemType: 'book',
 | |
|         finishedAt: {
 | |
|           [Sequelize.Op.gte]: `${year}-01-01`,
 | |
|           [Sequelize.Op.lt]: `${year + 1}-01-01`
 | |
|         }
 | |
|       },
 | |
|       include: {
 | |
|         model: Database.bookModel,
 | |
|         attributes: ['id', 'title', 'coverPath'],
 | |
|         include: {
 | |
|           model: Database.libraryItemModel,
 | |
|           attributes: ['id', 'mediaId', 'mediaType']
 | |
|         },
 | |
|         required: true
 | |
|       },
 | |
|       order: Database.sequelize.random()
 | |
|     })
 | |
|     return progresses
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * @param {string} userId
 | |
|    * @param {number} year YYYY
 | |
|    */
 | |
|   async getStatsForYear(userId, year) {
 | |
|     const listeningSessions = await this.getUserListeningSessionsForYear(userId, year)
 | |
|     const bookProgressesFinished = await this.getBookMediaProgressFinishedForYear(userId, year)
 | |
| 
 | |
|     let totalBookListeningTime = 0
 | |
|     let totalPodcastListeningTime = 0
 | |
|     let totalListeningTime = 0
 | |
| 
 | |
|     let authorListeningMap = {}
 | |
|     let genreListeningMap = {}
 | |
|     let narratorListeningMap = {}
 | |
|     let monthListeningMap = {}
 | |
|     let bookListeningMap = {}
 | |
| 
 | |
|     const booksWithCovers = []
 | |
|     const finishedBooksWithCovers = []
 | |
| 
 | |
|     // Get finished book stats
 | |
|     const numBooksFinished = bookProgressesFinished.length
 | |
|     let longestAudiobookFinished = null
 | |
|     for (const mediaProgress of bookProgressesFinished) {
 | |
|       // Grab first 5 that have a cover
 | |
|       if (mediaProgress.mediaItem?.coverPath && !finishedBooksWithCovers.includes(mediaProgress.mediaItem.libraryItem.id) && finishedBooksWithCovers.length < 5 && (await fsExtra.pathExists(mediaProgress.mediaItem.coverPath))) {
 | |
|         finishedBooksWithCovers.push(mediaProgress.mediaItem.libraryItem.id)
 | |
|       }
 | |
| 
 | |
|       if (mediaProgress.duration && (!longestAudiobookFinished?.duration || mediaProgress.duration > longestAudiobookFinished.duration)) {
 | |
|         longestAudiobookFinished = {
 | |
|           id: mediaProgress.mediaItem.id,
 | |
|           title: mediaProgress.mediaItem.title,
 | |
|           duration: Math.round(mediaProgress.duration),
 | |
|           finishedAt: mediaProgress.finishedAt
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Get listening session stats
 | |
|     for (const ls of listeningSessions) {
 | |
|       // Grab first 25 that have a cover
 | |
|       if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && !finishedBooksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 25 && (await fsExtra.pathExists(ls.mediaItem.coverPath))) {
 | |
|         booksWithCovers.push(ls.mediaItem.libraryItem.id)
 | |
|       }
 | |
| 
 | |
|       const listeningSessionListeningTime = ls.timeListening || 0
 | |
| 
 | |
|       const lsMonth = ls.createdAt.getMonth()
 | |
|       if (!monthListeningMap[lsMonth]) monthListeningMap[lsMonth] = 0
 | |
|       monthListeningMap[lsMonth] += listeningSessionListeningTime
 | |
| 
 | |
|       totalListeningTime += listeningSessionListeningTime
 | |
|       if (ls.mediaItemType === 'book') {
 | |
|         totalBookListeningTime += listeningSessionListeningTime
 | |
| 
 | |
|         if (ls.displayTitle && !bookListeningMap[ls.displayTitle]) {
 | |
|           bookListeningMap[ls.displayTitle] = listeningSessionListeningTime
 | |
|         } else if (ls.displayTitle) {
 | |
|           bookListeningMap[ls.displayTitle] += listeningSessionListeningTime
 | |
|         }
 | |
| 
 | |
|         const authors = ls.mediaMetadata.authors || []
 | |
|         authors.forEach((au) => {
 | |
|           if (!authorListeningMap[au.name]) authorListeningMap[au.name] = 0
 | |
|           authorListeningMap[au.name] += listeningSessionListeningTime
 | |
|         })
 | |
| 
 | |
|         const narrators = ls.mediaMetadata.narrators || []
 | |
|         narrators.forEach((narrator) => {
 | |
|           if (!narratorListeningMap[narrator]) narratorListeningMap[narrator] = 0
 | |
|           narratorListeningMap[narrator] += listeningSessionListeningTime
 | |
|         })
 | |
| 
 | |
|         // Filter out bad genres like "audiobook" and "audio book"
 | |
|         const genres = (ls.mediaMetadata.genres || []).filter((g) => g && !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book'))
 | |
|         genres.forEach((genre) => {
 | |
|           if (!genreListeningMap[genre]) genreListeningMap[genre] = 0
 | |
|           genreListeningMap[genre] += listeningSessionListeningTime
 | |
|         })
 | |
|       } else {
 | |
|         totalPodcastListeningTime += listeningSessionListeningTime
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     totalListeningTime = Math.round(totalListeningTime)
 | |
|     totalBookListeningTime = Math.round(totalBookListeningTime)
 | |
|     totalPodcastListeningTime = Math.round(totalPodcastListeningTime)
 | |
| 
 | |
|     let topAuthors = null
 | |
|     topAuthors = Object.keys(authorListeningMap)
 | |
|       .map((authorName) => ({
 | |
|         name: authorName,
 | |
|         time: Math.round(authorListeningMap[authorName])
 | |
|       }))
 | |
|       .sort((a, b) => b.time - a.time)
 | |
|       .slice(0, 3)
 | |
| 
 | |
|     let mostListenedNarrator = null
 | |
|     for (const narrator in narratorListeningMap) {
 | |
|       if (!mostListenedNarrator?.time || narratorListeningMap[narrator] > mostListenedNarrator.time) {
 | |
|         mostListenedNarrator = {
 | |
|           time: Math.round(narratorListeningMap[narrator]),
 | |
|           name: narrator
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let topGenres = null
 | |
|     topGenres = Object.keys(genreListeningMap)
 | |
|       .map((genre) => ({
 | |
|         genre,
 | |
|         time: Math.round(genreListeningMap[genre])
 | |
|       }))
 | |
|       .sort((a, b) => b.time - a.time)
 | |
|       .slice(0, 3)
 | |
| 
 | |
|     let mostListenedMonth = null
 | |
|     for (const month in monthListeningMap) {
 | |
|       if (!mostListenedMonth?.time || monthListeningMap[month] > mostListenedMonth.time) {
 | |
|         mostListenedMonth = {
 | |
|           month: Number(month),
 | |
|           time: Math.round(monthListeningMap[month])
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       totalListeningSessions: listeningSessions.length,
 | |
|       totalListeningTime,
 | |
|       totalBookListeningTime,
 | |
|       totalPodcastListeningTime,
 | |
|       topAuthors,
 | |
|       topGenres,
 | |
|       mostListenedNarrator,
 | |
|       mostListenedMonth,
 | |
|       numBooksFinished,
 | |
|       numBooksListened: Object.keys(bookListeningMap).length,
 | |
|       longestAudiobookFinished,
 | |
|       booksWithCovers,
 | |
|       finishedBooksWithCovers
 | |
|     }
 | |
|   }
 | |
| }
 |