diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index 9b0f3f93..2284da7f 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -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() {} } - \ No newline at end of file + diff --git a/client/components/tables/podcast/LazyEpisodesTable.vue b/client/components/tables/podcast/LazyEpisodesTable.vue index f2c6f342..27b624b7 100644 --- a/client/components/tables/podcast/LazyEpisodesTable.vue +++ b/client/components/tables/podcast/LazyEpisodesTable.vue @@ -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) => { diff --git a/server/Database.js b/server/Database.js index 2115ac09..5bae390f 100644 --- a/server/Database.js +++ b/server/Database.js @@ -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))) diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 7126d45b..d2e6f252 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -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) } diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 0ab50119..196353d8 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -182,6 +182,61 @@ class MediaProgress extends Model { finishedAt: this.finishedAt?.valueOf() || null } } + + /** + * Apply update to media progress + * + * @param {Object} progress + * @returns {Promise} + */ + 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 diff --git a/server/models/User.js b/server/models/User.js index 075276d4..bcdf9d54 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -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 diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 938c6d07..26728954 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -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 \ No newline at end of file +module.exports = User diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 98a42163..81dbc44c 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -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))