Skip to content

Commit

Permalink
Update API media progress endpoints to use new user model. Merge book…
Browse files Browse the repository at this point in the history
… & episode endpoints
  • Loading branch information
advplyr committed Aug 11, 2024
1 parent 68ef3a0 commit 9cd92c7
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 108 deletions.
4 changes: 2 additions & 2 deletions client/components/tables/podcast/LazyEpisodeRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export default {
toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${this.title}" as finished?`,
message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`,
callback: (confirmed) => {
if (confirmed) {
this.toggleFinished(true)
Expand Down Expand Up @@ -233,4 +233,4 @@ export default {
},
mounted() {}
}
</script>
</script>
3 changes: 2 additions & 1 deletion client/components/tables/podcast/LazyEpisodesTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export default {
message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished,
callback: (confirmed) => {
if (confirmed) {
this.batchUpdateEpisodesFinished(this.episodesSorted, newIsFinished)
this.batchUpdateEpisodesFinished(this.episodesCopy, newIsFinished)
}
},
type: 'yesNo'
Expand Down Expand Up @@ -305,6 +305,7 @@ export default {
this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished)
},
batchUpdateEpisodesFinished(episodes, newIsFinished) {
if (!episodes.length) return
this.processing = true
const updateProgressPayloads = episodes.map((episode) => {
Expand Down
5 changes: 0 additions & 5 deletions server/Database.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,6 @@ class Database {
return this.models.mediaProgress.upsertFromOld(oldMediaProgress)
}

removeMediaProgress(mediaProgressId) {
if (!this.sequelize) return false
return this.models.mediaProgress.removeById(mediaProgressId)
}

updateBulkBooks(oldBooks) {
if (!this.sequelize) return false
return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook)))
Expand Down
146 changes: 88 additions & 58 deletions server/controllers/MeController.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
const { Request, Response } = require('express')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
const { sort } = require('../libs/fastSort')
const { toNumber } = require('../utils/index')
const userStats = require('../utils/queries/userStats')

/**
* @typedef RequestUserObjects
* @property {import('../models/User')} userNew
* @property {import('../objects/user/User')} user
*
* @typedef {Request & RequestUserObjects} RequestWithUser
*
*/

class MeController {
constructor() {}

/**
* GET: /api/me
*
* @param {RequestWithUser} req
* @param {Response} res
*/
getCurrentUser(req, res) {
res.json(req.user.toJSONForBrowser())
res.json(req.userNew.toOldJSONForBrowser())
}

// GET: api/me/listening-sessions
/**
* GET: /api/me/listening-sessions
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getListeningSessions(req, res) {
var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id)
const listeningSessions = await this.getUserListeningSessionsHelper(req.userNew.id)

const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
const page = toNumber(req.query.page, 0)
Expand All @@ -38,8 +59,8 @@ class MeController {
*
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {RequestWithUser} req
* @param {Response} res
*/
async getItemListeningSessions(req, res) {
const libraryItem = await Database.libraryItemModel.findByPk(req.params.libraryItemId)
Expand All @@ -51,7 +72,7 @@ class MeController {
}

const mediaItemId = episode?.id || libraryItem.mediaId
let listeningSessions = await this.getUserItemListeningSessionsHelper(req.user.id, mediaItemId)
let listeningSessions = await this.getUserItemListeningSessionsHelper(req.userNew.id, mediaItemId)

const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
const page = toNumber(req.query.page, 0)
Expand All @@ -70,102 +91,111 @@ class MeController {
res.json(payload)
}

// GET: api/me/listening-stats
/**
* GET: /api/me/listening-stats
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getListeningStats(req, res) {
const listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
const listeningStats = await this.getUserListeningStatsHelpers(req.userNew.id)
res.json(listeningStats)
}

// GET: api/me/progress/:id/:episodeId?
/**
* GET: /api/me/progress/:id/:episodeId?
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getMediaProgress(req, res) {
const mediaProgress = req.user.getMediaProgress(req.params.id, req.params.episodeId || null)
const mediaProgress = req.userNew.getOldMediaProgress(req.params.id, req.params.episodeId || null)
if (!mediaProgress) {
return res.sendStatus(404)
}
res.json(mediaProgress)
}

// DELETE: api/me/progress/:id
/**
* DELETE: /api/me/progress/:id
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async removeMediaProgress(req, res) {
if (!req.user.removeMediaProgress(req.params.id)) {
return res.sendStatus(200)
}
await Database.removeMediaProgress(req.params.id)
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.sendStatus(200)
}
await Database.mediaProgressModel.removeById(req.params.id)
req.userNew.mediaProgresses = req.userNew.mediaProgresses.filter((mp) => mp.id !== req.params.id)

// PATCH: api/me/progress/:id
async createUpdateMediaProgress(req, res) {
const libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
if (!libraryItem) {
return res.status(404).send('Item not found')
}

if (req.user.createUpdateMediaProgress(libraryItem, req.body)) {
const mediaProgress = req.user.getMediaProgress(libraryItem.id)
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser())
res.sendStatus(200)
}

// PATCH: api/me/progress/:id/:episodeId
async createUpdateEpisodeMediaProgress(req, res) {
const episodeId = req.params.episodeId
const libraryItem = await Database.libraryItemModel.getOldById(req.params.id)
if (!libraryItem) {
return res.status(404).send('Item not found')
/**
* PATCH: /api/me/progress/:libraryItemId/:episodeId?
* TODO: Update to use mediaItemId and mediaItemType
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async createUpdateMediaProgress(req, res) {
const progressUpdatePayload = {
...req.body,
libraryItemId: req.params.libraryItemId,
episodeId: req.params.episodeId
}
if (!libraryItem.media.episodes.find((ep) => ep.id === episodeId)) {
Logger.error(`[MeController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
return res.status(404).send('Episode not found')
const mediaProgressResponse = await req.userNew.createUpdateMediaProgressFromPayload(progressUpdatePayload)
if (mediaProgressResponse.error) {
return res.status(mediaProgressResponse.statusCode || 400).send(mediaProgressResponse.error)
}

if (req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)) {
const mediaProgress = req.user.getMediaProgress(libraryItem.id, episodeId)
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser())
res.sendStatus(200)
}

// PATCH: api/me/progress/batch/update
/**
* PATCH: /api/me/progress/batch/update
* TODO: Update to use mediaItemId and mediaItemType
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async batchUpdateMediaProgress(req, res) {
const itemProgressPayloads = req.body
if (!itemProgressPayloads?.length) {
return res.status(400).send('Missing request payload')
}

let shouldUpdate = false
let hasUpdated = false
for (const itemProgress of itemProgressPayloads) {
const libraryItem = await Database.libraryItemModel.getOldById(itemProgress.libraryItemId)
if (libraryItem) {
if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) {
const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId)
if (mediaProgress) await Database.upsertMediaProgress(mediaProgress)
shouldUpdate = true
}
const mediaProgressResponse = await req.userNew.createUpdateMediaProgressFromPayload(itemProgress)
if (mediaProgressResponse.error) {
Logger.error(`[MeController] batchUpdateMediaProgress: ${mediaProgressResponse.error}`)
continue
} else {
Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`)
hasUpdated = true
}
}

if (shouldUpdate) {
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
if (hasUpdated) {
SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser())
}

res.sendStatus(200)
}

// POST: api/me/item/:id/bookmark
/**
* POST: /api/me/item/:id/bookmark
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async createBookmark(req, res) {
if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404)

const { time, title } = req.body
const bookmark = req.user.createBookmark(req.params.id, time, title)
await Database.updateUser(req.user)
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.user.toJSONForBrowser())
res.json(bookmark)
}

Expand Down
55 changes: 55 additions & 0 deletions server/models/MediaProgress.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,61 @@ class MediaProgress extends Model {
finishedAt: this.finishedAt?.valueOf() || null
}
}

/**
* Apply update to media progress
*
* @param {Object} progress
* @returns {Promise<MediaProgress>}
*/
applyProgressUpdate(progressPayload) {
if (!this.extraData) this.extraData = {}
if (progressPayload.isFinished !== undefined) {
if (progressPayload.isFinished && !this.isFinished) {
this.finishedAt = Date.now()
this.extraData.progress = 1
this.changed('extraData', true)
delete progressPayload.finishedAt
} else if (!progressPayload.isFinished && this.isFinished) {
this.finishedAt = null
this.extraData.progress = 0
this.currentTime = 0
this.changed('extraData', true)
delete progressPayload.finishedAt
delete progressPayload.currentTime
}
} else if (!isNaN(progressPayload.progress) && progressPayload.progress !== this.progress) {
// Old model stored progress on object
this.extraData.progress = Math.min(1, Math.max(0, progressPayload.progress))
this.changed('extraData', true)
}

this.set(progressPayload)

// Reset hideFromContinueListening if the progress has changed
if (this.changed('currentTime') && !progressPayload.hideFromContinueListening) {
this.hideFromContinueListening = false
}

const timeRemaining = this.duration - this.currentTime
// Set to finished if time remaining is less than 5 seconds
if (!this.isFinished && this.duration && timeRemaining < 5) {
this.isFinished = true
this.finishedAt = this.finishedAt || Date.now()
this.extraData.progress = 1
this.changed('extraData', true)
} else if (this.isFinished && this.changed('currentTime') && this.currentTime < this.duration) {
this.isFinished = false
this.finishedAt = null
}

// For local sync
if (progressPayload.lastUpdate) {
this.updatedAt = progressPayload.lastUpdate
}

return this.save()
}
}

module.exports = MediaProgress
Loading

0 comments on commit 9cd92c7

Please sign in to comment.