From 12c6f2e9a5b7401d5efd548361ec5d31758981a9 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 2 Jan 2025 17:21:07 -0600 Subject: [PATCH] Update updateMedia endpoint to use new model --- server/controllers/LibraryItemController.js | 19 ++-- server/managers/CronManager.js | 9 +- server/models/Book.js | 112 +++++++++++++++++++- server/models/Podcast.js | 79 +++++++++++++- 4 files changed, 204 insertions(+), 15 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 4b9ee894..f08a6011 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -234,32 +234,27 @@ class LibraryItemController { } } - const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(req.libraryItem) - // Book specific - Get all series being removed from this item let seriesRemoved = [] if (req.libraryItem.isBook && mediaPayload.metadata?.series) { const seriesIdsInUpdate = mediaPayload.metadata.series?.map((se) => se.id) || [] - seriesRemoved = oldLibraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id)) + seriesRemoved = req.libraryItem.media.series.filter((se) => !seriesIdsInUpdate.includes(se.id)) } let authorsRemoved = [] if (req.libraryItem.isBook && mediaPayload.metadata?.authors) { const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id) - authorsRemoved = oldLibraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id)) + authorsRemoved = req.libraryItem.media.authors.filter((au) => !authorIdsInUpdate.includes(au.id)) } - const hasUpdates = oldLibraryItem.media.update(mediaPayload) || mediaPayload.url + const hasUpdates = (await req.libraryItem.media.updateFromRequest(mediaPayload)) || mediaPayload.url if (hasUpdates) { - oldLibraryItem.updatedAt = Date.now() - if (isPodcastAutoDownloadUpdated) { - this.cronManager.checkUpdatePodcastCron(oldLibraryItem) + this.cronManager.checkUpdatePodcastCron(req.libraryItem) } - Logger.debug(`[LibraryItemController] Updated library item media ${oldLibraryItem.media.metadata.title}`) - await Database.updateLibraryItem(oldLibraryItem) - SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded()) + Logger.debug(`[LibraryItemController] Updated library item media ${req.libraryItem.media.title}`) + SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) if (authorsRemoved.length) { // Check remove empty authors @@ -274,7 +269,7 @@ class LibraryItemController { } res.json({ updated: hasUpdates, - libraryItem: oldLibraryItem + libraryItem: req.libraryItem.toOldJSON() }) } diff --git a/server/managers/CronManager.js b/server/managers/CronManager.js index 7a8c9bd0..a4dbe6b4 100644 --- a/server/managers/CronManager.js +++ b/server/managers/CronManager.js @@ -215,6 +215,10 @@ class CronManager { this.podcastCrons = this.podcastCrons.filter((pc) => pc.expression !== podcastCron.expression) } + /** + * + * @param {import('../models/LibraryItem')} libraryItem - this can be the old model + */ checkUpdatePodcastCron(libraryItem) { // Remove from old cron by library item id const existingCron = this.podcastCrons.find((pc) => pc.libraryItemIds.includes(libraryItem.id)) @@ -230,7 +234,10 @@ class CronManager { const cronMatchingExpression = this.podcastCrons.find((pc) => pc.expression === libraryItem.media.autoDownloadSchedule) if (cronMatchingExpression) { cronMatchingExpression.libraryItemIds.push(libraryItem.id) - Logger.info(`[CronManager] Added podcast "${libraryItem.media.metadata.title}" to auto dl episode cron "${cronMatchingExpression.expression}"`) + + // TODO: Update after old model removed + const podcastTitle = libraryItem.media.title || libraryItem.media.metadata?.title + Logger.info(`[CronManager] Added podcast "${podcastTitle}" to auto dl episode cron "${cronMatchingExpression.expression}"`) } else { this.startPodcastCron(libraryItem.media.autoDownloadSchedule, [libraryItem.id]) } diff --git a/server/models/Book.js b/server/models/Book.js index 8f3e1cae..756a9dea 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -1,6 +1,6 @@ const { DataTypes, Model } = require('sequelize') const Logger = require('../Logger') -const { getTitlePrefixAtEnd } = require('../utils') +const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils') const parseNameString = require('../utils/parsers/parseNameString') /** @@ -425,6 +425,116 @@ class Book extends Model { } } + /** + * + * @param {Object} payload - old book object + * @returns {Promise} + */ + async updateFromRequest(payload) { + if (!payload) return false + + let hasUpdates = false + + if (payload.metadata) { + const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language'] + metadataStringKeys.forEach((key) => { + if (typeof payload.metadata[key] === 'string' && this[key] !== payload.metadata[key]) { + this[key] = payload.metadata[key] || null + + if (key === 'title') { + this.titleIgnorePrefix = getTitleIgnorePrefix(this.title) + } + + hasUpdates = true + } + }) + if (payload.metadata.explicit !== undefined && this.explicit !== !!payload.metadata.explicit) { + this.explicit = !!payload.metadata.explicit + hasUpdates = true + } + if (payload.metadata.abridged !== undefined && this.abridged !== !!payload.metadata.abridged) { + this.abridged = !!payload.metadata.abridged + hasUpdates = true + } + const arrayOfStringsKeys = ['narrators', 'genres'] + arrayOfStringsKeys.forEach((key) => { + if (Array.isArray(payload.metadata[key]) && !payload.metadata[key].some((item) => typeof item !== 'string') && JSON.stringify(this[key]) !== JSON.stringify(payload.metadata[key])) { + this[key] = payload.metadata[key] + this.changed(key, true) + hasUpdates = true + } + }) + } + + if (Array.isArray(payload.tags) && !payload.tags.some((tag) => typeof tag !== 'string') && JSON.stringify(this.tags) !== JSON.stringify(payload.tags)) { + this.tags = payload.tags + this.changed('tags', true) + hasUpdates = true + } + + // TODO: Remove support for updating audioFiles, chapters and ebookFile here + const arrayOfObjectsKeys = ['audioFiles', 'chapters'] + arrayOfObjectsKeys.forEach((key) => { + if (Array.isArray(payload[key]) && !payload[key].some((item) => typeof item !== 'object') && JSON.stringify(this[key]) !== JSON.stringify(payload[key])) { + this[key] = payload[key] + this.changed(key, true) + hasUpdates = true + } + }) + if (payload.ebookFile && JSON.stringify(this.ebookFile) !== JSON.stringify(payload.ebookFile)) { + this.ebookFile = payload.ebookFile + this.changed('ebookFile', true) + hasUpdates = true + } + + if (hasUpdates) { + Logger.debug(`[Book] "${this.title}" changed keys:`, this.changed()) + await this.save() + } + + if (Array.isArray(payload.metadata?.authors)) { + const authorsRemoved = this.authors.filter((au) => !payload.metadata.authors.some((a) => a.id === au.id)) + const newAuthors = payload.metadata.authors.filter((a) => !this.authors.some((au) => au.id === a.id)) + + for (const author of authorsRemoved) { + await this.sequelize.models.bookAuthor.removeByIds(author.id, this.id) + Logger.debug(`[Book] "${this.title}" Removed author ${author.id}`) + hasUpdates = true + } + for (const author of newAuthors) { + await this.sequelize.models.bookAuthor.create({ bookId: this.id, authorId: author.id }) + Logger.debug(`[Book] "${this.title}" Added author ${author.id}`) + hasUpdates = true + } + } + + if (Array.isArray(payload.metadata?.series)) { + const seriesRemoved = this.series.filter((se) => !payload.metadata.series.some((s) => s.id === se.id)) + const newSeries = payload.metadata.series.filter((s) => !this.series.some((se) => se.id === s.id)) + + for (const series of seriesRemoved) { + await this.sequelize.models.bookSeries.removeByIds(series.id, this.id) + Logger.debug(`[Book] "${this.title}" Removed series ${series.id}`) + hasUpdates = true + } + for (const series of newSeries) { + await this.sequelize.models.bookSeries.create({ bookId: this.id, seriesId: series.id, sequence: series.sequence }) + Logger.debug(`[Book] "${this.title}" Added series ${series.id}`) + hasUpdates = true + } + for (const series of payload.metadata.series) { + const existingSeries = this.series.find((se) => se.id === series.id) + if (existingSeries && existingSeries.bookSeries.sequence !== series.sequence) { + await existingSeries.bookSeries.update({ sequence: series.sequence }) + Logger.debug(`[Book] "${this.title}" Updated series ${series.id} sequence ${series.sequence}`) + hasUpdates = true + } + } + } + + return hasUpdates + } + /** * Old model kept metadata in a separate object */ diff --git a/server/models/Podcast.js b/server/models/Podcast.js index ec26e091..172e36a2 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -1,5 +1,6 @@ const { DataTypes, Model } = require('sequelize') -const { getTitlePrefixAtEnd } = require('../utils') +const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils') +const Logger = require('../Logger') /** * @typedef PodcastExpandedProperties @@ -199,6 +200,82 @@ class Podcast extends Model { } } + /** + * + * @param {Object} payload - Old podcast object + * @returns {Promise} + */ + async updateFromRequest(payload) { + if (!payload) return false + + let hasUpdates = false + + if (payload.metadata) { + const stringKeys = ['title', 'author', 'releaseDate', 'feedUrl', 'imageUrl', 'description', 'itunesPageUrl', 'itunesId', 'itunesArtistId', 'language', 'type'] + stringKeys.forEach((key) => { + let newKey = key + if (key === 'type') { + newKey = 'podcastType' + } else if (key === 'feedUrl') { + newKey = 'feedURL' + } else if (key === 'imageUrl') { + newKey = 'imageURL' + } else if (key === 'itunesPageUrl') { + newKey = 'itunesPageURL' + } + if (typeof payload.metadata[key] === 'string' && payload.metadata[key] !== this[newKey]) { + this[newKey] = payload.metadata[key] + if (key === 'title') { + this.titleIgnorePrefix = getTitleIgnorePrefix(this.title) + } + + hasUpdates = true + } + }) + + if (payload.metadata.explicit !== undefined && payload.metadata.explicit !== this.explicit) { + this.explicit = !!payload.metadata.explicit + hasUpdates = true + } + + if (Array.isArray(payload.metadata.genres) && !payload.metadata.genres.some((item) => typeof item !== 'string') && JSON.stringify(this.genres) !== JSON.stringify(payload.metadata.genres)) { + this.genres = payload.metadata.genres + this.changed('genres', true) + hasUpdates = true + } + } + + if (Array.isArray(payload.tags) && !payload.tags.some((item) => typeof item !== 'string') && JSON.stringify(this.tags) !== JSON.stringify(payload.tags)) { + this.tags = payload.tags + this.changed('tags', true) + hasUpdates = true + } + + if (payload.autoDownloadEpisodes !== undefined && payload.autoDownloadEpisodes !== this.autoDownloadEpisodes) { + this.autoDownloadEpisodes = !!payload.autoDownloadEpisodes + hasUpdates = true + } + if (typeof payload.autoDownloadSchedule === 'string' && payload.autoDownloadSchedule !== this.autoDownloadSchedule) { + this.autoDownloadSchedule = payload.autoDownloadSchedule + hasUpdates = true + } + + const numberKeys = ['maxEpisodesToKeep', 'maxNewEpisodesToDownload'] + numberKeys.forEach((key) => { + if (typeof payload[key] === 'number' && payload[key] !== this[key]) { + this[key] = payload[key] + hasUpdates = true + } + }) + + if (hasUpdates) { + Logger.debug(`[Podcast] changed keys:`, this.changed()) + await this.save() + } + + return hasUpdates + } + /** * Old model kept metadata in a separate object */