mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update API media progress endpoints to use new user model. Merge book & episode endpoints
This commit is contained in:
		
							parent
							
								
									68ef3a07a7
								
							
						
					
					
						commit
						9cd92c7b7f
					
				| @ -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) | ||||
| @ -233,4 +233,4 @@ export default { | ||||
|   }, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| </script> | ||||
|  | ||||
| @ -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' | ||||
| @ -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) => { | ||||
|  | ||||
| @ -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))) | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| const { Request, Response } = require('express') | ||||
| const Logger = require('../Logger') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const Database = require('../Database') | ||||
| @ -5,16 +6,36 @@ 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) | ||||
| @ -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) | ||||
| @ -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) | ||||
| @ -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()) | ||||
|     await Database.mediaProgressModel.removeById(req.params.id) | ||||
|     req.userNew.mediaProgresses = req.userNew.mediaProgresses.filter((mp) => mp.id !== req.params.id) | ||||
| 
 | ||||
|     SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/me/progress/:id
 | ||||
|   /** | ||||
|    * PATCH: /api/me/progress/:libraryItemId/:episodeId? | ||||
|    * TODO: Update to use mediaItemId and mediaItemType | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async createUpdateMediaProgress(req, res) { | ||||
|     const libraryItem = await Database.libraryItemModel.getOldById(req.params.id) | ||||
|     if (!libraryItem) { | ||||
|       return res.status(404).send('Item not found') | ||||
|     const progressUpdatePayload = { | ||||
|       ...req.body, | ||||
|       libraryItemId: req.params.libraryItemId, | ||||
|       episodeId: req.params.episodeId | ||||
|     } | ||||
|     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)) { | ||||
|       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') | ||||
|     } | ||||
|     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') | ||||
|     } | ||||
| 
 | ||||
|     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()) | ||||
|     } | ||||
|     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) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -3,6 +3,8 @@ const sequelize = require('sequelize') | ||||
| const Logger = require('../Logger') | ||||
| const oldUser = require('../objects/user/User') | ||||
| const SocketAuthority = require('../SocketAuthority') | ||||
| const { isNullOrNaN } = require('../utils') | ||||
| 
 | ||||
| const { DataTypes, Model } = sequelize | ||||
| 
 | ||||
| class User extends Model { | ||||
| @ -577,6 +579,116 @@ class User extends Model { | ||||
|     }) | ||||
|     return mediaProgress?.getOldMediaProgress() || null | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * TODO: Uses old model and should account for the different between ebook/audiobook progress | ||||
|    * | ||||
|    * @typedef ProgressUpdatePayload | ||||
|    * @property {string} libraryItemId | ||||
|    * @property {string} [episodeId] | ||||
|    * @property {number} [duration] | ||||
|    * @property {number} [progress] | ||||
|    * @property {number} [currentTime] | ||||
|    * @property {boolean} [isFinished] | ||||
|    * @property {boolean} [hideFromContinueListening] | ||||
|    * @property {string} [ebookLocation] | ||||
|    * @property {number} [ebookProgress] | ||||
|    * @property {string} [finishedAt] | ||||
|    * @property {number} [lastUpdate] | ||||
|    * | ||||
|    * @param {ProgressUpdatePayload} progressPayload | ||||
|    * @returns {Promise<{ mediaProgress: import('./MediaProgress'), error: [string], statusCode: [number] }>} | ||||
|    */ | ||||
|   async createUpdateMediaProgressFromPayload(progressPayload) { | ||||
|     /** @type {import('./MediaProgress')|null} */ | ||||
|     let mediaProgress = null | ||||
|     let mediaItemId = null | ||||
|     if (progressPayload.episodeId) { | ||||
|       const podcastEpisode = await this.sequelize.models.podcastEpisode.findByPk(progressPayload.episodeId, { | ||||
|         attributes: ['id', 'podcastId'], | ||||
|         include: [ | ||||
|           { | ||||
|             model: this.sequelize.models.mediaProgress, | ||||
|             where: { userId: this.id }, | ||||
|             required: false | ||||
|           }, | ||||
|           { | ||||
|             model: this.sequelize.models.podcast, | ||||
|             attributes: ['id', 'title'], | ||||
|             include: { | ||||
|               model: this.sequelize.models.libraryItem, | ||||
|               attributes: ['id'] | ||||
|             } | ||||
|           } | ||||
|         ] | ||||
|       }) | ||||
|       if (!podcastEpisode) { | ||||
|         Logger.error(`[User] createUpdateMediaProgress: episode ${progressPayload.episodeId} not found`) | ||||
|         return { | ||||
|           error: 'Episode not found', | ||||
|           statusCode: 404 | ||||
|         } | ||||
|       } | ||||
|       mediaItemId = podcastEpisode.id | ||||
|       mediaProgress = podcastEpisode.mediaProgresses?.[0] | ||||
|     } else { | ||||
|       const libraryItem = await this.sequelize.models.libraryItem.findByPk(progressPayload.libraryItemId, { | ||||
|         attributes: ['id', 'mediaId', 'mediaType'], | ||||
|         include: { | ||||
|           model: this.sequelize.models.book, | ||||
|           attributes: ['id', 'title'], | ||||
|           required: false, | ||||
|           include: { | ||||
|             model: this.sequelize.models.mediaProgress, | ||||
|             where: { userId: this.id }, | ||||
|             required: false | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|       if (!libraryItem) { | ||||
|         Logger.error(`[User] createUpdateMediaProgress: library item ${progressPayload.libraryItemId} not found`) | ||||
|         return { | ||||
|           error: 'Library item not found', | ||||
|           statusCode: 404 | ||||
|         } | ||||
|       } | ||||
|       mediaItemId = libraryItem.media.id | ||||
|       mediaProgress = libraryItem.media.mediaProgresses?.[0] | ||||
|     } | ||||
| 
 | ||||
|     if (mediaProgress) { | ||||
|       mediaProgress = await mediaProgress.applyProgressUpdate(progressPayload) | ||||
|       this.mediaProgresses = this.mediaProgresses.map((mp) => (mp.id === mediaProgress.id ? mediaProgress : mp)) | ||||
|     } else { | ||||
|       const newMediaProgressPayload = { | ||||
|         userId: this.id, | ||||
|         mediaItemId, | ||||
|         mediaItemType: progressPayload.episodeId ? 'podcastEpisode' : 'book', | ||||
|         duration: isNullOrNaN(progressPayload.duration) ? 0 : Number(progressPayload.duration), | ||||
|         currentTime: isNullOrNaN(progressPayload.currentTime) ? 0 : Number(progressPayload.currentTime), | ||||
|         isFinished: !!progressPayload.isFinished, | ||||
|         hideFromContinueListening: !!progressPayload.hideFromContinueListening, | ||||
|         ebookLocation: progressPayload.ebookLocation || null, | ||||
|         ebookProgress: isNullOrNaN(progressPayload.ebookProgress) ? 0 : Number(progressPayload.ebookProgress), | ||||
|         finishedAt: progressPayload.finishedAt || null, | ||||
|         extraData: { | ||||
|           libraryItemId: progressPayload.libraryItemId, | ||||
|           progress: isNullOrNaN(progressPayload.progress) ? 0 : Number(progressPayload.progress) | ||||
|         } | ||||
|       } | ||||
|       if (newMediaProgressPayload.isFinished) { | ||||
|         newMediaProgressPayload.finishedAt = new Date() | ||||
|         newMediaProgressPayload.extraData.progress = 1 | ||||
|       } else { | ||||
|         newMediaProgressPayload.finishedAt = null | ||||
|       } | ||||
|       mediaProgress = await this.sequelize.models.mediaProgress.create(newMediaProgressPayload) | ||||
|       this.mediaProgresses.push(mediaProgress) | ||||
|     } | ||||
|     return { | ||||
|       mediaProgress | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports = User | ||||
|  | ||||
| @ -86,9 +86,9 @@ class User { | ||||
|       pash: this.pash, | ||||
|       type: this.type, | ||||
|       token: this.token, | ||||
|       mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [], | ||||
|       mediaProgress: this.mediaProgress ? this.mediaProgress.map((li) => li.toJSON()) : [], | ||||
|       seriesHideFromContinueListening: [...this.seriesHideFromContinueListening], | ||||
|       bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [], | ||||
|       bookmarks: this.bookmarks ? this.bookmarks.map((b) => b.toJSON()) : [], | ||||
|       isActive: this.isActive, | ||||
|       isLocked: this.isLocked, | ||||
|       lastSeen: this.lastSeen, | ||||
| @ -107,10 +107,10 @@ class User { | ||||
|       username: this.username, | ||||
|       email: this.email, | ||||
|       type: this.type, | ||||
|       token: (this.type === 'root' && hideRootToken) ? '' : this.token, | ||||
|       mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [], | ||||
|       token: this.type === 'root' && hideRootToken ? '' : this.token, | ||||
|       mediaProgress: this.mediaProgress ? this.mediaProgress.map((li) => li.toJSON()) : [], | ||||
|       seriesHideFromContinueListening: [...this.seriesHideFromContinueListening], | ||||
|       bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [], | ||||
|       bookmarks: this.bookmarks ? this.bookmarks.map((b) => b.toJSON()) : [], | ||||
|       isActive: this.isActive, | ||||
|       isLocked: this.isLocked, | ||||
|       lastSeen: this.lastSeen, | ||||
| @ -133,7 +133,7 @@ class User { | ||||
|    * @returns {object} | ||||
|    */ | ||||
|   toJSONForPublic(sessions) { | ||||
|     const userSession = sessions?.find(s => s.userId === this.id) || null | ||||
|     const userSession = sessions?.find((s) => s.userId === this.id) || null | ||||
|     const session = userSession?.toJSONForClient() || null | ||||
|     return { | ||||
|       id: this.id, | ||||
| @ -157,18 +157,18 @@ class User { | ||||
| 
 | ||||
|     this.mediaProgress = [] | ||||
|     if (user.mediaProgress) { | ||||
|       this.mediaProgress = user.mediaProgress.map(li => new MediaProgress(li)).filter(lip => lip.id) | ||||
|       this.mediaProgress = user.mediaProgress.map((li) => new MediaProgress(li)).filter((lip) => lip.id) | ||||
|     } | ||||
| 
 | ||||
|     this.bookmarks = [] | ||||
|     if (user.bookmarks) { | ||||
|       this.bookmarks = user.bookmarks.filter(bm => typeof bm.libraryItemId == 'string').map(bm => new AudioBookmark(bm)) | ||||
|       this.bookmarks = user.bookmarks.filter((bm) => typeof bm.libraryItemId == 'string').map((bm) => new AudioBookmark(bm)) | ||||
|     } | ||||
| 
 | ||||
|     this.seriesHideFromContinueListening = [] | ||||
|     if (user.seriesHideFromContinueListening) this.seriesHideFromContinueListening = [...user.seriesHideFromContinueListening] | ||||
| 
 | ||||
|     this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive | ||||
|     this.isActive = user.isActive === undefined || user.type === 'root' ? true : !!user.isActive | ||||
|     this.isLocked = user.type === 'root' ? false : !!user.isLocked | ||||
|     this.lastSeen = user.lastSeen || null | ||||
|     this.createdAt = user.createdAt || Date.now() | ||||
| @ -200,7 +200,8 @@ class User { | ||||
|     const keysToCheck = ['pash', 'type', 'username', 'email', 'isActive'] | ||||
|     keysToCheck.forEach((key) => { | ||||
|       if (payload[key] !== undefined) { | ||||
|         if (key === 'isActive' || payload[key]) { // pash, type, username must evaluate to true (cannot be null or empty)
 | ||||
|         if (key === 'isActive' || payload[key]) { | ||||
|           // pash, type, username must evaluate to true (cannot be null or empty)
 | ||||
|           if (payload[key] !== this[key]) { | ||||
|             hasUpdates = true | ||||
|             this[key] = payload[key] | ||||
| @ -285,7 +286,7 @@ class User { | ||||
| 
 | ||||
|   /** | ||||
|    * Update user permissions from external JSON | ||||
|    *  | ||||
|    * | ||||
|    * @param {Object} absPermissions JSON containing user permissions | ||||
|    * @returns {boolean} true if updates were made | ||||
|    */ | ||||
| @ -294,7 +295,7 @@ class User { | ||||
|     let updatedUserPermissions = {} | ||||
| 
 | ||||
|     // Initialize all permissions to false first
 | ||||
|     Object.keys(User.permissionMapping).forEach(mappingKey => { | ||||
|     Object.keys(User.permissionMapping).forEach((mappingKey) => { | ||||
|       const userPermKey = User.permissionMapping[mappingKey] | ||||
|       if (typeof this.permissions[userPermKey] === 'boolean') { | ||||
|         updatedUserPermissions[userPermKey] = false // Default to false for boolean permissions
 | ||||
| @ -302,7 +303,7 @@ class User { | ||||
|     }) | ||||
| 
 | ||||
|     // Map the boolean permissions from absPermissions
 | ||||
|     Object.keys(absPermissions).forEach(absKey => { | ||||
|     Object.keys(absPermissions).forEach((absKey) => { | ||||
|       const userPermKey = User.permissionMapping[absKey] | ||||
|       if (!userPermKey) { | ||||
|         throw new Error(`Unexpected permission property: ${absKey}`) | ||||
| @ -326,7 +327,7 @@ class User { | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } else if (absPermissions.allowedLibraries?.length && absPermissions.allowedLibraries.join(',') !== this.librariesAccessible.join(',')) { | ||||
|       if (absPermissions.allowedLibraries.some(lid => typeof lid !== 'string')) { | ||||
|       if (absPermissions.allowedLibraries.some((lid) => typeof lid !== 'string')) { | ||||
|         throw new Error('Invalid permission property "allowedLibraries", expecting array of strings') | ||||
|       } | ||||
|       this.librariesAccessible = absPermissions.allowedLibraries | ||||
| @ -340,7 +341,7 @@ class User { | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } else if (absPermissions.allowedTags?.length && absPermissions.allowedTags.join(',') !== this.itemTagsSelected.join(',')) { | ||||
|       if (absPermissions.allowedTags.some(tag => typeof tag !== 'string')) { | ||||
|       if (absPermissions.allowedTags.some((tag) => typeof tag !== 'string')) { | ||||
|         throw new Error('Invalid permission property "allowedTags", expecting array of strings') | ||||
|       } | ||||
|       this.itemTagsSelected = absPermissions.allowedTags | ||||
| @ -350,10 +351,9 @@ class User { | ||||
|     return hasUpdates | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   /** | ||||
|    * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like  | ||||
|    *  | ||||
|    * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like | ||||
|    * | ||||
|    * @returns {string} JSON string | ||||
|    */ | ||||
|   static getSampleAbsPermissions() { | ||||
| @ -375,18 +375,18 @@ class User { | ||||
| 
 | ||||
|   /** | ||||
|    * Get first available library id for user | ||||
|    *  | ||||
|    * | ||||
|    * @param {string[]} libraryIds | ||||
|    * @returns {string|null} | ||||
|    */ | ||||
|   getDefaultLibraryId(libraryIds) { | ||||
|     // Libraries should already be in ascending display order, find first accessible
 | ||||
|     return libraryIds.find(lid => this.checkCanAccessLibrary(lid)) || null | ||||
|     return libraryIds.find((lid) => this.checkCanAccessLibrary(lid)) || null | ||||
|   } | ||||
| 
 | ||||
|   getMediaProgress(libraryItemId, episodeId = null) { | ||||
|     if (!this.mediaProgress) return null | ||||
|     return this.mediaProgress.find(lip => { | ||||
|     return this.mediaProgress.find((lip) => { | ||||
|       if (episodeId && lip.episodeId !== episodeId) return false | ||||
|       return lip.libraryItemId === libraryItemId | ||||
|     }) | ||||
| @ -394,11 +394,11 @@ class User { | ||||
| 
 | ||||
|   getAllMediaProgressForLibraryItem(libraryItemId) { | ||||
|     if (!this.mediaProgress) return [] | ||||
|     return this.mediaProgress.filter(li => li.libraryItemId === libraryItemId) | ||||
|     return this.mediaProgress.filter((li) => li.libraryItemId === libraryItemId) | ||||
|   } | ||||
| 
 | ||||
|   createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) { | ||||
|     const itemProgress = this.mediaProgress.find(li => { | ||||
|     const itemProgress = this.mediaProgress.find((li) => { | ||||
|       if (episodeId && li.episodeId !== episodeId) return false | ||||
|       return li.libraryItemId === libraryItem.id | ||||
|     }) | ||||
| @ -415,12 +415,6 @@ class User { | ||||
|     return wasUpdated | ||||
|   } | ||||
| 
 | ||||
|   removeMediaProgress(id) { | ||||
|     if (!this.mediaProgress.some(mp => mp.id === id)) return false | ||||
|     this.mediaProgress = this.mediaProgress.filter(mp => mp.id !== id) | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   checkCanAccessLibrary(libraryId) { | ||||
|     if (this.permissions.accessAllLibraries) return true | ||||
|     if (!this.librariesAccessible) return false | ||||
| @ -431,10 +425,10 @@ class User { | ||||
|     if (this.permissions.accessAllTags) return true | ||||
|     if (this.permissions.selectedTagsNotAccessible) { | ||||
|       if (!tags?.length) return true | ||||
|       return tags.every(tag => !this.itemTagsSelected.includes(tag)) | ||||
|       return tags.every((tag) => !this.itemTagsSelected.includes(tag)) | ||||
|     } | ||||
|     if (!tags?.length) return false | ||||
|     return this.itemTagsSelected.some(tag => tags.includes(tag)) | ||||
|     return this.itemTagsSelected.some((tag) => tags.includes(tag)) | ||||
|   } | ||||
| 
 | ||||
|   checkCanAccessLibraryItem(libraryItem) { | ||||
| @ -446,9 +440,9 @@ class User { | ||||
| 
 | ||||
|   /** | ||||
|    * Checks if a user can access a library item | ||||
|    * @param {string} libraryId  | ||||
|    * @param {boolean} explicit  | ||||
|    * @param {string[]} tags  | ||||
|    * @param {string} libraryId | ||||
|    * @param {boolean} explicit | ||||
|    * @param {string[]} tags | ||||
|    */ | ||||
|   checkCanAccessLibraryItemWithData(libraryId, explicit, tags) { | ||||
|     if (!this.checkCanAccessLibrary(libraryId)) return false | ||||
| @ -457,7 +451,7 @@ class User { | ||||
|   } | ||||
| 
 | ||||
|   findBookmark(libraryItemId, time) { | ||||
|     return this.bookmarks.find(bm => bm.libraryItemId === libraryItemId && bm.time == time) | ||||
|     return this.bookmarks.find((bm) => bm.libraryItemId === libraryItemId && bm.time == time) | ||||
|   } | ||||
| 
 | ||||
|   createBookmark(libraryItemId, time, title) { | ||||
| @ -484,7 +478,7 @@ class User { | ||||
|   } | ||||
| 
 | ||||
|   removeBookmark(libraryItemId, time) { | ||||
|     this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time)) | ||||
|     this.bookmarks = this.bookmarks.filter((bm) => bm.libraryItemId !== libraryItemId || bm.time !== time) | ||||
|   } | ||||
| 
 | ||||
|   checkShouldHideSeriesFromContinueListening(seriesId) { | ||||
| @ -499,12 +493,12 @@ class User { | ||||
| 
 | ||||
|   removeSeriesFromHideFromContinueListening(seriesId) { | ||||
|     if (!this.seriesHideFromContinueListening.includes(seriesId)) return false | ||||
|     this.seriesHideFromContinueListening = this.seriesHideFromContinueListening.filter(sid => sid !== seriesId) | ||||
|     this.seriesHideFromContinueListening = this.seriesHideFromContinueListening.filter((sid) => sid !== seriesId) | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   removeProgressFromContinueListening(progressId) { | ||||
|     const progress = this.mediaProgress.find(mp => mp.id === progressId) | ||||
|     const progress = this.mediaProgress.find((mp) => mp.id === progressId) | ||||
|     if (!progress) return false | ||||
|     return progress.removeFromContinueListening() | ||||
|   } | ||||
| @ -512,7 +506,7 @@ class User { | ||||
|   /** | ||||
|    * Number of podcast episodes not finished for library item | ||||
|    * Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance | ||||
|    * @param {LibraryItem|object} libraryItem  | ||||
|    * @param {LibraryItem|object} libraryItem | ||||
|    * @returns {number} | ||||
|    */ | ||||
|   getNumEpisodesIncompleteForPodcast(libraryItem) { | ||||
| @ -527,4 +521,4 @@ class User { | ||||
|     return numEpisodesIncomplete | ||||
|   } | ||||
| } | ||||
| module.exports = User | ||||
| module.exports = User | ||||
|  | ||||
| @ -176,9 +176,8 @@ class ApiRouter { | ||||
|     this.router.get('/me/progress/:id/remove-from-continue-listening', MeController.removeItemFromContinueListening.bind(this)) | ||||
|     this.router.get('/me/progress/:id/:episodeId?', MeController.getMediaProgress.bind(this)) | ||||
|     this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this)) | ||||
|     this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this)) | ||||
|     this.router.patch('/me/progress/:libraryItemId/:episodeId?', MeController.createUpdateMediaProgress.bind(this)) | ||||
|     this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this)) | ||||
|     this.router.patch('/me/progress/:id/:episodeId', MeController.createUpdateEpisodeMediaProgress.bind(this)) | ||||
|     this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this)) | ||||
|     this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this)) | ||||
|     this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this)) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user