2023-12-20 00:19:33 +01:00
|
|
|
const Sequelize = require('sequelize')
|
|
|
|
const Database = require('../../Database')
|
|
|
|
const PlaybackSession = require('../../models/PlaybackSession')
|
|
|
|
const MediaProgress = require('../../models/MediaProgress')
|
2023-12-21 00:18:21 +01:00
|
|
|
const fsExtra = require('../../libs/fsExtra')
|
2023-12-20 00:19:33 +01:00
|
|
|
|
|
|
|
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`
|
2023-12-21 00:18:21 +01:00
|
|
|
},
|
|
|
|
timeListening: {
|
|
|
|
[Sequelize.Op.gt]: 5
|
2023-12-20 00:19:33 +01:00
|
|
|
}
|
2023-12-21 00:18:21 +01:00
|
|
|
},
|
|
|
|
include: {
|
|
|
|
model: Database.bookModel,
|
|
|
|
attributes: ['id', 'coverPath'],
|
|
|
|
include: {
|
|
|
|
model: Database.libraryItemModel,
|
|
|
|
attributes: ['id', 'mediaId', 'mediaType']
|
|
|
|
},
|
|
|
|
required: false
|
|
|
|
},
|
|
|
|
order: Database.sequelize.random()
|
2023-12-20 00:19:33 +01:00
|
|
|
})
|
|
|
|
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,
|
2023-12-21 00:18:21 +01:00
|
|
|
include: {
|
|
|
|
model: Database.libraryItemModel,
|
|
|
|
attributes: ['id', 'mediaId', 'mediaType']
|
|
|
|
},
|
2023-12-20 00:19:33 +01:00
|
|
|
required: true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return progresses
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} userId
|
|
|
|
* @param {number} year YYYY
|
|
|
|
*/
|
|
|
|
async getStatsForYear(userId, year) {
|
|
|
|
const listeningSessions = await this.getUserListeningSessionsForYear(userId, year)
|
|
|
|
|
|
|
|
let totalBookListeningTime = 0
|
|
|
|
let totalPodcastListeningTime = 0
|
|
|
|
let totalListeningTime = 0
|
|
|
|
|
|
|
|
let authorListeningMap = {}
|
|
|
|
let genreListeningMap = {}
|
|
|
|
let narratorListeningMap = {}
|
|
|
|
let monthListeningMap = {}
|
2023-12-21 00:18:21 +01:00
|
|
|
let bookListeningMap = {}
|
|
|
|
const booksWithCovers = []
|
|
|
|
|
|
|
|
for (const ls of listeningSessions) {
|
|
|
|
// Grab first 16 that have a cover
|
|
|
|
if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 16 && await fsExtra.pathExists(ls.mediaItem.coverPath)) {
|
|
|
|
booksWithCovers.push(ls.mediaItem.libraryItem.id)
|
|
|
|
}
|
2023-12-20 00:19:33 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-12-21 00:18:21 +01:00
|
|
|
if (ls.displayTitle && !bookListeningMap[ls.displayTitle]) {
|
|
|
|
bookListeningMap[ls.displayTitle] = listeningSessionListeningTime
|
|
|
|
} else if (ls.displayTitle) {
|
|
|
|
bookListeningMap[ls.displayTitle] += listeningSessionListeningTime
|
|
|
|
}
|
|
|
|
|
2023-12-20 00:19:33 +01:00
|
|
|
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.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book'))
|
|
|
|
genres.forEach((genre) => {
|
|
|
|
if (!genreListeningMap[genre]) genreListeningMap[genre] = 0
|
|
|
|
genreListeningMap[genre] += listeningSessionListeningTime
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
totalPodcastListeningTime += listeningSessionListeningTime
|
|
|
|
}
|
2023-12-21 00:18:21 +01:00
|
|
|
}
|
2023-12-20 00:19:33 +01:00
|
|
|
|
|
|
|
totalListeningTime = Math.round(totalListeningTime)
|
|
|
|
totalBookListeningTime = Math.round(totalBookListeningTime)
|
|
|
|
totalPodcastListeningTime = Math.round(totalPodcastListeningTime)
|
|
|
|
|
2023-12-21 00:18:21 +01:00
|
|
|
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)
|
|
|
|
|
2023-12-20 00:19:33 +01:00
|
|
|
let mostListenedNarrator = null
|
|
|
|
for (const narrator in narratorListeningMap) {
|
|
|
|
if (!mostListenedNarrator?.time || narratorListeningMap[narrator] > mostListenedNarrator.time) {
|
|
|
|
mostListenedNarrator = {
|
|
|
|
time: Math.round(narratorListeningMap[narrator]),
|
|
|
|
name: narrator
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-12-21 00:18:21 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2023-12-20 00:19:33 +01:00
|
|
|
let mostListenedMonth = null
|
|
|
|
for (const month in monthListeningMap) {
|
|
|
|
if (!mostListenedMonth?.time || monthListeningMap[month] > mostListenedMonth.time) {
|
|
|
|
mostListenedMonth = {
|
|
|
|
month: Number(month),
|
2023-12-21 00:18:21 +01:00
|
|
|
time: Math.round(monthListeningMap[month])
|
2023-12-20 00:19:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-21 00:18:21 +01:00
|
|
|
const bookProgressesFinished = await this.getBookMediaProgressFinishedForYear(userId, year)
|
2023-12-20 00:19:33 +01:00
|
|
|
|
2023-12-21 00:18:21 +01:00
|
|
|
const numBooksFinished = bookProgressesFinished.length
|
2023-12-20 00:19:33 +01:00
|
|
|
let longestAudiobookFinished = null
|
2023-12-21 00:18:21 +01:00
|
|
|
bookProgressesFinished.forEach((mediaProgress) => {
|
2023-12-20 00:19:33 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
totalListeningSessions: listeningSessions.length,
|
|
|
|
totalListeningTime,
|
|
|
|
totalBookListeningTime,
|
|
|
|
totalPodcastListeningTime,
|
2023-12-21 00:18:21 +01:00
|
|
|
topAuthors,
|
|
|
|
topGenres,
|
2023-12-20 00:19:33 +01:00
|
|
|
mostListenedNarrator,
|
|
|
|
mostListenedMonth,
|
|
|
|
numBooksFinished,
|
2023-12-21 00:18:21 +01:00
|
|
|
numBooksListened: Object.keys(bookListeningMap).length,
|
|
|
|
longestAudiobookFinished,
|
|
|
|
booksWithCovers
|
2023-12-20 00:19:33 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|