diff --git a/client/components/stats/YearInReview.vue b/client/components/stats/YearInReview.vue index 104392b4..3fef3a8c 100644 --- a/client/components/stats/YearInReview.vue +++ b/client/components/stats/YearInReview.vue @@ -1,24 +1,34 @@ \ No newline at end of file diff --git a/client/components/stats/YearInReviewServer.vue b/client/components/stats/YearInReviewServer.vue index 0d1fa8aa..3aeddfaf 100644 --- a/client/components/stats/YearInReviewServer.vue +++ b/client/components/stats/YearInReviewServer.vue @@ -1,24 +1,34 @@ \ No newline at end of file diff --git a/client/pages/config/stats.vue b/client/pages/config/stats.vue index 96581714..05d0550f 100644 --- a/client/pages/config/stats.vue +++ b/client/pages/config/stats.vue @@ -1,6 +1,9 @@ @@ -81,9 +78,7 @@ export default { return { listeningStats: null, windowWidth: 0, - showYearInReview: false, - processingYearInReview: false, - processingYearInReviewAlt: false + showYearInReviewBanner: false } }, watch: { @@ -126,22 +121,17 @@ export default { } }, methods: { - clickShowYearInReview() { - if (this.showYearInReview) { - this.$refs.yearInReview.refresh() - - if (this.$refs.yearInReviewAlt) { - this.$refs.yearInReviewAlt.refresh() - } - } else { - this.showYearInReview = true - } - }, async init() { this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => { console.error('Failed to load listening sesions', err) return [] }) + + let month = new Date().getMonth() + // January and December show year in review banner + if (month === 11 || month === 0) { + this.showYearInReviewBanner = true + } } }, mounted() { diff --git a/server/utils/queries/adminStats.js b/server/utils/queries/adminStats.js index 66a31e8d..f1d64f47 100644 --- a/server/utils/queries/adminStats.js +++ b/server/utils/queries/adminStats.js @@ -88,12 +88,53 @@ module.exports = { const numAuthorsAdded = await this.getNumAuthorsAddedForYear(year) + let authorListeningMap = {} + let narratorListeningMap = {} + let genreListeningMap = {} + const listeningSessions = await this.getListeningSessionsForYear(year) let totalListeningTime = 0 - for (const listeningSession of listeningSessions) { - totalListeningTime += (listeningSession.timeListening || 0) + for (const ls of listeningSessions) { + totalListeningTime += (ls.timeListening || 0) + + const authors = ls.mediaMetadata.authors || [] + authors.forEach((au) => { + if (!authorListeningMap[au.name]) authorListeningMap[au.name] = 0 + authorListeningMap[au.name] += (ls.timeListening || 0) + }) + + const narrators = ls.mediaMetadata.narrators || [] + narrators.forEach((narrator) => { + if (!narratorListeningMap[narrator]) narratorListeningMap[narrator] = 0 + narratorListeningMap[narrator] += (ls.timeListening || 0) + }) + + // 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] += (ls.timeListening || 0) + }) } + 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 topNarrators = null + topNarrators = Object.keys(narratorListeningMap).map(narratorName => ({ + name: narratorName, + time: Math.round(narratorListeningMap[narratorName]) + })).sort((a, b) => b.time - a.time).slice(0, 3) + + 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) + // 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: { @@ -112,7 +153,10 @@ module.exports = { totalBooksSize: totalStatResults?.totalSize || 0, totalBooksDuration: totalStatResults?.totalDuration || 0, totalListeningTime, - numBooks: totalStatResults?.totalItems || 0 + numBooks: totalStatResults?.totalItems || 0, + topAuthors, + topNarrators, + topGenres } } } diff --git a/server/utils/queries/userStats.js b/server/utils/queries/userStats.js index 0f997789..6fd5d506 100644 --- a/server/utils/queries/userStats.js +++ b/server/utils/queries/userStats.js @@ -52,12 +52,14 @@ module.exports = { }, include: { model: Database.bookModel, + attributes: ['id', 'title', 'coverPath'], include: { model: Database.libraryItemModel, attributes: ['id', 'mediaId', 'mediaType'] }, required: true - } + }, + order: Database.sequelize.random() }) return progresses }, @@ -69,6 +71,7 @@ module.exports = { async getStatsForYear(user, year) { const userId = user.id const listeningSessions = await this.getUserListeningSessionsForYear(userId, year) + const bookProgressesFinished = await this.getBookMediaProgressFinishedForYear(userId, year) let totalBookListeningTime = 0 let totalPodcastListeningTime = 0 @@ -79,11 +82,33 @@ module.exports = { let narratorListeningMap = {} let monthListeningMap = {} let bookListeningMap = {} - const booksWithCovers = [] + 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) && booksWithCovers.length < 25 && await fsExtra.pathExists(ls.mediaItem.coverPath)) { + 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) } @@ -162,21 +187,6 @@ module.exports = { } } - const bookProgressesFinished = await this.getBookMediaProgressFinishedForYear(userId, year) - - const numBooksFinished = bookProgressesFinished.length - let longestAudiobookFinished = null - bookProgressesFinished.forEach((mediaProgress) => { - 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, @@ -189,7 +199,8 @@ module.exports = { numBooksFinished, numBooksListened: Object.keys(bookListeningMap).length, longestAudiobookFinished, - booksWithCovers + booksWithCovers, + finishedBooksWithCovers } } }