Skip to content

Commit

Permalink
Add:User stats API for year stats
Browse files Browse the repository at this point in the history
  • Loading branch information
advplyr committed Dec 19, 2023
1 parent f33b011 commit 7391b4d
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 1 deletion.
7 changes: 6 additions & 1 deletion server/Database.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,16 @@ class Database {
return this.models.feed
}

/** @type {typeof import('./models/Feed')} */
/** @type {typeof import('./models/FeedEpisode')} */
get feedEpisodeModel() {
return this.models.feedEpisode
}

/** @type {typeof import('./models/PlaybackSession')} */
get playbackSessionModel() {
return this.models.playbackSession
}

/**
* Check if db file exists
* @returns {boolean}
Expand Down
16 changes: 16 additions & 0 deletions server/controllers/MeController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
const { sort } = require('../libs/fastSort')
const { toNumber } = require('../utils/index')
const userStats = require('../utils/queries/userStats')

class MeController {
constructor() { }
Expand Down Expand Up @@ -333,5 +334,20 @@ class MeController {
}
res.json(req.user.toJSONForBrowser())
}

/**
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async getStatsForYear(req, res) {
const year = Number(req.params.year)
if (isNaN(year) || year < 2000 || year > 9999) {
Logger.error(`[MeController] Invalid year "${year}"`)
return res.status(400).send('Invalid year')
}
const data = await userStats.getStatsForYear(req.user.id, year)
res.json(data)
}
}
module.exports = new MeController()
1 change: 1 addition & 0 deletions server/routers/ApiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ class ApiRouter {
this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this))
this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this))
this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this))
this.router.get('/me/year/:year/stats', MeController.getStatsForYear.bind(this))

//
// Backup Routes
Expand Down
178 changes: 178 additions & 0 deletions server/utils/queries/userStats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
const Sequelize = require('sequelize')
const Database = require('../../Database')
const PlaybackSession = require('../../models/PlaybackSession')
const MediaProgress = require('../../models/MediaProgress')
const { elapsedPretty } = require('../index')

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`
}
}
})
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,
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 = {}

listeningSessions.forEach((ls) => {
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

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
}
})

totalListeningTime = Math.round(totalListeningTime)
totalBookListeningTime = Math.round(totalBookListeningTime)
totalPodcastListeningTime = Math.round(totalPodcastListeningTime)

let mostListenedAuthor = null
for (const authorName in authorListeningMap) {
if (!mostListenedAuthor?.time || authorListeningMap[authorName] > mostListenedAuthor.time) {
mostListenedAuthor = {
time: Math.round(authorListeningMap[authorName]),
pretty: elapsedPretty(Math.round(authorListeningMap[authorName])),
name: authorName
}
}
}
let mostListenedNarrator = null
for (const narrator in narratorListeningMap) {
if (!mostListenedNarrator?.time || narratorListeningMap[narrator] > mostListenedNarrator.time) {
mostListenedNarrator = {
time: Math.round(narratorListeningMap[narrator]),
pretty: elapsedPretty(Math.round(narratorListeningMap[narrator])),
name: narrator
}
}
}
let mostListenedGenre = null
for (const genre in genreListeningMap) {
if (!mostListenedGenre?.time || genreListeningMap[genre] > mostListenedGenre.time) {
mostListenedGenre = {
time: Math.round(genreListeningMap[genre]),
pretty: elapsedPretty(Math.round(genreListeningMap[genre])),
name: genre
}
}
}
let mostListenedMonth = null
for (const month in monthListeningMap) {
if (!mostListenedMonth?.time || monthListeningMap[month] > mostListenedMonth.time) {
mostListenedMonth = {
month: Number(month),
time: Math.round(monthListeningMap[month]),
pretty: elapsedPretty(Math.round(monthListeningMap[month]))
}
}
}

const bookProgresses = await this.getBookMediaProgressFinishedForYear(userId, year)

const numBooksFinished = bookProgresses.length
let longestAudiobookFinished = null
bookProgresses.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),
durationPretty: elapsedPretty(Math.round(mediaProgress.duration)),
finishedAt: mediaProgress.finishedAt
}
}
})

return {
totalListeningSessions: listeningSessions.length,
totalListeningTime,
totalListeningTimePretty: elapsedPretty(totalListeningTime),
totalBookListeningTime,
totalBookListeningTimePretty: elapsedPretty(totalBookListeningTime),
totalPodcastListeningTime,
totalPodcastListeningTimePretty: elapsedPretty(totalPodcastListeningTime),
mostListenedAuthor,
mostListenedNarrator,
mostListenedGenre,
mostListenedMonth,
numBooksFinished,
longestAudiobookFinished
}
}
}

0 comments on commit 7391b4d

Please sign in to comment.