From dd0ebdf2d8db0bed7403d2c93e8b358c26dc7893 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 2 Jan 2025 12:49:58 -0600 Subject: [PATCH] Implementing toOld functions for LibraryItem/Book/Podcast --- server/controllers/LibraryItemController.js | 245 ++++++++++--------- server/managers/PlaybackSessionManager.js | 6 +- server/models/Book.js | 256 ++++++++++++++++++-- server/models/LibraryItem.js | 189 +++++++++++---- server/models/Podcast.js | 153 ++++++++++-- server/models/PodcastEpisode.js | 86 ++++--- 6 files changed, 682 insertions(+), 253 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 17c7be83..8a5ab860 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -24,6 +24,17 @@ const ShareManager = require('../managers/ShareManager') * @property {import('../models/User')} user * * @typedef {Request & RequestUserObject} RequestWithUser + * + * @typedef RequestEntityObject + * @property {import('../models/LibraryItem')} libraryItem + * @property {Object} oldLibraryItem - To be removed + * + * @typedef {RequestWithUser & RequestEntityObject} LibraryItemControllerRequest + * + * @typedef RequestLibraryFileObject + * @property {import('../models/LibraryItem').LibraryFileObject} libraryFile + * + * @typedef {RequestWithUser & RequestEntityObject & RequestLibraryFileObject} LibraryItemControllerRequestWithFile */ class LibraryItemController { @@ -35,17 +46,17 @@ class LibraryItemController { * ?include=progress,rssfeed,downloads,share * ?expanded=1 * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async findOne(req, res) { const includeEntities = (req.query.include || '').split(',') if (req.query.expanded == 1) { - var item = req.libraryItem.toJSONExpanded() + const item = req.libraryItem.toOldJSONExpanded() // Include users media progress if (includeEntities.includes('progress')) { - var episodeId = req.query.episode || null + const episodeId = req.query.episode || null item.userMediaProgress = req.user.getOldMediaProgress(item.id, episodeId) } @@ -68,28 +79,27 @@ class LibraryItemController { return res.json(item) } - res.json(req.libraryItem) + res.json(req.libraryItem.toOldJSON()) } /** * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async update(req, res) { - var libraryItem = req.libraryItem // Item has cover and update is removing cover so purge it from cache - if (libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) { - await CacheManager.purgeCoverCache(libraryItem.id) + if (req.libraryItem.media.coverPath && req.body.media && (req.body.media.coverPath === '' || req.body.media.coverPath === null)) { + await CacheManager.purgeCoverCache(req.libraryItem.id) } - const hasUpdates = libraryItem.update(req.body) + const hasUpdates = req.oldLibraryItem.update(req.body) if (hasUpdates) { Logger.debug(`[LibraryItemController] Updated now saving`) - await Database.updateLibraryItem(libraryItem) - SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) } - res.json(libraryItem.toJSON()) + res.json(req.oldLibraryItem.toJSON()) } /** @@ -100,7 +110,7 @@ class LibraryItemController { * * @this {import('../routers/ApiRouter')} * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async delete(req, res) { @@ -111,14 +121,14 @@ class LibraryItemController { const authorIds = [] const seriesIds = [] if (req.libraryItem.isPodcast) { - mediaItemIds.push(...req.libraryItem.media.episodes.map((ep) => ep.id)) + mediaItemIds.push(...req.libraryItem.media.podcastEpisodes.map((ep) => ep.id)) } else { mediaItemIds.push(req.libraryItem.media.id) - if (req.libraryItem.media.metadata.authors?.length) { - authorIds.push(...req.libraryItem.media.metadata.authors.map((au) => au.id)) + if (req.libraryItem.media.authors?.length) { + authorIds.push(...req.libraryItem.media.authors.map((au) => au.id)) } - if (req.libraryItem.media.metadata.series?.length) { - seriesIds.push(...req.libraryItem.media.metadata.series.map((se) => se.id)) + if (req.libraryItem.media.series?.length) { + seriesIds.push(...req.libraryItem.media.series.map((se) => se.id)) } } @@ -155,7 +165,7 @@ class LibraryItemController { * GET: /api/items/:id/download * Download library item. Zip file if multiple files. * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async download(req, res) { @@ -164,7 +174,7 @@ class LibraryItemController { return res.sendStatus(403) } const libraryItemPath = req.libraryItem.path - const itemTitle = req.libraryItem.media.metadata.title + const itemTitle = req.libraryItem.media.title Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) @@ -194,11 +204,10 @@ class LibraryItemController { * * @this {import('../routers/ApiRouter')} * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async updateMedia(req, res) { - const libraryItem = req.libraryItem const mediaPayload = req.body if (mediaPayload.url) { @@ -207,44 +216,44 @@ class LibraryItemController { } // Book specific - if (libraryItem.isBook) { - await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, libraryItem.libraryId) + if (req.libraryItem.isBook) { + await this.createAuthorsAndSeriesForItemUpdate(mediaPayload, req.libraryItem.libraryId) } // Podcast specific let isPodcastAutoDownloadUpdated = false - if (libraryItem.isPodcast) { - if (mediaPayload.autoDownloadEpisodes !== undefined && libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) { + if (req.libraryItem.isPodcast) { + if (mediaPayload.autoDownloadEpisodes !== undefined && req.libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) { isPodcastAutoDownloadUpdated = true - } else if (mediaPayload.autoDownloadSchedule !== undefined && libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) { + } else if (mediaPayload.autoDownloadSchedule !== undefined && req.libraryItem.media.autoDownloadSchedule !== mediaPayload.autoDownloadSchedule) { isPodcastAutoDownloadUpdated = true } } // Book specific - Get all series being removed from this item let seriesRemoved = [] - if (libraryItem.isBook && mediaPayload.metadata?.series) { + if (req.libraryItem.isBook && mediaPayload.metadata?.series) { const seriesIdsInUpdate = mediaPayload.metadata.series?.map((se) => se.id) || [] - seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id)) + seriesRemoved = req.oldLibraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id)) } let authorsRemoved = [] - if (libraryItem.isBook && mediaPayload.metadata?.authors) { + if (req.libraryItem.isBook && mediaPayload.metadata?.authors) { const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id) - authorsRemoved = libraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id)) + authorsRemoved = req.oldLibraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id)) } - const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url + const hasUpdates = req.oldLibraryItem.media.update(mediaPayload) || mediaPayload.url if (hasUpdates) { - libraryItem.updatedAt = Date.now() + req.oldLibraryItem.updatedAt = Date.now() if (isPodcastAutoDownloadUpdated) { - this.cronManager.checkUpdatePodcastCron(libraryItem) + this.cronManager.checkUpdatePodcastCron(req.oldLibraryItem) } - Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`) - await Database.updateLibraryItem(libraryItem) - SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) + Logger.debug(`[LibraryItemController] Updated library item media ${req.oldLibraryItem.media.metadata.title}`) + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) if (authorsRemoved.length) { // Check remove empty authors @@ -259,14 +268,14 @@ class LibraryItemController { } res.json({ updated: hasUpdates, - libraryItem + libraryItem: req.oldLibraryItem }) } /** * POST: /api/items/:id/cover * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res * @param {boolean} [updateAndReturnJson=true] */ @@ -276,15 +285,13 @@ class LibraryItemController { return res.sendStatus(403) } - let libraryItem = req.libraryItem - let result = null if (req.body?.url) { Logger.debug(`[LibraryItemController] Requesting download cover from url "${req.body.url}"`) - result = await CoverManager.downloadCoverFromUrl(libraryItem, req.body.url) + result = await CoverManager.downloadCoverFromUrl(req.oldLibraryItem, req.body.url) } else if (req.files?.cover) { Logger.debug(`[LibraryItemController] Handling uploaded cover`) - result = await CoverManager.uploadCover(libraryItem, req.files.cover) + result = await CoverManager.uploadCover(req.oldLibraryItem, req.files.cover) } else { return res.status(400).send('Invalid request no file or url') } @@ -296,8 +303,8 @@ class LibraryItemController { } if (updateAndReturnJson) { - await Database.updateLibraryItem(libraryItem) - SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) res.json({ success: true, cover: result.cover @@ -308,22 +315,21 @@ class LibraryItemController { /** * PATCH: /api/items/:id/cover * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async updateCover(req, res) { - const libraryItem = req.libraryItem if (!req.body.cover) { return res.status(400).send('Invalid request no cover path') } - const validationResult = await CoverManager.validateCoverPath(req.body.cover, libraryItem) + const validationResult = await CoverManager.validateCoverPath(req.body.cover, req.oldLibraryItem) if (validationResult.error) { return res.status(500).send(validationResult.error) } if (validationResult.updated) { - await Database.updateLibraryItem(libraryItem) - SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) } res.json({ success: true, @@ -334,17 +340,15 @@ class LibraryItemController { /** * DELETE: /api/items/:id/cover * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async removeCover(req, res) { - var libraryItem = req.libraryItem - - if (libraryItem.media.coverPath) { - libraryItem.updateMediaCover('') - await CacheManager.purgeCoverCache(libraryItem.id) - await Database.updateLibraryItem(libraryItem) - SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) + if (req.libraryItem.media.coverPath) { + req.oldLibraryItem.updateMediaCover('') + await CacheManager.purgeCoverCache(req.libraryItem.id) + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) } res.sendStatus(200) @@ -353,7 +357,7 @@ class LibraryItemController { /** * GET: /api/items/:id/cover * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async getCover(req, res) { @@ -395,11 +399,11 @@ class LibraryItemController { * * @this {import('../routers/ApiRouter')} * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ startPlaybackSession(req, res) { - if (!req.libraryItem.media.numTracks) { + if (!req.libraryItem.hasAudioTracks) { Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) return res.sendStatus(404) } @@ -412,18 +416,18 @@ class LibraryItemController { * * @this {import('../routers/ApiRouter')} * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ startEpisodePlaybackSession(req, res) { - var libraryItem = req.libraryItem - if (!libraryItem.media.numTracks) { - Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${libraryItem.id}`) - return res.sendStatus(404) + if (!req.libraryItem.isPodcast) { + Logger.error(`[LibraryItemController] startEpisodePlaybackSession invalid media type ${req.libraryItem.id}`) + return res.sendStatus(400) } - var episodeId = req.params.episodeId - if (!libraryItem.media.episodes.find((ep) => ep.id === episodeId)) { - Logger.error(`[LibraryItemController] startPlaybackSession episode ${episodeId} not found for item ${libraryItem.id}`) + + const episodeId = req.params.episodeId + if (!req.libraryItem.media.podcastEpisodes.some((ep) => ep.id === episodeId)) { + Logger.error(`[LibraryItemController] startPlaybackSession episode ${episodeId} not found for item ${req.libraryItem.id}`) return res.sendStatus(404) } @@ -433,30 +437,34 @@ class LibraryItemController { /** * PATCH: /api/items/:id/tracks * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async updateTracks(req, res) { - var libraryItem = req.libraryItem - var orderedFileData = req.body.orderedFileData - if (!libraryItem.media.updateAudioTracks) { - Logger.error(`[LibraryItemController] updateTracks invalid media type ${libraryItem.id}`) - return res.sendStatus(500) + const orderedFileData = req.body?.orderedFileData + + if (!req.libraryItem.isBook) { + Logger.error(`[LibraryItemController] updateTracks invalid media type ${req.libraryItem.id}`) + return res.sendStatus(400) } - libraryItem.media.updateAudioTracks(orderedFileData) - await Database.updateLibraryItem(libraryItem) - SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) - res.json(libraryItem.toJSON()) + if (!Array.isArray(orderedFileData) || !orderedFileData.length) { + Logger.error(`[LibraryItemController] updateTracks invalid orderedFileData ${req.libraryItem.id}`) + return res.sendStatus(400) + } + + req.oldLibraryItem.media.updateAudioTracks(orderedFileData) + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) + res.json(req.oldLibraryItem.toJSON()) } /** * POST /api/items/:id/match * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async match(req, res) { - const libraryItem = req.libraryItem const reqBody = req.body || {} const options = {} @@ -473,7 +481,7 @@ class LibraryItemController { options.overrideDetails = !!reqBody.overrideDetails } - var matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options) + var matchResult = await Scanner.quickMatchLibraryItem(this, req.oldLibraryItem, options) res.json(matchResult) } @@ -741,7 +749,7 @@ class LibraryItemController { /** * POST: /api/items/:id/scan * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async scan(req, res) { @@ -765,7 +773,7 @@ class LibraryItemController { /** * GET: /api/items/:id/metadata-object * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ getMetadataObject(req, res) { @@ -774,18 +782,18 @@ class LibraryItemController { return res.sendStatus(403) } - if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) { + if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.includedAudioFiles.length) { Logger.error(`[LibraryItemController] Invalid library item`) return res.sendStatus(500) } - res.json(this.audioMetadataManager.getMetadataObjectForApi(req.libraryItem)) + res.json(this.audioMetadataManager.getMetadataObjectForApi(req.oldLibraryItem)) } /** * POST: /api/items/:id/chapters * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async updateMediaChapters(req, res) { @@ -794,7 +802,7 @@ class LibraryItemController { return res.sendStatus(403) } - if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) { + if (req.libraryItem.isMissing || !req.libraryItem.isBook || !req.libraryItem.media.includedAudioFiles.length) { Logger.error(`[LibraryItemController] Invalid library item`) return res.sendStatus(500) } @@ -805,10 +813,10 @@ class LibraryItemController { } const chapters = req.body.chapters || [] - const wasUpdated = req.libraryItem.media.updateChapters(chapters) + const wasUpdated = req.oldLibraryItem.media.updateChapters(chapters) if (wasUpdated) { - await Database.updateLibraryItem(req.libraryItem) - SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) } res.json({ @@ -821,7 +829,7 @@ class LibraryItemController { * GET: /api/items/:id/ffprobe/:fileid * FFProbe JSON result from audio file * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequestWithFile} req * @param {Response} res */ async getFFprobeData(req, res) { @@ -834,7 +842,7 @@ class LibraryItemController { return res.sendStatus(400) } - const audioFile = req.libraryItem.media.findFileWithInode(req.params.fileid) + const audioFile = req.oldLibraryItem.media.findFileWithInode(req.params.fileid) if (!audioFile) { Logger.error(`[LibraryItemController] Audio file not found with inode value ${req.params.fileid}`) return res.sendStatus(404) @@ -847,7 +855,7 @@ class LibraryItemController { /** * GET api/items/:id/file/:fileid * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequestWithFile} req * @param {Response} res */ async getLibraryFile(req, res) { @@ -870,7 +878,7 @@ class LibraryItemController { /** * DELETE api/items/:id/file/:fileid * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequestWithFile} req * @param {Response} res */ async deleteLibraryFile(req, res) { @@ -881,17 +889,17 @@ class LibraryItemController { await fs.remove(libraryFile.metadata.path).catch((error) => { Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error) }) - req.libraryItem.removeLibraryFile(req.params.fileid) + req.oldLibraryItem.removeLibraryFile(req.params.fileid) - if (req.libraryItem.media.removeFileWithInode(req.params.fileid)) { + if (req.oldLibraryItem.media.removeFileWithInode(req.params.fileid)) { // If book has no more media files then mark it as missing - if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) { - req.libraryItem.setMissing() + if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaFiles) { + req.oldLibraryItem.setMissing() } } - req.libraryItem.updatedAt = Date.now() - await Database.updateLibraryItem(req.libraryItem) - SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) + req.oldLibraryItem.updatedAt = Date.now() + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) res.sendStatus(200) } @@ -899,7 +907,7 @@ class LibraryItemController { * GET api/items/:id/file/:fileid/download * Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequestWithFile} req * @param {Response} res */ async downloadLibraryFile(req, res) { @@ -911,7 +919,7 @@ class LibraryItemController { return res.sendStatus(403) } - Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`) + Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.title}" file at "${libraryFile.metadata.path}"`) if (global.XAccel) { const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path) @@ -947,28 +955,28 @@ class LibraryItemController { * fileid is only required when reading a supplementary ebook * when no fileid is passed in the primary ebook will be returned * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async getEBookFile(req, res) { let ebookFile = null if (req.params.fileid) { - ebookFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) + ebookFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) if (!ebookFile?.isEBookFile) { Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`) return res.status(400).send('Invalid ebook file id') } } else { - ebookFile = req.libraryItem.media.ebookFile + ebookFile = req.oldLibraryItem.media.ebookFile } if (!ebookFile) { - Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.metadata.title}"`) + Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.title}"`) return res.sendStatus(404) } const ebookFilePath = ebookFile.metadata.path - Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" ebook at "${ebookFilePath}"`) + Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.title}" ebook at "${ebookFilePath}"`) if (global.XAccel) { const encodedURI = encodeUriPath(global.XAccel + ebookFilePath) @@ -991,11 +999,11 @@ class LibraryItemController { * if an ebook file is the primary ebook, then it will be changed to supplementary * if an ebook file is supplementary, then it will be changed to primary * - * @param {RequestWithUser} req + * @param {LibraryItemControllerRequest} req * @param {Response} res */ async updateEbookFileStatus(req, res) { - const ebookLibraryFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) + const ebookLibraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) if (!ebookLibraryFile?.isEBookFile) { Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`) return res.status(400).send('Invalid ebook file id') @@ -1003,16 +1011,16 @@ class LibraryItemController { if (ebookLibraryFile.isSupplementary) { Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to primary`) - req.libraryItem.setPrimaryEbook(ebookLibraryFile) + req.oldLibraryItem.setPrimaryEbook(ebookLibraryFile) } else { Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`) ebookLibraryFile.isSupplementary = true - req.libraryItem.setPrimaryEbook(null) + req.oldLibraryItem.setPrimaryEbook(null) } - req.libraryItem.updatedAt = Date.now() - await Database.updateLibraryItem(req.libraryItem) - SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) + req.oldLibraryItem.updatedAt = Date.now() + await Database.updateLibraryItem(req.oldLibraryItem) + SocketAuthority.emitter('item_updated', req.oldLibraryItem.toJSONExpanded()) res.sendStatus(200) } @@ -1023,7 +1031,8 @@ class LibraryItemController { * @param {NextFunction} next */ async middleware(req, res, next) { - req.libraryItem = await Database.libraryItemModel.getOldById(req.params.id) + req.libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id) + req.oldLibraryItem = await Database.libraryItemModel.getOldLibraryItem(req.libraryItem) if (!req.libraryItem?.media) return res.sendStatus(404) // Check user can access this library item @@ -1033,7 +1042,7 @@ class LibraryItemController { // For library file routes, get the library file if (req.params.fileid) { - req.libraryFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) + req.libraryFile = req.oldLibraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) if (!req.libraryFile) { Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`) return res.sendStatus(404) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index ce43fc8c..aace3df7 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -39,7 +39,7 @@ class PlaybackSessionManager { /** * - * @param {import('../controllers/SessionController').RequestWithUser} req + * @param {import('../controllers/LibraryItemController').LibraryItemControllerRequest} req * @param {Object} [clientDeviceInfo] * @returns {Promise} */ @@ -67,14 +67,14 @@ class PlaybackSessionManager { /** * - * @param {import('../controllers/SessionController').RequestWithUser} req + * @param {import('../controllers/LibraryItemController').LibraryItemControllerRequest} req * @param {import('express').Response} res * @param {string} [episodeId] */ async startSessionRequest(req, res, episodeId) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) - const { libraryItem, body: options } = req + const { oldLibraryItem: libraryItem, body: options } = req const session = await this.startSession(req.user, deviceInfo, libraryItem, episodeId, options) res.json(session.toJSONForClient(libraryItem)) } diff --git a/server/models/Book.js b/server/models/Book.js index a904f536..8f3e1cae 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -1,5 +1,7 @@ const { DataTypes, Model } = require('sequelize') const Logger = require('../Logger') +const { getTitlePrefixAtEnd } = require('../utils') +const parseNameString = require('../utils/parsers/parseNameString') /** * @typedef EBookFileObject @@ -113,8 +115,12 @@ class Book extends Model { /** @type {Date} */ this.createdAt + // Expanded properties + /** @type {import('./Author')[]} - optional if expanded */ this.authors + /** @type {import('./Series')[]} - optional if expanded */ + this.series } static getOldBook(libraryItemExpanded) { @@ -241,32 +247,6 @@ class Book extends Model { } } - getAbsMetadataJson() { - return { - tags: this.tags || [], - chapters: this.chapters?.map((c) => ({ ...c })) || [], - title: this.title, - subtitle: this.subtitle, - authors: this.authors.map((a) => a.name), - narrators: this.narrators, - series: this.series.map((se) => { - const sequence = se.bookSeries?.sequence || '' - if (!sequence) return se.name - return `${se.name} #${sequence}` - }), - genres: this.genres || [], - publishedYear: this.publishedYear, - publishedDate: this.publishedDate, - publisher: this.publisher, - description: this.description, - isbn: this.isbn, - asin: this.asin, - language: this.language, - explicit: !!this.explicit, - abridged: !!this.abridged - } - } - /** * Initialize model * @param {import('../Database').sequelize} sequelize @@ -343,9 +323,50 @@ class Book extends Model { } return this.authors.map((au) => au.name).join(', ') } + + /** + * Comma separated array of author names in Last, First format + * Requires authors to be loaded + * + * @returns {string} + */ + get authorNameLF() { + if (this.authors === undefined) { + Logger.error(`[Book] authorNameLF: Cannot get authorNameLF because authors are not loaded`) + return '' + } + + // Last, First + if (!this.authors.length) return '' + return this.authors.map((au) => parseNameString.nameToLastFirst(au.name)).join(', ') + } + + /** + * Comma separated array of series with sequence + * Requires series to be loaded + * + * @returns {string} + */ + get seriesName() { + if (this.series === undefined) { + Logger.error(`[Book] seriesName: Cannot get seriesName because series are not loaded`) + return '' + } + + if (!this.series.length) return '' + return this.series + .map((se) => { + const sequence = se.bookSeries?.sequence || '' + if (!sequence) return se.name + return `${se.name} #${sequence}` + }) + .join(', ') + } + get includedAudioFiles() { return this.audioFiles.filter((af) => !af.exclude) } + get trackList() { let startOffset = 0 return this.includedAudioFiles.map((af) => { @@ -355,6 +376,189 @@ class Book extends Model { return track }) } + + get hasMediaFiles() { + return !!this.hasAudioTracks || !!this.ebookFile + } + + get hasAudioTracks() { + return !!this.includedAudioFiles.length + } + + /** + * Total file size of all audio files and ebook file + * + * @returns {number} + */ + get size() { + let total = 0 + this.audioFiles.forEach((af) => (total += af.metadata.size)) + if (this.ebookFile) { + total += this.ebookFile.metadata.size + } + return total + } + + getAbsMetadataJson() { + return { + tags: this.tags || [], + chapters: this.chapters?.map((c) => ({ ...c })) || [], + title: this.title, + subtitle: this.subtitle, + authors: this.authors.map((a) => a.name), + narrators: this.narrators, + series: this.series.map((se) => { + const sequence = se.bookSeries?.sequence || '' + if (!sequence) return se.name + return `${se.name} #${sequence}` + }), + genres: this.genres || [], + publishedYear: this.publishedYear, + publishedDate: this.publishedDate, + publisher: this.publisher, + description: this.description, + isbn: this.isbn, + asin: this.asin, + language: this.language, + explicit: !!this.explicit, + abridged: !!this.abridged + } + } + + /** + * Old model kept metadata in a separate object + */ + oldMetadataToJSON() { + const authors = this.authors.map((au) => ({ id: au.id, name: au.name })) + const series = this.series.map((se) => ({ id: se.id, name: se.name, sequence: se.bookSeries.sequence })) + return { + title: this.title, + subtitle: this.subtitle, + authors, + narrators: [...(this.narrators || [])], + series, + genres: [...(this.genres || [])], + publishedYear: this.publishedYear, + publishedDate: this.publishedDate, + publisher: this.publisher, + description: this.description, + isbn: this.isbn, + asin: this.asin, + language: this.language, + explicit: this.explicit, + abridged: this.abridged + } + } + + oldMetadataToJSONMinified() { + return { + title: this.title, + titleIgnorePrefix: getTitlePrefixAtEnd(this.title), + subtitle: this.subtitle, + authorName: this.authorName, + authorNameLF: this.authorNameLF, + narratorName: (this.narrators || []).join(', '), + seriesName: this.seriesName, + genres: [...(this.genres || [])], + publishedYear: this.publishedYear, + publishedDate: this.publishedDate, + publisher: this.publisher, + description: this.description, + isbn: this.isbn, + asin: this.asin, + language: this.language, + explicit: this.explicit, + abridged: this.abridged + } + } + + oldMetadataToJSONExpanded() { + const oldMetadataJSON = this.oldMetadataToJSON() + oldMetadataJSON.titleIgnorePrefix = getTitlePrefixAtEnd(this.title) + oldMetadataJSON.authorName = this.authorName + oldMetadataJSON.authorNameLF = this.authorNameLF + oldMetadataJSON.narratorName = (this.narrators || []).join(', ') + oldMetadataJSON.seriesName = this.seriesName + return oldMetadataJSON + } + + /** + * The old model stored a minified series and authors array with the book object. + * Minified series is { id, name, sequence } + * Minified author is { id, name } + * + * @param {string} libraryItemId + */ + toOldJSON(libraryItemId) { + if (!libraryItemId) { + throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`) + } + if (!this.authors) { + throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`) + } + if (!this.series) { + throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`) + } + + return { + id: this.id, + libraryItemId: libraryItemId, + metadata: this.oldMetadataToJSON(), + coverPath: this.coverPath, + tags: [...(this.tags || [])], + audioFiles: structuredClone(this.audioFiles), + chapters: structuredClone(this.chapters), + ebookFile: structuredClone(this.ebookFile) + } + } + + toOldJSONMinified() { + if (!this.authors) { + throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`) + } + if (!this.series) { + throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`) + } + + return { + id: this.id, + metadata: this.oldMetadataToJSONMinified(), + coverPath: this.coverPath, + tags: [...(this.tags || [])], + numTracks: this.trackList.length, + numAudioFiles: this.audioFiles?.length || 0, + numChapters: this.chapters?.length || 0, + duration: this.duration, + size: this.size, + ebookFormat: this.ebookFile?.ebookFormat + } + } + + toOldJSONExpanded(libraryItemId) { + if (!libraryItemId) { + throw new Error(`[Book] Cannot convert to old JSON because libraryItemId is not provided`) + } + if (!this.authors) { + throw new Error(`[Book] Cannot convert to old JSON because authors are not loaded`) + } + if (!this.series) { + throw new Error(`[Book] Cannot convert to old JSON because series are not loaded`) + } + + return { + id: this.id, + libraryItemId: libraryItemId, + metadata: this.oldMetadataToJSONExpanded(), + coverPath: this.coverPath, + tags: [...(this.tags || [])], + audioFiles: structuredClone(this.audioFiles), + chapters: structuredClone(this.chapters), + ebookFile: structuredClone(this.ebookFile), + duration: this.duration, + size: this.size, + tracks: structuredClone(this.trackList) + } + } } module.exports = Book diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 2aa41b70..412860d2 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -865,54 +865,6 @@ class LibraryItem extends Model { return libraryItem.media.coverPath } - /** - * - * @param {import('sequelize').FindOptions} options - * @returns {Promise} - */ - getMedia(options) { - if (!this.mediaType) return Promise.resolve(null) - const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}` - return this[mixinMethodName](options) - } - - /** - * - * @returns {Promise} - */ - getMediaExpanded() { - if (this.mediaType === 'podcast') { - return this.getMedia({ - include: [ - { - model: this.sequelize.models.podcastEpisode - } - ] - }) - } else { - return this.getMedia({ - include: [ - { - model: this.sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: this.sequelize.models.series, - through: { - attributes: ['sequence'] - } - } - ], - order: [ - [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], - [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] - ] - }) - } - } - /** * * @returns {Promise} @@ -1131,6 +1083,64 @@ class LibraryItem extends Model { }) } + get isBook() { + return this.mediaType === 'book' + } + get isPodcast() { + return this.mediaType === 'podcast' + } + get hasAudioTracks() { + return this.media.hasAudioTracks() + } + + /** + * + * @param {import('sequelize').FindOptions} options + * @returns {Promise} + */ + getMedia(options) { + if (!this.mediaType) return Promise.resolve(null) + const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.mediaType)}` + return this[mixinMethodName](options) + } + + /** + * + * @returns {Promise} + */ + getMediaExpanded() { + if (this.mediaType === 'podcast') { + return this.getMedia({ + include: [ + { + model: this.sequelize.models.podcastEpisode + } + ] + }) + } else { + return this.getMedia({ + include: [ + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ], + order: [ + [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'], + [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC'] + ] + }) + } + } + /** * Check if book or podcast library item has audio tracks * Requires expanded library item @@ -1148,6 +1158,89 @@ class LibraryItem extends Model { return this.media.podcastEpisodes?.length > 0 } } + + toOldJSON() { + if (!this.media) { + throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`) + } + + return { + id: this.id, + ino: this.ino, + oldLibraryItemId: this.extraData?.oldLibraryItemId || null, + libraryId: this.libraryId, + folderId: this.libraryFolderId, + path: this.path, + relPath: this.relPath, + isFile: this.isFile, + mtimeMs: this.mtime?.valueOf(), + ctimeMs: this.ctime?.valueOf(), + birthtimeMs: this.birthtime?.valueOf(), + addedAt: this.createdAt.valueOf(), + updatedAt: this.updatedAt.valueOf(), + lastScan: this.lastScan?.valueOf(), + scanVersion: this.lastScanVersion, + isMissing: !!this.isMissing, + isInvalid: !!this.isInvalid, + mediaType: this.mediaType, + media: this.media.toOldJSON(this.id), + libraryFiles: structuredClone(this.libraryFiles) + } + } + + toOldJSONMinified() { + if (!this.media) { + throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`) + } + + return { + id: this.id, + ino: this.ino, + oldLibraryItemId: this.extraData?.oldLibraryItemId || null, + libraryId: this.libraryId, + folderId: this.libraryFolderId, + path: this.path, + relPath: this.relPath, + isFile: this.isFile, + mtimeMs: this.mtime?.valueOf(), + ctimeMs: this.ctime?.valueOf(), + birthtimeMs: this.birthtime?.valueOf(), + addedAt: this.createdAt.valueOf(), + updatedAt: this.updatedAt.valueOf(), + isMissing: !!this.isMissing, + isInvalid: !!this.isInvalid, + mediaType: this.mediaType, + media: this.media.toOldJSONMinified(), + numFiles: this.libraryFiles.length, + size: this.size + } + } + + toOldJSONExpanded() { + return { + id: this.id, + ino: this.ino, + oldLibraryItemId: this.extraData?.oldLibraryItemId || null, + libraryId: this.libraryId, + folderId: this.libraryFolderId, + path: this.path, + relPath: this.relPath, + isFile: this.isFile, + mtimeMs: this.mtime?.valueOf(), + ctimeMs: this.ctime?.valueOf(), + birthtimeMs: this.birthtime?.valueOf(), + addedAt: this.createdAt.valueOf(), + updatedAt: this.updatedAt.valueOf(), + lastScan: this.lastScan?.valueOf(), + scanVersion: this.lastScanVersion, + isMissing: !!this.isMissing, + isInvalid: !!this.isInvalid, + mediaType: this.mediaType, + media: this.media.toOldJSONExpanded(this.id), + libraryFiles: structuredClone(this.libraryFiles), + size: this.size + } + } } module.exports = LibraryItem diff --git a/server/models/Podcast.js b/server/models/Podcast.js index 60f879d0..ec26e091 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -1,4 +1,5 @@ const { DataTypes, Model } = require('sequelize') +const { getTitlePrefixAtEnd } = require('../utils') /** * @typedef PodcastExpandedProperties @@ -47,6 +48,8 @@ class Podcast extends Model { this.lastEpisodeCheck /** @type {number} */ this.maxEpisodesToKeep + /** @type {number} */ + this.maxNewEpisodesToDownload /** @type {string} */ this.coverPath /** @type {string[]} */ @@ -57,6 +60,9 @@ class Podcast extends Model { this.createdAt /** @type {Date} */ this.updatedAt + + /** @type {import('./PodcastEpisode')[]} */ + this.podcastEpisodes } static getOldPodcast(libraryItemExpanded) { @@ -119,25 +125,6 @@ class Podcast extends Model { } } - getAbsMetadataJson() { - return { - tags: this.tags || [], - title: this.title, - author: this.author, - description: this.description, - releaseDate: this.releaseDate, - genres: this.genres || [], - feedURL: this.feedURL, - imageURL: this.imageURL, - itunesPageURL: this.itunesPageURL, - itunesId: this.itunesId, - itunesArtistId: this.itunesArtistId, - language: this.language, - explicit: !!this.explicit, - podcastType: this.podcastType - } - } - /** * Initialize model * @param {import('../Database').sequelize} sequelize @@ -179,6 +166,134 @@ class Podcast extends Model { } ) } + + get hasMediaFiles() { + return !!this.podcastEpisodes?.length + } + + get hasAudioTracks() { + return this.hasMediaFiles + } + + get size() { + if (!this.podcastEpisodes?.length) return 0 + return this.podcastEpisodes.reduce((total, episode) => total + episode.size, 0) + } + + getAbsMetadataJson() { + return { + tags: this.tags || [], + title: this.title, + author: this.author, + description: this.description, + releaseDate: this.releaseDate, + genres: this.genres || [], + feedURL: this.feedURL, + imageURL: this.imageURL, + itunesPageURL: this.itunesPageURL, + itunesId: this.itunesId, + itunesArtistId: this.itunesArtistId, + language: this.language, + explicit: !!this.explicit, + podcastType: this.podcastType + } + } + + /** + * Old model kept metadata in a separate object + */ + oldMetadataToJSON() { + return { + title: this.title, + author: this.author, + description: this.description, + releaseDate: this.releaseDate, + genres: [...(this.genres || [])], + feedUrl: this.feedURL, + imageUrl: this.imageURL, + itunesPageUrl: this.itunesPageURL, + itunesId: this.itunesId, + itunesArtistId: this.itunesArtistId, + explicit: this.explicit, + language: this.language, + type: this.podcastType + } + } + + oldMetadataToJSONExpanded() { + const oldMetadataJSON = this.oldMetadataToJSON() + oldMetadataJSON.titleIgnorePrefix = getTitlePrefixAtEnd(this.title) + return oldMetadataJSON + } + + /** + * The old model stored episodes with the podcast object + * + * @param {string} libraryItemId + */ + toOldJSON(libraryItemId) { + if (!libraryItemId) { + throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`) + } + if (!this.podcastEpisodes) { + throw new Error(`[Podcast] Cannot convert to old JSON because episodes are not provided`) + } + + return { + id: this.id, + libraryItemId: libraryItemId, + metadata: this.oldMetadataToJSON(), + coverPath: this.coverPath, + tags: [...(this.tags || [])], + episodes: this.podcastEpisodes.map((episode) => episode.toOldJSON(libraryItemId)), + autoDownloadEpisodes: this.autoDownloadEpisodes, + autoDownloadSchedule: this.autoDownloadSchedule, + lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null, + maxEpisodesToKeep: this.maxEpisodesToKeep, + maxNewEpisodesToDownload: this.maxNewEpisodesToDownload + } + } + + toOldJSONMinified() { + return { + id: this.id, + // Minified metadata and expanded metadata are the same + metadata: this.oldMetadataToJSONExpanded(), + coverPath: this.coverPath, + tags: [...(this.tags || [])], + numEpisodes: this.podcastEpisodes?.length || 0, + autoDownloadEpisodes: this.autoDownloadEpisodes, + autoDownloadSchedule: this.autoDownloadSchedule, + lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null, + maxEpisodesToKeep: this.maxEpisodesToKeep, + maxNewEpisodesToDownload: this.maxNewEpisodesToDownload, + size: this.size + } + } + + toOldJSONExpanded(libraryItemId) { + if (!libraryItemId) { + throw new Error(`[Podcast] Cannot convert to old JSON because libraryItemId is not provided`) + } + if (!this.podcastEpisodes) { + throw new Error(`[Podcast] Cannot convert to old JSON because episodes are not provided`) + } + + return { + id: this.id, + libraryItemId: libraryItemId, + metadata: this.oldMetadataToJSONExpanded(), + coverPath: this.coverPath, + tags: [...(this.tags || [])], + episodes: this.podcastEpisodes.map((e) => e.toOldJSONExpanded(libraryItemId)), + autoDownloadEpisodes: this.autoDownloadEpisodes, + autoDownloadSchedule: this.autoDownloadSchedule, + lastEpisodeCheck: this.lastEpisodeCheck?.valueOf() || null, + maxEpisodesToKeep: this.maxEpisodesToKeep, + maxNewEpisodesToDownload: this.maxNewEpisodesToDownload, + size: this.size + } + } } module.exports = Podcast diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js index 1fa32da7..23d237e0 100644 --- a/server/models/PodcastEpisode.js +++ b/server/models/PodcastEpisode.js @@ -53,42 +53,6 @@ class PodcastEpisode extends Model { this.updatedAt } - /** - * @param {string} libraryItemId - * @returns {oldPodcastEpisode} - */ - getOldPodcastEpisode(libraryItemId = null) { - let enclosure = null - if (this.enclosureURL) { - enclosure = { - url: this.enclosureURL, - type: this.enclosureType, - length: this.enclosureSize !== null ? String(this.enclosureSize) : null - } - } - return new oldPodcastEpisode({ - libraryItemId: libraryItemId || null, - podcastId: this.podcastId, - id: this.id, - oldEpisodeId: this.extraData?.oldEpisodeId || null, - index: this.index, - season: this.season, - episode: this.episode, - episodeType: this.episodeType, - title: this.title, - subtitle: this.subtitle, - description: this.description, - enclosure, - guid: this.extraData?.guid || null, - pubDate: this.pubDate, - chapters: this.chapters, - audioFile: this.audioFile, - publishedAt: this.publishedAt?.valueOf() || null, - addedAt: this.createdAt.valueOf(), - updatedAt: this.updatedAt.valueOf() - }) - } - static createFromOld(oldEpisode) { const podcastEpisode = this.getFromOld(oldEpisode) return this.create(podcastEpisode) @@ -184,7 +148,51 @@ class PodcastEpisode extends Model { return track } + get size() { + return this.audioFile?.metadata.size || 0 + } + + /** + * @param {string} libraryItemId + * @returns {oldPodcastEpisode} + */ + getOldPodcastEpisode(libraryItemId = null) { + let enclosure = null + if (this.enclosureURL) { + enclosure = { + url: this.enclosureURL, + type: this.enclosureType, + length: this.enclosureSize !== null ? String(this.enclosureSize) : null + } + } + return new oldPodcastEpisode({ + libraryItemId: libraryItemId || null, + podcastId: this.podcastId, + id: this.id, + oldEpisodeId: this.extraData?.oldEpisodeId || null, + index: this.index, + season: this.season, + episode: this.episode, + episodeType: this.episodeType, + title: this.title, + subtitle: this.subtitle, + description: this.description, + enclosure, + guid: this.extraData?.guid || null, + pubDate: this.pubDate, + chapters: this.chapters, + audioFile: this.audioFile, + publishedAt: this.publishedAt?.valueOf() || null, + addedAt: this.createdAt.valueOf(), + updatedAt: this.updatedAt.valueOf() + }) + } + toOldJSON(libraryItemId) { + if (!libraryItemId) { + throw new Error(`[PodcastEpisode] Cannot convert to old JSON because libraryItemId is not provided`) + } + let enclosure = null if (this.enclosureURL) { enclosure = { @@ -209,8 +217,8 @@ class PodcastEpisode extends Model { enclosure, guid: this.extraData?.guid || null, pubDate: this.pubDate, - chapters: this.chapters?.map((ch) => ({ ...ch })) || [], - audioFile: this.audioFile || null, + chapters: structuredClone(this.chapters), + audioFile: structuredClone(this.audioFile), publishedAt: this.publishedAt?.valueOf() || null, addedAt: this.createdAt.valueOf(), updatedAt: this.updatedAt.valueOf() @@ -221,7 +229,7 @@ class PodcastEpisode extends Model { const json = this.toOldJSON(libraryItemId) json.audioTrack = this.track - json.size = this.audioFile?.metadata.size || 0 + json.size = this.size json.duration = this.audioFile?.duration || 0 return json