From d576625cb76ed35cc0bfe7dc0910b88ae262a0d4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 10:53:31 -0600 Subject: [PATCH 1/9] Refactor Feed model to create new feed for collection --- server/controllers/RSSFeedController.js | 22 ++++---- server/managers/RssFeedManager.js | 22 ++++---- server/models/Book.js | 6 +++ server/models/Collection.js | 34 +++++++++++++ server/models/Feed.js | 68 ++++++++++++++++++++++++- server/models/FeedEpisode.js | 57 +++++++++++++++------ server/objects/Feed.js | 55 -------------------- 7 files changed, 171 insertions(+), 93 deletions(-) diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index b16820aa..65c823a4 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -87,35 +87,39 @@ class RSSFeedController { * @param {Response} res */ async openRSSFeedForCollection(req, res) { - const options = req.body || {} + const reqBody = req.body || {} const collection = await Database.collectionModel.findByPk(req.params.collectionId) if (!collection) return res.sendStatus(404) // Check request body options exist - if (!options.serverAddress || !options.slug) { + if (!reqBody.serverAddress || !reqBody.slug || typeof reqBody.serverAddress !== 'string' || typeof reqBody.slug !== 'string') { Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`) return res.status(400).send('Invalid request body') } // Check that this slug is not being used for another feed (slug will also be the Feed id) - if (await this.rssFeedManager.findFeedBySlug(options.slug)) { - Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`) + if (await this.rssFeedManager.findFeedBySlug(reqBody.slug)) { + Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`) return res.status(400).send('Slug already in use') } - const collectionExpanded = await collection.getOldJsonExpanded() - const collectionItemsWithTracks = collectionExpanded.books.filter((li) => li.media.tracks.length) + collection.books = await collection.getBooksExpandedWithLibraryItem() // Check collection has audio tracks - if (!collectionItemsWithTracks.length) { + if (!collection.books.some((book) => book.includedAudioFiles.length)) { Logger.error(`[RSSFeedController] Cannot open RSS feed for collection "${collection.name}" because it has no audio tracks`) return res.status(400).send('Collection has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForCollection(req.user.id, collectionExpanded, req.body) + const feed = await this.rssFeedManager.openFeedForCollection(req.user.id, collection, reqBody) + if (!feed) { + Logger.error(`[RSSFeedController] Failed to open RSS feed for collection "${collection.name}"`) + return res.status(500).send('Failed to open RSS feed') + } + res.json({ - feed: feed.toJSONMinified() + feed: feed.toOldJSONMinified() }) } diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 3feb87d0..032aa4f2 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -264,24 +264,22 @@ class RssFeedManager { /** * * @param {string} userId - * @param {*} collectionExpanded + * @param {import('../models/Collection')} collectionExpanded * @param {*} options - * @returns + * @returns {Promise} */ async openFeedForCollection(userId, collectionExpanded, options) { const serverAddress = options.serverAddress const slug = options.slug - const preventIndexing = options.metadataDetails?.preventIndexing ?? true - const ownerName = options.metadataDetails?.ownerName - const ownerEmail = options.metadataDetails?.ownerEmail + const feedOptions = this.getFeedOptionsFromReqOptions(options) - const feed = new Feed() - feed.setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) - - Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) - await Database.createFeed(feed) - SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) - return feed + Logger.info(`[RssFeedManager] Creating RSS feed for collection "${collectionExpanded.name}"`) + const feedExpanded = await Database.feedModel.createFeedForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions) + if (feedExpanded) { + Logger.info(`[RssFeedManager] Opened RSS feed "${feedExpanded.feedURL}"`) + SocketAuthority.emitter('rss_feed_open', feedExpanded.toOldJSONMinified()) + } + return feedExpanded } /** diff --git a/server/models/Book.js b/server/models/Book.js index 71a55033..f7341db9 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -29,6 +29,12 @@ const Logger = require('../Logger') * @property {SeriesExpanded[]} series * * @typedef {Book & BookExpandedProperties} BookExpanded + * + * Collections use BookExpandedWithLibraryItem + * @typedef BookExpandedWithLibraryItemProperties + * @property {import('./LibraryItem')} libraryItem + * + * @typedef {BookExpanded & BookExpandedWithLibraryItemProperties} BookExpandedWithLibraryItem */ /** diff --git a/server/models/Collection.js b/server/models/Collection.js index a001dc5b..0fb622dd 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -1,6 +1,7 @@ const { DataTypes, Model, Sequelize } = require('sequelize') const oldCollection = require('../objects/Collection') +const Logger = require('../Logger') class Collection extends Model { constructor(values, options) { @@ -18,6 +19,11 @@ class Collection extends Model { this.updatedAt /** @type {Date} */ this.createdAt + + // Expanded properties + + /** @type {import('./Book').BookExpandedWithLibraryItem[]} - only set when expanded */ + this.books } /** @@ -219,6 +225,34 @@ class Collection extends Model { Collection.belongsTo(library) } + /** + * Get all books in collection expanded with library item + * + * @returns {Promise} + */ + getBooksExpandedWithLibraryItem() { + return this.getBooks({ + include: [ + { + model: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ], + order: [Sequelize.literal('`collectionBook.order` ASC')] + }) + } + /** * Get old collection toJSONExpanded, items filtered for user permissions * diff --git a/server/models/Feed.js b/server/models/Feed.js index ff25a612..f22c1ac3 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -279,8 +279,8 @@ class Feed extends Model { /** * * @param {string} userId - * @param {string} slug * @param {import('./LibraryItem').LibraryItemExpanded} libraryItem + * @param {string} slug * @param {string} serverAddress * @param {FeedOptions} feedOptions * @@ -334,6 +334,72 @@ class Feed extends Model { } } + /** + * + * @param {string} userId + * @param {import('./Collection')} collectionExpanded + * @param {string} slug + * @param {string} serverAddress + * @param {FeedOptions} feedOptions + * + * @returns {Promise} + */ + static async createFeedForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions) { + const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length) + const libraryItemMostRecentlyUpdatedAt = booksWithTracks.reduce((mostRecent, book) => { + return book.libraryItem.updatedAt > mostRecent.libraryItem.updatedAt ? book : mostRecent + }).libraryItem.updatedAt + + const firstBookWithCover = booksWithTracks.find((book) => book.coverPath) + + const allBookAuthorNames = booksWithTracks.reduce((authorNames, book) => { + const bookAuthorsToAdd = book.authors.filter((author) => !authorNames.includes(author.name)).map((author) => author.name) + return authorNames.concat(bookAuthorsToAdd) + }, []) + let author = allBookAuthorNames.slice(0, 3).join(', ') + if (allBookAuthorNames.length > 3) { + author += ' & more' + } + + const feedObj = { + slug, + entityType: 'collection', + entityId: collectionExpanded.id, + entityUpdatedAt: libraryItemMostRecentlyUpdatedAt, + serverAddress, + feedURL: `/feed/${slug}`, + imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`, + siteURL: `/collection/${collectionExpanded.id}`, + title: collectionExpanded.name, + description: collectionExpanded.description || '', + author, + podcastType: 'serial', + preventIndexing: feedOptions.preventIndexing, + ownerName: feedOptions.ownerName, + ownerEmail: feedOptions.ownerEmail, + explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit + coverPath: firstBookWithCover?.coverPath || null, + userId + } + + /** @type {typeof import('./FeedEpisode')} */ + const feedEpisodeModel = this.sequelize.models.feedEpisode + + const transaction = await this.sequelize.transaction() + try { + const feed = await this.create(feedObj, { transaction }) + feed.feedEpisodes = await feedEpisodeModel.createFromCollectionBooks(collectionExpanded, feed, slug, transaction) + + await transaction.commit() + + return feed + } catch (error) { + Logger.error(`[Feed] Error creating feed for collection ${collectionExpanded.id}`, error) + await transaction.rollback() + return null + } + } + /** * Initialize model * diff --git a/server/models/FeedEpisode.js b/server/models/FeedEpisode.js index 0a90f97d..c92ee9cc 100644 --- a/server/models/FeedEpisode.js +++ b/server/models/FeedEpisode.js @@ -132,12 +132,12 @@ class FeedEpisode extends Model { /** * If chapters for an audiobook match the audio tracks then use chapter titles instead of audio file names * - * @param {import('./LibraryItem').LibraryItemExpanded} libraryItemExpanded + * @param {import('./Book')} book * @returns {boolean} */ - static checkUseChapterTitlesForEpisodes(libraryItemExpanded) { - const tracks = libraryItemExpanded.media.trackList || [] - const chapters = libraryItemExpanded.media.chapters || [] + static checkUseChapterTitlesForEpisodes(book) { + const tracks = book.trackList || [] + const chapters = book.chapters || [] if (tracks.length !== chapters.length) return false for (let i = 0; i < tracks.length; i++) { if (Math.abs(chapters[i].start - tracks[i].startOffset) >= 1) { @@ -149,32 +149,31 @@ class FeedEpisode extends Model { /** * - * @param {import('./LibraryItem').LibraryItemExpanded} libraryItemExpanded + * @param {import('./Book')} book + * @param {Date} pubDateStart * @param {import('./Feed')} feed * @param {string} slug * @param {import('./Book').AudioFileObject} audioTrack * @param {boolean} useChapterTitles - * @param {string} [pubDateOverride] */ - static getFeedEpisodeObjFromAudiobookTrack(libraryItemExpanded, feed, slug, audioTrack, useChapterTitles, pubDateOverride = null) { + static getFeedEpisodeObjFromAudiobookTrack(book, pubDateStart, feed, slug, audioTrack, useChapterTitles) { // Example: Fri, 04 Feb 2015 00:00:00 GMT let timeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset pubdate to ensure correct order let episodeId = uuidv4() // e.g. Track 1 will have a pub date before Track 2 - const audiobookPubDate = pubDateOverride || date.format(new Date(libraryItemExpanded.createdAt.valueOf() + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') + const audiobookPubDate = date.format(new Date(pubDateStart.valueOf() + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') const contentUrl = `/feed/${slug}/item/${episodeId}/media${Path.extname(audioTrack.metadata.filename)}` - const media = libraryItemExpanded.media let title = audioTrack.title - if (media.trackList.length == 1) { + if (book.trackList.length == 1) { // If audiobook is a single file, use book title instead of chapter/file title - title = media.title + title = book.title } else { if (useChapterTitles) { // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title - const matchingChapter = media.chapters.find((ch) => Math.abs(ch.start - audioTrack.startOffset) < 1) + const matchingChapter = book.chapters.find((ch) => Math.abs(ch.start - audioTrack.startOffset) < 1) if (matchingChapter?.title) title = matchingChapter.title } } @@ -183,7 +182,7 @@ class FeedEpisode extends Model { id: episodeId, title, author: feed.author, - description: media.description || '', + description: book.description || '', siteURL: feed.siteURL, enclosureURL: contentUrl, enclosureType: audioTrack.mimeType, @@ -191,7 +190,7 @@ class FeedEpisode extends Model { pubDate: audiobookPubDate, duration: audioTrack.duration, filePath: audioTrack.metadata.path, - explicit: media.explicit, + explicit: book.explicit, feedId: feed.id } } @@ -205,11 +204,37 @@ class FeedEpisode extends Model { * @returns {Promise} */ static async createFromAudiobookTracks(libraryItemExpanded, feed, slug, transaction) { - const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItemExpanded) + const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItemExpanded.media) const feedEpisodeObjs = [] for (const track of libraryItemExpanded.media.trackList) { - feedEpisodeObjs.push(this.getFeedEpisodeObjFromAudiobookTrack(libraryItemExpanded, feed, slug, track, useChapterTitles)) + feedEpisodeObjs.push(this.getFeedEpisodeObjFromAudiobookTrack(libraryItemExpanded.media, libraryItemExpanded.createdAt, feed, slug, track, useChapterTitles)) + } + Logger.info(`[FeedEpisode] Creating ${feedEpisodeObjs.length} episodes for feed ${feed.id}`) + return this.bulkCreate(feedEpisodeObjs, { transaction }) + } + + /** + * + * @param {import('./Collection')} collectionExpanded + * @param {import('./Feed')} feed + * @param {string} slug + * @param {import('sequelize').Transaction} transaction + * @returns {Promise} + */ + static async createFromCollectionBooks(collectionExpanded, feed, slug, transaction) { + const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length) + + const earliestLibraryItemCreatedAt = collectionExpanded.books.reduce((earliest, book) => { + return book.libraryItem.createdAt < earliest.libraryItem.createdAt ? book : earliest + }).libraryItem.createdAt + + const feedEpisodeObjs = [] + for (const book of booksWithTracks) { + const useChapterTitles = this.checkUseChapterTitlesForEpisodes(book) + for (const track of book.trackList) { + feedEpisodeObjs.push(this.getFeedEpisodeObjFromAudiobookTrack(book, earliestLibraryItemCreatedAt, feed, slug, track, useChapterTitles)) + } } Logger.info(`[FeedEpisode] Creating ${feedEpisodeObjs.length} episodes for feed ${feed.id}`) return this.bulkCreate(feedEpisodeObjs, { transaction }) diff --git a/server/objects/Feed.js b/server/objects/Feed.js index dd086bb0..378ef3ee 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -143,61 +143,6 @@ class Feed { this.updatedAt = Date.now() } - setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { - const feedUrl = `/feed/${slug}` - - const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) - const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) - - this.id = uuidv4() - this.slug = slug - this.userId = userId - this.entityType = 'collection' - this.entityId = collectionExpanded.id - this.entityUpdatedAt = collectionExpanded.lastUpdate // This will be set to the most recently updated library item - this.coverPath = firstItemWithCover?.media.coverPath || null - this.serverAddress = serverAddress - this.feedUrl = feedUrl - - const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null - - this.meta = new FeedMeta() - this.meta.title = collectionExpanded.name - this.meta.description = collectionExpanded.description || '' - this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png` - this.meta.feedUrl = feedUrl - this.meta.link = `/collection/${collectionExpanded.id}` - this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit - this.meta.preventIndexing = preventIndexing - this.meta.ownerName = ownerName - this.meta.ownerEmail = ownerEmail - - this.episodes = [] - - // Used for calculating pubdate - const earliestItemAddedAt = itemsWithTracks.reduce((earliest, item) => (item.addedAt < earliest ? item.addedAt : earliest), itemsWithTracks[0].addedAt) - - itemsWithTracks.forEach((item, index) => { - if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt - - const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item) - item.media.tracks.forEach((audioTrack) => { - const feedEpisode = new FeedEpisode() - - // Offset pubdate to ensure correct order - let trackTimeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset track - trackTimeOffset += index * 1000 // Offset item - const episodePubDateOverride = date.format(new Date(earliestItemAddedAt + trackTimeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') - feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, episodePubDateOverride) - this.episodes.push(feedEpisode) - }) - }) - - this.createdAt = Date.now() - this.updatedAt = Date.now() - } - updateFromCollection(collectionExpanded) { const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) From e50bd939581bf2eab5764fbaf8916a10dd5cacb1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 11:44:07 -0600 Subject: [PATCH 2/9] Refactor Feed model to create new feed for series --- server/controllers/RSSFeedController.js | 26 +++++----- server/managers/RssFeedManager.js | 22 ++++---- server/models/Feed.js | 68 ++++++++++++++++++++++++- server/models/FeedEpisode.js | 10 ++-- server/models/Series.js | 36 ++++++++++++- server/objects/Feed.js | 59 --------------------- 6 files changed, 130 insertions(+), 91 deletions(-) diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index 65c823a4..57e02c5c 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -132,37 +132,39 @@ class RSSFeedController { * @param {Response} res */ async openRSSFeedForSeries(req, res) { - const options = req.body || {} + const reqBody = req.body || {} const series = await Database.seriesModel.findByPk(req.params.seriesId) if (!series) return res.sendStatus(404) // Check request body options exist - if (!options.serverAddress || !options.slug) { + if (!reqBody.serverAddress || !reqBody.slug || typeof reqBody.serverAddress !== 'string' || typeof reqBody.slug !== 'string') { Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`) return res.status(400).send('Invalid request body') } // Check that this slug is not being used for another feed (slug will also be the Feed id) - if (await this.rssFeedManager.findFeedBySlug(options.slug)) { - Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`) + if (await this.rssFeedManager.findFeedBySlug(reqBody.slug)) { + Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`) return res.status(400).send('Slug already in use') } - const seriesJson = series.toOldJSON() - - // Get books in series that have audio tracks - seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) + series.books = await series.getBooksExpandedWithLibraryItem() // Check series has audio tracks - if (!seriesJson.books.length) { - Logger.error(`[RSSFeedController] Cannot open RSS feed for series "${seriesJson.name}" because it has no audio tracks`) + if (!series.books.some((book) => book.includedAudioFiles.length)) { + Logger.error(`[RSSFeedController] Cannot open RSS feed for series "${series.name}" because it has no audio tracks`) return res.status(400).send('Series has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForSeries(req.user.id, seriesJson, req.body) + const feed = await this.rssFeedManager.openFeedForSeries(req.user.id, series, req.body) + if (!feed) { + Logger.error(`[RSSFeedController] Failed to open RSS feed for series "${series.name}"`) + return res.status(500).send('Failed to open RSS feed') + } + res.json({ - feed: feed.toJSONMinified() + feed: feed.toOldJSONMinified() }) } diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 032aa4f2..8d5b5e40 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -285,24 +285,22 @@ class RssFeedManager { /** * * @param {string} userId - * @param {*} seriesExpanded + * @param {import('../models/Series')} seriesExpanded * @param {*} options - * @returns + * @returns {Promise} */ async openFeedForSeries(userId, seriesExpanded, options) { const serverAddress = options.serverAddress const slug = options.slug - const preventIndexing = options.metadataDetails?.preventIndexing ?? true - const ownerName = options.metadataDetails?.ownerName - const ownerEmail = options.metadataDetails?.ownerEmail + const feedOptions = this.getFeedOptionsFromReqOptions(options) - const feed = new Feed() - feed.setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) - - Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) - await Database.createFeed(feed) - SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified()) - return feed + Logger.info(`[RssFeedManager] Creating RSS feed for series "${seriesExpanded.name}"`) + const feedExpanded = await Database.feedModel.createFeedForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions) + if (feedExpanded) { + Logger.info(`[RssFeedManager] Opened RSS feed "${feedExpanded.feedURL}"`) + SocketAuthority.emitter('rss_feed_open', feedExpanded.toOldJSONMinified()) + } + return feedExpanded } async handleCloseFeed(feed) { diff --git a/server/models/Feed.js b/server/models/Feed.js index f22c1ac3..fa1b0f00 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -388,7 +388,73 @@ class Feed extends Model { const transaction = await this.sequelize.transaction() try { const feed = await this.create(feedObj, { transaction }) - feed.feedEpisodes = await feedEpisodeModel.createFromCollectionBooks(collectionExpanded, feed, slug, transaction) + feed.feedEpisodes = await feedEpisodeModel.createFromBooks(booksWithTracks, feed, slug, transaction) + + await transaction.commit() + + return feed + } catch (error) { + Logger.error(`[Feed] Error creating feed for collection ${collectionExpanded.id}`, error) + await transaction.rollback() + return null + } + } + + /** + * + * @param {string} userId + * @param {import('./Series')} seriesExpanded + * @param {string} slug + * @param {string} serverAddress + * @param {FeedOptions} feedOptions + * + * @returns {Promise} + */ + static async createFeedForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions) { + const booksWithTracks = seriesExpanded.books.filter((book) => book.includedAudioFiles.length) + const libraryItemMostRecentlyUpdatedAt = booksWithTracks.reduce((mostRecent, book) => { + return book.libraryItem.updatedAt > mostRecent.libraryItem.updatedAt ? book : mostRecent + }).libraryItem.updatedAt + + const firstBookWithCover = booksWithTracks.find((book) => book.coverPath) + + const allBookAuthorNames = booksWithTracks.reduce((authorNames, book) => { + const bookAuthorsToAdd = book.authors.filter((author) => !authorNames.includes(author.name)).map((author) => author.name) + return authorNames.concat(bookAuthorsToAdd) + }, []) + let author = allBookAuthorNames.slice(0, 3).join(', ') + if (allBookAuthorNames.length > 3) { + author += ' & more' + } + + const feedObj = { + slug, + entityType: 'series', + entityId: seriesExpanded.id, + entityUpdatedAt: libraryItemMostRecentlyUpdatedAt, + serverAddress, + feedURL: `/feed/${slug}`, + imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`, + siteURL: `/library/${booksWithTracks[0].libraryItem.libraryId}/series/${seriesExpanded.id}`, + title: seriesExpanded.name, + description: seriesExpanded.description || '', + author, + podcastType: 'serial', + preventIndexing: feedOptions.preventIndexing, + ownerName: feedOptions.ownerName, + ownerEmail: feedOptions.ownerEmail, + explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit + coverPath: firstBookWithCover?.coverPath || null, + userId + } + + /** @type {typeof import('./FeedEpisode')} */ + const feedEpisodeModel = this.sequelize.models.feedEpisode + + const transaction = await this.sequelize.transaction() + try { + const feed = await this.create(feedObj, { transaction }) + feed.feedEpisodes = await feedEpisodeModel.createFromBooks(booksWithTracks, feed, slug, transaction) await transaction.commit() diff --git a/server/models/FeedEpisode.js b/server/models/FeedEpisode.js index c92ee9cc..4979ae6d 100644 --- a/server/models/FeedEpisode.js +++ b/server/models/FeedEpisode.js @@ -216,21 +216,19 @@ class FeedEpisode extends Model { /** * - * @param {import('./Collection')} collectionExpanded + * @param {import('./Book')[]} books * @param {import('./Feed')} feed * @param {string} slug * @param {import('sequelize').Transaction} transaction * @returns {Promise} */ - static async createFromCollectionBooks(collectionExpanded, feed, slug, transaction) { - const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length) - - const earliestLibraryItemCreatedAt = collectionExpanded.books.reduce((earliest, book) => { + static async createFromBooks(books, feed, slug, transaction) { + const earliestLibraryItemCreatedAt = books.reduce((earliest, book) => { return book.libraryItem.createdAt < earliest.libraryItem.createdAt ? book : earliest }).libraryItem.createdAt const feedEpisodeObjs = [] - for (const book of booksWithTracks) { + for (const book of books) { const useChapterTitles = this.checkUseChapterTitlesForEpisodes(book) for (const track of book.trackList) { feedEpisodeObjs.push(this.getFeedEpisodeObjFromAudiobookTrack(book, earliestLibraryItemCreatedAt, feed, slug, track, useChapterTitles)) diff --git a/server/models/Series.js b/server/models/Series.js index 731908e9..4fa6b403 100644 --- a/server/models/Series.js +++ b/server/models/Series.js @@ -1,4 +1,4 @@ -const { DataTypes, Model, where, fn, col } = require('sequelize') +const { DataTypes, Model, where, fn, col, literal } = require('sequelize') const { getTitlePrefixAtEnd } = require('../utils/index') @@ -20,6 +20,11 @@ class Series extends Model { this.createdAt /** @type {Date} */ this.updatedAt + + // Expanded properties + + /** @type {import('./Book').BookExpandedWithLibraryItem[]} - only set when expanded */ + this.books } /** @@ -103,6 +108,35 @@ class Series extends Model { Series.belongsTo(library) } + /** + * Get all books in collection expanded with library item + * + * @returns {Promise} + */ + getBooksExpandedWithLibraryItem() { + return this.getBooks({ + joinTableAttributes: ['sequence'], + include: [ + { + model: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ], + order: [[literal('CAST(`bookSeries.sequence` AS FLOAT) ASC NULLS LAST')]] + }) + } + toOldJSON() { return { id: this.id, diff --git a/server/objects/Feed.js b/server/objects/Feed.js index 378ef3ee..668544d9 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -182,65 +182,6 @@ class Feed { this.updatedAt = Date.now() } - setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing = true, ownerName = null, ownerEmail = null) { - const feedUrl = `/feed/${slug}` - - let itemsWithTracks = seriesExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) - // Sort series items by series sequence - itemsWithTracks = naturalSort(itemsWithTracks).asc((li) => li.media.metadata.getSeriesSequence(seriesExpanded.id)) - - const libraryId = itemsWithTracks[0].libraryId - const firstItemWithCover = itemsWithTracks.find((li) => li.media.coverPath) - - this.id = uuidv4() - this.slug = slug - this.userId = userId - this.entityType = 'series' - this.entityId = seriesExpanded.id - this.entityUpdatedAt = seriesExpanded.updatedAt // This will be set to the most recently updated library item - this.coverPath = firstItemWithCover?.media.coverPath || null - this.serverAddress = serverAddress - this.feedUrl = feedUrl - - const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null - - this.meta = new FeedMeta() - this.meta.title = seriesExpanded.name - this.meta.description = seriesExpanded.description || '' - this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `/feed/${slug}/cover${coverFileExtension}` : `/Logo.png` - this.meta.feedUrl = feedUrl - this.meta.link = `/library/${libraryId}/series/${seriesExpanded.id}` - this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit - this.meta.preventIndexing = preventIndexing - this.meta.ownerName = ownerName - this.meta.ownerEmail = ownerEmail - - this.episodes = [] - - // Used for calculating pubdate - const earliestItemAddedAt = itemsWithTracks.reduce((earliest, item) => (item.addedAt < earliest ? item.addedAt : earliest), itemsWithTracks[0].addedAt) - - itemsWithTracks.forEach((item, index) => { - if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt - - const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item) - item.media.tracks.forEach((audioTrack) => { - const feedEpisode = new FeedEpisode() - - // Offset pubdate to ensure correct order - let trackTimeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset track - trackTimeOffset += index * 1000 // Offset item - const episodePubDateOverride = date.format(new Date(earliestItemAddedAt + trackTimeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') - feedEpisode.setFromAudiobookTrack(item, serverAddress, slug, audioTrack, this.meta, useChapterTitles, episodePubDateOverride) - this.episodes.push(feedEpisode) - }) - }) - - this.createdAt = Date.now() - this.updatedAt = Date.now() - } - updateFromSeries(seriesExpanded) { let itemsWithTracks = seriesExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) // Sort series items by series sequence From 4c68ad46f43a891ff604ce3a62286e824866b0f8 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 12:37:01 -0600 Subject: [PATCH 3/9] Refactor RssFeedManager to use new model when closing feeds, fix close series feed when series is removed, update RssFeedManager to singleton --- server/Server.js | 9 ++- server/controllers/CollectionController.js | 5 +- server/controllers/LibraryController.js | 4 +- server/controllers/LibraryItemController.js | 4 +- server/controllers/RSSFeedController.js | 29 ++++++--- server/controllers/SeriesController.js | 5 +- server/managers/RssFeedManager.js | 68 +++++++++++++-------- server/models/Feed.js | 34 ++++------- server/routers/ApiRouter.js | 7 +-- server/scanner/BookScanner.js | 18 ++++-- 10 files changed, 105 insertions(+), 78 deletions(-) diff --git a/server/Server.js b/server/Server.js index 2f1220d8..95e3d683 100644 --- a/server/Server.js +++ b/server/Server.js @@ -71,7 +71,6 @@ class Server { this.playbackSessionManager = new PlaybackSessionManager() this.podcastManager = new PodcastManager() this.audioMetadataManager = new AudioMetadataMangaer() - this.rssFeedManager = new RssFeedManager() this.cronManager = new CronManager(this.podcastManager, this.playbackSessionManager) this.apiCacheManager = new ApiCacheManager() this.binaryManager = new BinaryManager() @@ -137,7 +136,7 @@ class Server { await ShareManager.init() await this.backupManager.init() - await this.rssFeedManager.init() + await RssFeedManager.init() const libraries = await Database.libraryModel.getAllWithFolders() await this.cronManager.init(libraries) @@ -291,14 +290,14 @@ class Server { // RSS Feed temp route router.get('/feed/:slug', (req, res) => { Logger.info(`[Server] Requesting rss feed ${req.params.slug}`) - this.rssFeedManager.getFeed(req, res) + RssFeedManager.getFeed(req, res) }) router.get('/feed/:slug/cover*', (req, res) => { - this.rssFeedManager.getFeedCover(req, res) + RssFeedManager.getFeedCover(req, res) }) router.get('/feed/:slug/item/:episodeId/*', (req, res) => { Logger.debug(`[Server] Requesting rss feed episode ${req.params.slug}/${req.params.episodeId}`) - this.rssFeedManager.getFeedItem(req, res) + RssFeedManager.getFeedItem(req, res) }) // Auth routes diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 708c00b5..2445eb50 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -4,6 +4,7 @@ const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') +const RssFeedManager = require('../managers/RssFeedManager') const Collection = require('../objects/Collection') /** @@ -148,6 +149,8 @@ class CollectionController { /** * DELETE: /api/collections/:id * + * @this {import('../routers/ApiRouter')} + * * @param {RequestWithUser} req * @param {Response} res */ @@ -155,7 +158,7 @@ class CollectionController { const jsonExpanded = await req.collection.getOldJsonExpanded() // Close rss feed - remove from db and emit socket event - await this.rssFeedManager.closeFeedForEntityId(req.collection.id) + await RssFeedManager.closeFeedForEntityId(req.collection.id) await req.collection.destroy() diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index fc15488d..da9859f2 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -18,6 +18,8 @@ const LibraryScanner = require('../scanner/LibraryScanner') const Scanner = require('../scanner/Scanner') const Database = require('../Database') const Watcher = require('../Watcher') +const RssFeedManager = require('../managers/RssFeedManager') + const libraryFilters = require('../utils/queries/libraryFilters') const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters') const authorFilters = require('../utils/queries/authorFilters') @@ -759,7 +761,7 @@ class LibraryController { } if (include.includes('rssfeed')) { - const feedObj = await this.rssFeedManager.findFeedForEntityId(seriesJson.id) + const feedObj = await RssFeedManager.findFeedForEntityId(seriesJson.id) seriesJson.rssFeed = feedObj?.toJSONMinified() || null } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 5aaacee0..5fac31aa 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -13,6 +13,8 @@ const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUti const LibraryItemScanner = require('../scanner/LibraryItemScanner') const AudioFileScanner = require('../scanner/AudioFileScanner') const Scanner = require('../scanner/Scanner') + +const RssFeedManager = require('../managers/RssFeedManager') const CacheManager = require('../managers/CacheManager') const CoverManager = require('../managers/CoverManager') const ShareManager = require('../managers/ShareManager') @@ -48,7 +50,7 @@ class LibraryItemController { } if (includeEntities.includes('rssfeed')) { - const feedData = await this.rssFeedManager.findFeedForEntityId(item.id) + const feedData = await RssFeedManager.findFeedForEntityId(item.id) item.rssFeed = feedData?.toJSONMinified() || null } diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index 57e02c5c..3cd7736b 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -1,7 +1,8 @@ const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const Database = require('../Database') -const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') + +const RssFeedManager = require('../managers/RssFeedManager') /** * @typedef RequestUserObject @@ -22,7 +23,7 @@ class RSSFeedController { * @param {Response} res */ async getAll(req, res) { - const feeds = await this.rssFeedManager.getFeeds() + const feeds = await RssFeedManager.getFeeds() res.json({ feeds: feeds.map((f) => f.toJSON()), minified: feeds.map((f) => f.toJSONMinified()) @@ -62,12 +63,12 @@ class RSSFeedController { } // Check that this slug is not being used for another feed (slug will also be the Feed id) - if (await this.rssFeedManager.findFeedBySlug(reqBody.slug)) { + if (await RssFeedManager.findFeedBySlug(reqBody.slug)) { Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`) return res.status(400).send('Slug already in use') } - const feed = await this.rssFeedManager.openFeedForItem(req.user.id, itemExpanded, reqBody) + const feed = await RssFeedManager.openFeedForItem(req.user.id, itemExpanded, reqBody) if (!feed) { Logger.error(`[RSSFeedController] Failed to open RSS feed for item "${itemExpanded.media.title}"`) return res.status(500).send('Failed to open RSS feed') @@ -99,7 +100,7 @@ class RSSFeedController { } // Check that this slug is not being used for another feed (slug will also be the Feed id) - if (await this.rssFeedManager.findFeedBySlug(reqBody.slug)) { + if (await RssFeedManager.findFeedBySlug(reqBody.slug)) { Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`) return res.status(400).send('Slug already in use') } @@ -112,7 +113,7 @@ class RSSFeedController { return res.status(400).send('Collection has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForCollection(req.user.id, collection, reqBody) + const feed = await RssFeedManager.openFeedForCollection(req.user.id, collection, reqBody) if (!feed) { Logger.error(`[RSSFeedController] Failed to open RSS feed for collection "${collection.name}"`) return res.status(500).send('Failed to open RSS feed') @@ -144,7 +145,7 @@ class RSSFeedController { } // Check that this slug is not being used for another feed (slug will also be the Feed id) - if (await this.rssFeedManager.findFeedBySlug(reqBody.slug)) { + if (await RssFeedManager.findFeedBySlug(reqBody.slug)) { Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`) return res.status(400).send('Slug already in use') } @@ -157,7 +158,7 @@ class RSSFeedController { return res.status(400).send('Series has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForSeries(req.user.id, series, req.body) + const feed = await RssFeedManager.openFeedForSeries(req.user.id, series, req.body) if (!feed) { Logger.error(`[RSSFeedController] Failed to open RSS feed for series "${series.name}"`) return res.status(500).send('Failed to open RSS feed') @@ -176,8 +177,16 @@ class RSSFeedController { * @param {RequestWithUser} req * @param {Response} res */ - closeRSSFeed(req, res) { - this.rssFeedManager.closeRssFeed(req, res) + async closeRSSFeed(req, res) { + const feed = await Database.feedModel.findByPk(req.params.id) + if (!feed) { + Logger.error(`[RSSFeedController] Cannot close RSS feed because feed "${req.params.id}" does not exist`) + return res.sendStatus(404) + } + + await RssFeedManager.handleCloseFeed(feed) + + res.sendStatus(200) } /** diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index 5d761ba9..f8aef05c 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -2,6 +2,9 @@ const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') + +const RssFeedManager = require('../managers/RssFeedManager') + const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') /** @@ -51,7 +54,7 @@ class SeriesController { } if (include.includes('rssfeed')) { - const feedObj = await this.rssFeedManager.findFeedForEntityId(seriesJson.id) + const feedObj = await RssFeedManager.findFeedForEntityId(seriesJson.id) seriesJson.rssFeed = feedObj?.toJSONMinified() || null } diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 8d5b5e40..da0d8dd2 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -6,7 +6,6 @@ const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const fs = require('../libs/fsExtra') -const Feed = require('../objects/Feed') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') class RssFeedManager { @@ -69,15 +68,6 @@ class RssFeedManager { return Database.feedModel.findOneOld({ slug }) } - /** - * Find open feed for a slug - * @param {string} slug - * @returns {Promise} oldFeed - */ - findFeed(id) { - return Database.feedModel.findByPkOld(id) - } - /** * GET: /feed/:slug * @@ -303,33 +293,57 @@ class RssFeedManager { return feedExpanded } + /** + * Close Feed and emit Socket event + * + * @param {import('../models/Feed')} feed + * @returns {Promise} - true if feed was closed + */ async handleCloseFeed(feed) { - if (!feed) return - await Database.removeFeed(feed.id) - SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified()) - Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`) - } - - async closeRssFeed(req, res) { - const feed = await this.findFeed(req.params.id) - if (!feed) { - Logger.error(`[RssFeedManager] RSS feed not found with id "${req.params.id}"`) - return res.sendStatus(404) - } - await this.handleCloseFeed(feed) - res.sendStatus(200) + if (!feed) return false + const wasRemoved = await Database.feedModel.removeById(feed.id) + SocketAuthority.emitter('rss_feed_closed', feed.toOldJSONMinified()) + Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedURL}"`) + return wasRemoved } + /** + * + * @param {string} entityId + * @returns {Promise} - true if feed was closed + */ async closeFeedForEntityId(entityId) { - const feed = await this.findFeedForEntityId(entityId) - if (!feed) return + const feed = await Database.feedModel.findOne({ + where: { + entityId + } + }) + if (!feed) { + Logger.warn(`[RssFeedManager] closeFeedForEntityId: Feed not found for entity id ${entityId}`) + return false + } return this.handleCloseFeed(feed) } + /** + * + * @param {string[]} entityIds + */ + async closeFeedsForEntityIds(entityIds) { + const feeds = await Database.feedModel.findAll({ + where: { + entityId: entityIds + } + }) + for (const feed of feeds) { + await this.handleCloseFeed(feed) + } + } + async getFeeds() { const feeds = await Database.models.feed.getOldFeeds() Logger.info(`[RssFeedManager] Fetched all feeds`) return feeds } } -module.exports = RssFeedManager +module.exports = new RssFeedManager() diff --git a/server/models/Feed.js b/server/models/Feed.js index fa1b0f00..3b67ec62 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -124,12 +124,18 @@ class Feed extends Model { }) } - static removeById(feedId) { - return this.destroy({ - where: { - id: feedId - } - }) + /** + * @param {string} feedId + * @returns {Promise} - true if feed was removed + */ + static async removeById(feedId) { + return ( + (await this.destroy({ + where: { + id: feedId + } + })) > 0 + ) } /** @@ -163,22 +169,6 @@ class Feed extends Model { return this.getOldFeed(feedExpanded) } - /** - * Find feed and return oldFeed - * @param {string} id - * @returns {Promise} oldFeed - */ - static async findByPkOld(id) { - if (!id) return null - const feedExpanded = await this.findByPk(id, { - include: { - model: this.sequelize.models.feedEpisode - } - }) - if (!feedExpanded) return null - return this.getOldFeed(feedExpanded) - } - static async fullCreateFromOld(oldFeed) { const feedObj = this.getFromOld(oldFeed) const newFeed = await this.create(feedObj) diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index a92796e8..235d25cd 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -10,6 +10,7 @@ const fs = require('../libs/fsExtra') const date = require('../libs/dateAndTime') const CacheManager = require('../managers/CacheManager') +const RssFeedManager = require('../managers/RssFeedManager') const LibraryController = require('../controllers/LibraryController') const UserController = require('../controllers/UserController') @@ -49,8 +50,6 @@ class ApiRouter { this.podcastManager = Server.podcastManager /** @type {import('../managers/AudioMetadataManager')} */ this.audioMetadataManager = Server.audioMetadataManager - /** @type {import('../managers/RssFeedManager')} */ - this.rssFeedManager = Server.rssFeedManager /** @type {import('../managers/CronManager')} */ this.cronManager = Server.cronManager /** @type {import('../managers/EmailManager')} */ @@ -394,7 +393,7 @@ class ApiRouter { } // Close rss feed - remove from db and emit socket event - await this.rssFeedManager.closeFeedForEntityId(libraryItemId) + await RssFeedManager.closeFeedForEntityId(libraryItemId) // purge cover cache await CacheManager.purgeCoverCache(libraryItemId) @@ -493,7 +492,7 @@ class ApiRouter { * @param {import('../models/Series')} series */ async removeEmptySeries(series) { - await this.rssFeedManager.closeFeedForEntityId(series.id) + await RssFeedManager.closeFeedForEntityId(series.id) Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`) // Remove series from library filter data diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index f0737dac..74798dd6 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -6,21 +6,24 @@ const { getTitleIgnorePrefix, areEquivalent } = require('../utils/index') const parseNameString = require('../utils/parsers/parseNameString') const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata') const globals = require('../utils/globals') +const { readTextFile, filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') + const AudioFileScanner = require('./AudioFileScanner') const Database = require('../Database') -const { readTextFile, filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') -const AudioFile = require('../objects/files/AudioFile') -const CoverManager = require('../managers/CoverManager') -const LibraryFile = require('../objects/files/LibraryFile') const SocketAuthority = require('../SocketAuthority') -const fsExtra = require('../libs/fsExtra') const BookFinder = require('../finders/BookFinder') +const fsExtra = require('../libs/fsExtra') +const EBookFile = require('../objects/files/EBookFile') +const AudioFile = require('../objects/files/AudioFile') +const LibraryFile = require('../objects/files/LibraryFile') + +const RssFeedManager = require('../managers/RssFeedManager') +const CoverManager = require('../managers/CoverManager') const LibraryScan = require('./LibraryScan') const OpfFileScanner = require('./OpfFileScanner') const NfoFileScanner = require('./NfoFileScanner') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') -const EBookFile = require('../objects/files/EBookFile') /** * Metadata for books pulled from files @@ -941,6 +944,9 @@ class BookScanner { id: bookSeriesToRemove } }) + // Close any open feeds for series + await RssFeedManager.closeFeedsForEntityIds(bookSeriesToRemove) + bookSeriesToRemove.forEach((seriesId) => { Database.removeSeriesFromFilterData(libraryId, seriesId) SocketAuthority.emitter('series_removed', { id: seriesId, libraryId }) From 302b651e7b93c6f248a8389f26c86db43e2d4dcd Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 12:38:50 -0600 Subject: [PATCH 4/9] Fix library item unit test --- test/server/controllers/LibraryItemController.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/server/controllers/LibraryItemController.test.js b/test/server/controllers/LibraryItemController.test.js index 3e7c58b2..3fcd1cf8 100644 --- a/test/server/controllers/LibraryItemController.test.js +++ b/test/server/controllers/LibraryItemController.test.js @@ -6,7 +6,6 @@ const Database = require('../../../server/Database') const ApiRouter = require('../../../server/routers/ApiRouter') const LibraryItemController = require('../../../server/controllers/LibraryItemController') const ApiCacheManager = require('../../../server/managers/ApiCacheManager') -const RssFeedManager = require('../../../server/managers/RssFeedManager') const Logger = require('../../../server/Logger') describe('LibraryItemController', () => { @@ -20,8 +19,7 @@ describe('LibraryItemController', () => { await Database.buildModels() apiRouter = new ApiRouter({ - apiCacheManager: new ApiCacheManager(), - rssFeedManager: new RssFeedManager() + apiCacheManager: new ApiCacheManager() }) sinon.stub(Logger, 'info') From 837a180dc1eb35fabbf03f1e068a73fdab5652cc Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 13:14:55 -0600 Subject: [PATCH 5/9] Refactor RssFeedManager.init to use new model only --- server/managers/RssFeedManager.js | 63 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index da0d8dd2..e92c3259 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -11,43 +11,44 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter class RssFeedManager { constructor() {} - async validateFeedEntity(feedObj) { - if (feedObj.entityType === 'collection') { - const collection = await Database.collectionModel.getOldById(feedObj.entityId) - if (!collection) { - Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`) - return false - } - } else if (feedObj.entityType === 'libraryItem') { - const libraryItemExists = await Database.libraryItemModel.checkExistsById(feedObj.entityId) - if (!libraryItemExists) { - Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`) - return false - } - } else if (feedObj.entityType === 'series') { - const series = await Database.seriesModel.findByPk(feedObj.entityId) - if (!series) { - Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`) - return false - } - } else { - Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Invalid entityType "${feedObj.entityType}"`) - return false - } - return true - } - /** - * Validate all feeds and remove invalid + * Remove invalid feeds (invalid if the entity does not exist) */ async init() { - const feeds = await Database.feedModel.getOldFeeds() + const feeds = await Database.feedModel.findAll({ + attributes: ['id', 'entityId', 'entityType', 'title'], + include: [ + { + model: Database.libraryItemModel, + attributes: ['id'] + }, + { + model: Database.collectionModel, + attributes: ['id'] + }, + { + model: Database.seriesModel, + attributes: ['id'] + } + ] + }) + + const feedIdsToRemove = [] for (const feed of feeds) { - // Remove invalid feeds - if (!(await this.validateFeedEntity(feed))) { - await Database.removeFeed(feed.id) + if (!feed.entity) { + Logger.error(`[RssFeedManager] Removing feed "${feed.title}". Entity not found`) + feedIdsToRemove.push(feed.id) } } + + if (feedIdsToRemove.length) { + Logger.info(`[RssFeedManager] Removing ${feedIdsToRemove.length} invalid feeds`) + await Database.feedModel.destroy({ + where: { + id: feedIdsToRemove + } + }) + } } /** From 369c05936bb82efbb5185b3fca00b6321810a599 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 14:07:46 -0600 Subject: [PATCH 6/9] Fix feed create entityUpdatedAt value --- server/models/Feed.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/server/models/Feed.js b/server/models/Feed.js index 3b67ec62..28484cfd 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -279,11 +279,20 @@ class Feed extends Model { static async createFeedForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions) { const media = libraryItem.media + let entityUpdatedAt = libraryItem.updatedAt + + // Podcast feeds should use the most recent episode updatedAt if more recent + if (libraryItem.mediaType === 'podcast') { + entityUpdatedAt = libraryItem.media.podcastEpisodes.reduce((mostRecent, episode) => { + return episode.updatedAt > mostRecent ? episode.updatedAt : mostRecent + }, entityUpdatedAt) + } + const feedObj = { slug, entityType: 'libraryItem', entityId: libraryItem.id, - entityUpdatedAt: libraryItem.updatedAt, + entityUpdatedAt, serverAddress, feedURL: `/feed/${slug}`, imageURL: media.coverPath ? `/feed/${slug}/cover${Path.extname(media.coverPath)}` : `/Logo.png`, @@ -336,9 +345,10 @@ class Feed extends Model { */ static async createFeedForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions) { const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length) - const libraryItemMostRecentlyUpdatedAt = booksWithTracks.reduce((mostRecent, book) => { - return book.libraryItem.updatedAt > mostRecent.libraryItem.updatedAt ? book : mostRecent - }).libraryItem.updatedAt + + const entityUpdatedAt = booksWithTracks.reduce((mostRecent, book) => { + return book.libraryItem.updatedAt > mostRecent ? book.libraryItem.updatedAt : mostRecent + }, collectionExpanded.updatedAt) const firstBookWithCover = booksWithTracks.find((book) => book.coverPath) @@ -355,7 +365,7 @@ class Feed extends Model { slug, entityType: 'collection', entityId: collectionExpanded.id, - entityUpdatedAt: libraryItemMostRecentlyUpdatedAt, + entityUpdatedAt, serverAddress, feedURL: `/feed/${slug}`, imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`, @@ -402,9 +412,9 @@ class Feed extends Model { */ static async createFeedForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions) { const booksWithTracks = seriesExpanded.books.filter((book) => book.includedAudioFiles.length) - const libraryItemMostRecentlyUpdatedAt = booksWithTracks.reduce((mostRecent, book) => { - return book.libraryItem.updatedAt > mostRecent.libraryItem.updatedAt ? book : mostRecent - }).libraryItem.updatedAt + const entityUpdatedAt = booksWithTracks.reduce((mostRecent, book) => { + return book.libraryItem.updatedAt > mostRecent ? book.libraryItem.updatedAt : mostRecent + }, seriesExpanded.updatedAt) const firstBookWithCover = booksWithTracks.find((book) => book.coverPath) @@ -421,7 +431,7 @@ class Feed extends Model { slug, entityType: 'series', entityId: seriesExpanded.id, - entityUpdatedAt: libraryItemMostRecentlyUpdatedAt, + entityUpdatedAt, serverAddress, feedURL: `/feed/${slug}`, imageURL: firstBookWithCover?.coverPath ? `/feed/${slug}/cover${Path.extname(firstBookWithCover.coverPath)}` : `/Logo.png`, From f8fbd3ac8cca4e3076fdecab95cfd160cce58190 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 16:56:59 -0600 Subject: [PATCH 7/9] Migrate Feed updating and build xml to new model --- server/controllers/CollectionController.js | 9 +- server/controllers/RSSFeedController.js | 12 +- server/managers/RssFeedManager.js | 151 +++++++------- server/models/Collection.js | 34 ++- server/models/Feed.js | 227 +++++++++++++++++++-- server/models/FeedEpisode.js | 34 +++ server/models/Series.js | 12 ++ server/objects/Feed.js | 169 --------------- server/objects/FeedEpisode.js | 114 ----------- server/objects/FeedMeta.js | 37 ---- 10 files changed, 373 insertions(+), 426 deletions(-) diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 2445eb50..3e35c08b 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -116,6 +116,7 @@ class CollectionController { } // If books array is passed in then update order in collection + let collectionBooksUpdated = false if (req.body.books?.length) { const collectionBooks = await req.collection.getCollectionBooks({ include: { @@ -134,9 +135,15 @@ class CollectionController { await collectionBooks[i].update({ order: i + 1 }) - wasUpdated = true + collectionBooksUpdated = true } } + + if (collectionBooksUpdated) { + req.collection.changed('updatedAt', true) + await req.collection.save() + wasUpdated = true + } } const jsonExpanded = await req.collection.getOldJsonExpanded() diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index 3cd7736b..e1c9c514 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -90,9 +90,6 @@ class RSSFeedController { async openRSSFeedForCollection(req, res) { const reqBody = req.body || {} - const collection = await Database.collectionModel.findByPk(req.params.collectionId) - if (!collection) return res.sendStatus(404) - // Check request body options exist if (!reqBody.serverAddress || !reqBody.slug || typeof reqBody.serverAddress !== 'string' || typeof reqBody.slug !== 'string') { Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`) @@ -105,7 +102,8 @@ class RSSFeedController { return res.status(400).send('Slug already in use') } - collection.books = await collection.getBooksExpandedWithLibraryItem() + const collection = await Database.collectionModel.getExpandedById(req.params.collectionId) + if (!collection) return res.sendStatus(404) // Check collection has audio tracks if (!collection.books.some((book) => book.includedAudioFiles.length)) { @@ -135,9 +133,6 @@ class RSSFeedController { async openRSSFeedForSeries(req, res) { const reqBody = req.body || {} - const series = await Database.seriesModel.findByPk(req.params.seriesId) - if (!series) return res.sendStatus(404) - // Check request body options exist if (!reqBody.serverAddress || !reqBody.slug || typeof reqBody.serverAddress !== 'string' || typeof reqBody.slug !== 'string') { Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`) @@ -150,7 +145,8 @@ class RSSFeedController { return res.status(400).send('Slug already in use') } - series.books = await series.getBooksExpandedWithLibraryItem() + const series = await Database.seriesModel.getExpandedById(req.params.seriesId) + if (!series) return res.sendStatus(404) // Check series has audio tracks if (!series.books.some((book) => book.includedAudioFiles.length)) { diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index e92c3259..335df110 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -6,7 +6,6 @@ const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const fs = require('../libs/fsExtra') -const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') class RssFeedManager { constructor() {} @@ -69,6 +68,69 @@ class RssFeedManager { return Database.feedModel.findOneOld({ slug }) } + /** + * Feed requires update if the entity (or child entities) has been updated since the feed was last updated + * + * @param {import('../models/Feed')} feed + * @returns {Promise} + */ + async checkFeedRequiresUpdate(feed) { + if (feed.entityType === 'libraryItem') { + feed.entity = await feed.getEntity({ + attributes: ['id', 'updatedAt', 'mediaId', 'mediaType'] + }) + + let newEntityUpdatedAt = feed.entity.updatedAt + + if (feed.entity.mediaType === 'podcast') { + const mostRecentPodcastEpisode = await Database.podcastEpisodeModel.findOne({ + where: { + podcastId: feed.entity.mediaId + }, + attributes: ['id', 'updatedAt'], + order: [['createdAt', 'DESC']] + }) + if (mostRecentPodcastEpisode && mostRecentPodcastEpisode.updatedAt > newEntityUpdatedAt) { + newEntityUpdatedAt = mostRecentPodcastEpisode.updatedAt + } + } + + return newEntityUpdatedAt > feed.entityUpdatedAt + } else if (feed.entityType === 'collection' || feed.entityType === 'series') { + feed.entity = await feed.getEntity({ + attributes: ['id', 'updatedAt'], + include: { + model: Database.bookModel, + attributes: ['id'], + through: { + attributes: [] + }, + include: { + model: Database.libraryItemModel, + attributes: ['id', 'updatedAt'] + } + } + }) + + let newEntityUpdatedAt = feed.entity.updatedAt + + const mostRecentItemUpdatedAt = feed.entity.books.reduce((mostRecent, book) => { + if (book.libraryItem.updatedAt > mostRecent) { + return book.libraryItem.updatedAt + } + return mostRecent + }, 0) + + if (mostRecentItemUpdatedAt > newEntityUpdatedAt) { + newEntityUpdatedAt = mostRecentItemUpdatedAt + } + + return newEntityUpdatedAt > feed.entityUpdatedAt + } else { + throw new Error('Invalid feed entity type') + } + } + /** * GET: /feed/:slug * @@ -76,88 +138,23 @@ class RssFeedManager { * @param {Response} res */ async getFeed(req, res) { - const feed = await this.findFeedBySlug(req.params.slug) + let feed = await Database.feedModel.findOne({ + where: { + slug: req.params.slug + } + }) if (!feed) { Logger.warn(`[RssFeedManager] Feed not found ${req.params.slug}`) res.sendStatus(404) return } - // Check if feed needs to be updated - if (feed.entityType === 'libraryItem') { - const libraryItem = await Database.libraryItemModel.getOldById(feed.entityId) - - let mostRecentlyUpdatedAt = libraryItem.updatedAt - if (libraryItem.isPodcast) { - libraryItem.media.episodes.forEach((episode) => { - if (episode.updatedAt > mostRecentlyUpdatedAt) mostRecentlyUpdatedAt = episode.updatedAt - }) - } - - if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) { - Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`) - - feed.updateFromItem(libraryItem) - await Database.updateFeed(feed) - } - } else if (feed.entityType === 'collection') { - const collection = await Database.collectionModel.findByPk(feed.entityId, { - include: Database.collectionBookModel - }) - if (collection) { - const collectionExpanded = await collection.getOldJsonExpanded() - - // Find most recently updated item in collection - let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate - // Check for most recently updated book - collectionExpanded.books.forEach((libraryItem) => { - if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) { - mostRecentlyUpdatedAt = libraryItem.updatedAt - } - }) - // Check for most recently added collection book - collection.collectionBooks.forEach((collectionBook) => { - if (collectionBook.createdAt.valueOf() > mostRecentlyUpdatedAt) { - mostRecentlyUpdatedAt = collectionBook.createdAt.valueOf() - } - }) - const hasBooksRemoved = collection.collectionBooks.length < feed.episodes.length - - if (!feed.entityUpdatedAt || hasBooksRemoved || mostRecentlyUpdatedAt > feed.entityUpdatedAt) { - Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`) - - feed.updateFromCollection(collectionExpanded) - await Database.updateFeed(feed) - } - } - } else if (feed.entityType === 'series') { - const series = await Database.seriesModel.findByPk(feed.entityId) - if (series) { - const seriesJson = series.toOldJSON() - - // Get books in series that have audio tracks - seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) - - // Find most recently updated item in series - let mostRecentlyUpdatedAt = seriesJson.updatedAt - let totalTracks = 0 // Used to detect series items removed - seriesJson.books.forEach((libraryItem) => { - totalTracks += libraryItem.media.tracks.length - if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) { - mostRecentlyUpdatedAt = libraryItem.updatedAt - } - }) - if (totalTracks !== feed.episodes.length) { - mostRecentlyUpdatedAt = Date.now() - } - - if (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt) { - Logger.debug(`[RssFeedManager] Updating RSS feed for series "${seriesJson.name}"`) - - feed.updateFromSeries(seriesJson) - await Database.updateFeed(feed) - } - } + const feedRequiresUpdate = await this.checkFeedRequiresUpdate(feed) + if (feedRequiresUpdate) { + Logger.info(`[RssFeedManager] Feed "${feed.title}" requires update - updating feed`) + feed = await feed.updateFeedForEntity() + } else { + feed.feedEpisodes = await feed.getFeedEpisodes() } const xml = feed.buildXml(req.originalHostPrefix) diff --git a/server/models/Collection.js b/server/models/Collection.js index 0fb622dd..558ae843 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -1,7 +1,6 @@ const { DataTypes, Model, Sequelize } = require('sequelize') const oldCollection = require('../objects/Collection') -const Logger = require('../Logger') class Collection extends Model { constructor(values, options) { @@ -121,6 +120,39 @@ class Collection extends Model { .filter((c) => c) } + /** + * + * @param {string} collectionId + * @returns {Promise} + */ + static async getExpandedById(collectionId) { + return this.findByPk(collectionId, { + include: [ + { + model: this.sequelize.models.book, + include: [ + { + model: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ] + } + ], + order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']] + }) + } + /** * Get old collection from Collection * @param {Collection} collectionExpanded diff --git a/server/models/Feed.js b/server/models/Feed.js index 28484cfd..f6ee4e3c 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -2,6 +2,9 @@ const Path = require('path') const { DataTypes, Model } = require('sequelize') const oldFeed = require('../objects/Feed') const areEquivalent = require('../utils/areEquivalent') +const Logger = require('../Logger') + +const RSS = require('../libs/rss') /** * @typedef FeedOptions @@ -66,6 +69,8 @@ class Feed extends Model { /** @type {Date} */ this.updatedAt + // Expanded properties + /** @type {import('./FeedEpisode')[]} - only set if expanded */ this.feedEpisodes } @@ -272,11 +277,11 @@ class Feed extends Model { * @param {import('./LibraryItem').LibraryItemExpanded} libraryItem * @param {string} slug * @param {string} serverAddress - * @param {FeedOptions} feedOptions + * @param {FeedOptions} [feedOptions=null] * - * @returns {Promise} + * @returns {Feed} */ - static async createFeedForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions) { + static getFeedObjForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions = null) { const media = libraryItem.media let entityUpdatedAt = libraryItem.updatedAt @@ -302,14 +307,33 @@ class Feed extends Model { author: libraryItem.mediaType === 'podcast' ? media.author : media.authorName, podcastType: libraryItem.mediaType === 'podcast' ? media.podcastType : 'serial', language: media.language, - preventIndexing: feedOptions.preventIndexing, - ownerName: feedOptions.ownerName, - ownerEmail: feedOptions.ownerEmail, explicit: media.explicit, coverPath: media.coverPath, userId } + if (feedOptions) { + feedObj.preventIndexing = feedOptions.preventIndexing + feedObj.ownerName = feedOptions.ownerName + feedObj.ownerEmail = feedOptions.ownerEmail + } + + return feedObj + } + + /** + * + * @param {string} userId + * @param {import('./LibraryItem').LibraryItemExpanded} libraryItem + * @param {string} slug + * @param {string} serverAddress + * @param {FeedOptions} feedOptions + * + * @returns {Promise} + */ + static async createFeedForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions) { + const feedObj = this.getFeedObjForLibraryItem(userId, libraryItem, slug, serverAddress, feedOptions) + /** @type {typeof import('./FeedEpisode')} */ const feedEpisodeModel = this.sequelize.models.feedEpisode @@ -339,11 +363,11 @@ class Feed extends Model { * @param {import('./Collection')} collectionExpanded * @param {string} slug * @param {string} serverAddress - * @param {FeedOptions} feedOptions + * @param {FeedOptions} [feedOptions=null] * - * @returns {Promise} + * @returns {{ feedObj: Feed, booksWithTracks: import('./Book').BookExpandedWithLibraryItem[] }} */ - static async createFeedForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions) { + static getFeedObjForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions = null) { const booksWithTracks = collectionExpanded.books.filter((book) => book.includedAudioFiles.length) const entityUpdatedAt = booksWithTracks.reduce((mostRecent, book) => { @@ -374,14 +398,36 @@ class Feed extends Model { description: collectionExpanded.description || '', author, podcastType: 'serial', - preventIndexing: feedOptions.preventIndexing, - ownerName: feedOptions.ownerName, - ownerEmail: feedOptions.ownerEmail, explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit coverPath: firstBookWithCover?.coverPath || null, userId } + if (feedOptions) { + feedObj.preventIndexing = feedOptions.preventIndexing + feedObj.ownerName = feedOptions.ownerName + feedObj.ownerEmail = feedOptions.ownerEmail + } + + return { + feedObj, + booksWithTracks + } + } + + /** + * + * @param {string} userId + * @param {import('./Collection')} collectionExpanded + * @param {string} slug + * @param {string} serverAddress + * @param {FeedOptions} feedOptions + * + * @returns {Promise} + */ + static async createFeedForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions) { + const { feedObj, booksWithTracks } = this.getFeedObjForCollection(userId, collectionExpanded, slug, serverAddress, feedOptions) + /** @type {typeof import('./FeedEpisode')} */ const feedEpisodeModel = this.sequelize.models.feedEpisode @@ -406,11 +452,11 @@ class Feed extends Model { * @param {import('./Series')} seriesExpanded * @param {string} slug * @param {string} serverAddress - * @param {FeedOptions} feedOptions + * @param {FeedOptions} [feedOptions=null] * - * @returns {Promise} + * @returns {{ feedObj: Feed, booksWithTracks: import('./Book').BookExpandedWithLibraryItem[] }} */ - static async createFeedForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions) { + static getFeedObjForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions = null) { const booksWithTracks = seriesExpanded.books.filter((book) => book.includedAudioFiles.length) const entityUpdatedAt = booksWithTracks.reduce((mostRecent, book) => { return book.libraryItem.updatedAt > mostRecent ? book.libraryItem.updatedAt : mostRecent @@ -440,14 +486,36 @@ class Feed extends Model { description: seriesExpanded.description || '', author, podcastType: 'serial', - preventIndexing: feedOptions.preventIndexing, - ownerName: feedOptions.ownerName, - ownerEmail: feedOptions.ownerEmail, explicit: booksWithTracks.some((book) => book.explicit), // If any book is explicit, the feed is explicit coverPath: firstBookWithCover?.coverPath || null, userId } + if (feedOptions) { + feedObj.preventIndexing = feedOptions.preventIndexing + feedObj.ownerName = feedOptions.ownerName + feedObj.ownerEmail = feedOptions.ownerEmail + } + + return { + feedObj, + booksWithTracks + } + } + + /** + * + * @param {string} userId + * @param {import('./Series')} seriesExpanded + * @param {string} slug + * @param {string} serverAddress + * @param {FeedOptions} feedOptions + * + * @returns {Promise} + */ + static async createFeedForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions) { + const { feedObj, booksWithTracks } = this.getFeedObjForSeries(userId, seriesExpanded, slug, serverAddress, feedOptions) + /** @type {typeof import('./FeedEpisode')} */ const feedEpisodeModel = this.sequelize.models.feedEpisode @@ -460,7 +528,7 @@ class Feed extends Model { return feed } catch (error) { - Logger.error(`[Feed] Error creating feed for collection ${collectionExpanded.id}`, error) + Logger.error(`[Feed] Error creating feed for series ${seriesExpanded.id}`, error) await transaction.rollback() return null } @@ -580,12 +648,133 @@ class Feed extends Model { }) } + /** + * + * @returns {Promise} + */ + async updateFeedForEntity() { + /** @type {typeof import('./FeedEpisode')} */ + const feedEpisodeModel = this.sequelize.models.feedEpisode + + let feedObj = null + let feedEpisodeCreateFunc = null + let feedEpisodeCreateFuncEntity = null + + if (this.entityType === 'libraryItem') { + /** @type {typeof import('./LibraryItem')} */ + const libraryItemModel = this.sequelize.models.libraryItem + + const itemExpanded = await libraryItemModel.getExpandedById(this.entityId) + feedObj = Feed.getFeedObjForLibraryItem(this.userId, itemExpanded, this.slug, this.serverAddress) + + feedEpisodeCreateFuncEntity = itemExpanded + if (itemExpanded.mediaType === 'podcast') { + feedEpisodeCreateFunc = feedEpisodeModel.createFromPodcastEpisodes.bind(feedEpisodeModel) + } else { + feedEpisodeCreateFunc = feedEpisodeModel.createFromAudiobookTracks.bind(feedEpisodeModel) + } + } else if (this.entityType === 'collection') { + /** @type {typeof import('./Collection')} */ + const collectionModel = this.sequelize.models.collection + + const collectionExpanded = await collectionModel.getExpandedById(this.entityId) + const feedObjData = Feed.getFeedObjForCollection(this.userId, collectionExpanded, this.slug, this.serverAddress) + feedObj = feedObjData.feedObj + feedEpisodeCreateFuncEntity = feedObjData.booksWithTracks + feedEpisodeCreateFunc = feedEpisodeModel.createFromBooks.bind(feedEpisodeModel) + } else if (this.entityType === 'series') { + /** @type {typeof import('./Series')} */ + const seriesModel = this.sequelize.models.series + + const seriesExpanded = await seriesModel.getExpandedById(this.entityId) + const feedObjData = Feed.getFeedObjForSeries(this.userId, seriesExpanded, this.slug, this.serverAddress) + feedObj = feedObjData.feedObj + feedEpisodeCreateFuncEntity = feedObjData.booksWithTracks + feedEpisodeCreateFunc = feedEpisodeModel.createFromBooks.bind(feedEpisodeModel) + } else { + Logger.error(`[Feed] Invalid entity type ${this.entityType} for feed ${this.id}`) + return null + } + + const transaction = await this.sequelize.transaction() + try { + const updatedFeed = await this.update(feedObj, { transaction }) + + // Remove existing feed episodes + await feedEpisodeModel.destroy({ + where: { + feedId: this.id + }, + transaction + }) + + // Create new feed episodes + updatedFeed.feedEpisodes = await feedEpisodeCreateFunc(feedEpisodeCreateFuncEntity, updatedFeed, this.slug, transaction) + + await transaction.commit() + + return updatedFeed + } catch (error) { + Logger.error(`[Feed] Error updating feed ${this.entityId}`, error) + await transaction.rollback() + + return null + } + } + getEntity(options) { if (!this.entityType) return Promise.resolve(null) const mixinMethodName = `get${this.sequelize.uppercaseFirst(this.entityType)}` return this[mixinMethodName](options) } + /** + * + * @param {string} hostPrefix + */ + buildXml(hostPrefix) { + const blockTags = [{ 'itunes:block': 'yes' }, { 'googleplay:block': 'yes' }] + const rssData = { + title: this.title, + description: this.description || '', + generator: 'Audiobookshelf', + feed_url: `${hostPrefix}${this.feedURL}`, + site_url: `${hostPrefix}${this.siteURL}`, + image_url: `${hostPrefix}${this.imageURL}`, + custom_namespaces: { + itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd', + psc: 'http://podlove.org/simple-chapters', + podcast: 'https://podcastindex.org/namespace/1.0', + googleplay: 'http://www.google.com/schemas/play-podcasts/1.0' + }, + custom_elements: [ + { language: this.language || 'en' }, + { author: this.author || 'advplyr' }, + { 'itunes:author': this.author || 'advplyr' }, + { 'itunes:summary': this.description || '' }, + { 'itunes:type': this.podcastType }, + { + 'itunes:image': { + _attr: { + href: `${hostPrefix}${this.imageURL}` + } + } + }, + { + 'itunes:owner': [{ 'itunes:name': this.ownerName || this.author || '' }, { 'itunes:email': this.ownerEmail || '' }] + }, + { 'itunes:explicit': !!this.explicit }, + ...(this.preventIndexing ? blockTags : []) + ] + } + + const rssfeed = new RSS(rssData) + this.feedEpisodes.forEach((ep) => { + rssfeed.item(ep.getRSSData(hostPrefix)) + }) + return rssfeed.xml() + } + toOldJSON() { const episodes = this.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) return { diff --git a/server/models/FeedEpisode.js b/server/models/FeedEpisode.js index 4979ae6d..b14f1b7b 100644 --- a/server/models/FeedEpisode.js +++ b/server/models/FeedEpisode.js @@ -3,6 +3,7 @@ const { DataTypes, Model } = require('sequelize') const uuidv4 = require('uuid').v4 const Logger = require('../Logger') const date = require('../libs/dateAndTime') +const { secondsToTimestamp } = require('../utils') class FeedEpisode extends Model { constructor(values, options) { @@ -13,6 +14,8 @@ class FeedEpisode extends Model { /** @type {string} */ this.title /** @type {string} */ + this.author + /** @type {string} */ this.description /** @type {string} */ this.siteURL @@ -301,6 +304,37 @@ class FeedEpisode extends Model { fullPath: this.filePath } } + + /** + * + * @param {string} hostPrefix + */ + getRSSData(hostPrefix) { + return { + title: this.title, + description: this.description || '', + url: `${hostPrefix}${this.siteURL}`, + guid: `${hostPrefix}${this.enclosureURL}`, + author: this.author, + date: this.pubDate, + enclosure: { + url: `${hostPrefix}${this.enclosureURL}`, + type: this.enclosureType, + size: this.enclosureSize + }, + custom_elements: [ + { 'itunes:author': this.author }, + { 'itunes:duration': secondsToTimestamp(this.duration) }, + { 'itunes:summary': this.description || '' }, + { + 'itunes:explicit': !!this.explicit + }, + { 'itunes:episodeType': this.episodeType }, + { 'itunes:season': this.season }, + { 'itunes:episode': this.episode } + ] + } + } } module.exports = FeedEpisode diff --git a/server/models/Series.js b/server/models/Series.js index 4fa6b403..c4bc1594 100644 --- a/server/models/Series.js +++ b/server/models/Series.js @@ -54,6 +54,18 @@ class Series extends Model { }) } + /** + * + * @param {string} seriesId + * @returns {Promise} + */ + static async getExpandedById(seriesId) { + const series = await this.findByPk(seriesId) + if (!series) return null + series.books = await series.getBooksExpandedWithLibraryItem() + return series + } + /** * Initialize model * @param {import('../Database').sequelize} sequelize diff --git a/server/objects/Feed.js b/server/objects/Feed.js index 668544d9..4da7a995 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -1,15 +1,6 @@ -const Path = require('path') -const uuidv4 = require('uuid').v4 const FeedMeta = require('./FeedMeta') const FeedEpisode = require('./FeedEpisode') -const date = require('../libs/dateAndTime') -const RSS = require('../libs/rss') -const { createNewSortInstance } = require('../libs/fastSort') -const naturalSort = createNewSortInstance({ - comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare -}) - class Feed { constructor(feed) { this.id = null @@ -82,165 +73,5 @@ class Feed { if (!episode) return null return episode.fullPath } - - /** - * If chapters for an audiobook match the audio tracks then use chapter titles instead of audio file names - * - * @param {import('../objects/LibraryItem')} libraryItem - * @returns {boolean} - */ - checkUseChapterTitlesForEpisodes(libraryItem) { - const tracks = libraryItem.media.tracks - const chapters = libraryItem.media.chapters - if (tracks.length !== chapters.length) return false - for (let i = 0; i < tracks.length; i++) { - if (Math.abs(chapters[i].start - tracks[i].startOffset) >= 1) { - return false - } - } - return true - } - - updateFromItem(libraryItem) { - const media = libraryItem.media - const mediaMetadata = media.metadata - const isPodcast = libraryItem.mediaType === 'podcast' - const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName - - this.entityUpdatedAt = libraryItem.updatedAt - this.coverPath = media.coverPath || null - - const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null - - this.meta.title = mediaMetadata.title - this.meta.description = mediaMetadata.description - this.meta.author = author - this.meta.imageUrl = media.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png` - this.meta.explicit = !!mediaMetadata.explicit - this.meta.type = mediaMetadata.type - this.meta.language = mediaMetadata.language - - this.episodes = [] - if (isPodcast) { - // PODCAST EPISODES - media.episodes.forEach((episode) => { - if (episode.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = episode.updatedAt - - const feedEpisode = new FeedEpisode() - feedEpisode.setFromPodcastEpisode(libraryItem, this.serverAddress, this.slug, episode, this.meta) - this.episodes.push(feedEpisode) - }) - } else { - // AUDIOBOOK EPISODES - const useChapterTitles = this.checkUseChapterTitlesForEpisodes(libraryItem) - media.tracks.forEach((audioTrack) => { - const feedEpisode = new FeedEpisode() - feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles) - this.episodes.push(feedEpisode) - }) - } - - this.updatedAt = Date.now() - } - - updateFromCollection(collectionExpanded) { - const itemsWithTracks = collectionExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) - const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) - - this.entityUpdatedAt = collectionExpanded.lastUpdate - this.coverPath = firstItemWithCover?.media.coverPath || null - - const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null - - this.meta.title = collectionExpanded.name - this.meta.description = collectionExpanded.description || '' - this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png` - this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit - - this.episodes = [] - - // Used for calculating pubdate - const earliestItemAddedAt = itemsWithTracks.reduce((earliest, item) => (item.addedAt < earliest ? item.addedAt : earliest), itemsWithTracks[0].addedAt) - - itemsWithTracks.forEach((item, index) => { - if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt - - const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item) - item.media.tracks.forEach((audioTrack) => { - const feedEpisode = new FeedEpisode() - - // Offset pubdate to ensure correct order - let trackTimeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset track - trackTimeOffset += index * 1000 // Offset item - const episodePubDateOverride = date.format(new Date(earliestItemAddedAt + trackTimeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') - feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, episodePubDateOverride) - this.episodes.push(feedEpisode) - }) - }) - - this.updatedAt = Date.now() - } - - updateFromSeries(seriesExpanded) { - let itemsWithTracks = seriesExpanded.books.filter((libraryItem) => libraryItem.media.tracks.length) - // Sort series items by series sequence - itemsWithTracks = naturalSort(itemsWithTracks).asc((li) => li.media.metadata.getSeriesSequence(seriesExpanded.id)) - - const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) - - this.entityUpdatedAt = seriesExpanded.updatedAt - this.coverPath = firstItemWithCover?.media.coverPath || null - - const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null - - this.meta.title = seriesExpanded.name - this.meta.description = seriesExpanded.description || '' - this.meta.author = this.getAuthorsStringFromLibraryItems(itemsWithTracks) - this.meta.imageUrl = this.coverPath ? `/feed/${this.slug}/cover${coverFileExtension}` : `/Logo.png` - this.meta.explicit = !!itemsWithTracks.some((li) => li.media.metadata.explicit) // explicit if any item is explicit - - this.episodes = [] - - // Used for calculating pubdate - const earliestItemAddedAt = itemsWithTracks.reduce((earliest, item) => (item.addedAt < earliest ? item.addedAt : earliest), itemsWithTracks[0].addedAt) - - itemsWithTracks.forEach((item, index) => { - if (item.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = item.updatedAt - - const useChapterTitles = this.checkUseChapterTitlesForEpisodes(item) - item.media.tracks.forEach((audioTrack) => { - const feedEpisode = new FeedEpisode() - - // Offset pubdate to ensure correct order - let trackTimeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset track - trackTimeOffset += index * 1000 // Offset item - const episodePubDateOverride = date.format(new Date(earliestItemAddedAt + trackTimeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') - feedEpisode.setFromAudiobookTrack(item, this.serverAddress, this.slug, audioTrack, this.meta, useChapterTitles, episodePubDateOverride) - this.episodes.push(feedEpisode) - }) - }) - - this.updatedAt = Date.now() - } - - buildXml(originalHostPrefix) { - var rssfeed = new RSS(this.meta.getRSSData(originalHostPrefix)) - this.episodes.forEach((ep) => { - rssfeed.item(ep.getRSSData(originalHostPrefix)) - }) - return rssfeed.xml() - } - - getAuthorsStringFromLibraryItems(libraryItems) { - let itemAuthors = [] - libraryItems.forEach((item) => itemAuthors.push(...item.media.metadata.authors.map((au) => au.name))) - itemAuthors = [...new Set(itemAuthors)] // Filter out dupes - let author = itemAuthors.slice(0, 3).join(', ') - if (itemAuthors.length > 3) { - author += ' & more' - } - return author - } } module.exports = Feed diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js index 13d590ff..4438eb52 100644 --- a/server/objects/FeedEpisode.js +++ b/server/objects/FeedEpisode.js @@ -1,8 +1,3 @@ -const Path = require('path') -const uuidv4 = require('uuid').v4 -const date = require('../libs/dateAndTime') -const { secondsToTimestamp } = require('../utils/index') - class FeedEpisode { constructor(episode) { this.id = null @@ -68,114 +63,5 @@ class FeedEpisode { fullPath: this.fullPath } } - - setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, meta) { - const contentFileExtension = Path.extname(episode.audioFile.metadata.filename) - const contentUrl = `/feed/${slug}/item/${episode.id}/media${contentFileExtension}` - const media = libraryItem.media - const mediaMetadata = media.metadata - - this.id = episode.id - this.title = episode.title - this.description = episode.description || '' - this.enclosure = { - url: `${contentUrl}`, - type: episode.audioTrack.mimeType, - size: episode.size - } - this.pubDate = episode.pubDate - this.link = meta.link - this.author = meta.author - this.explicit = mediaMetadata.explicit - this.duration = episode.duration - this.season = episode.season - this.episode = episode.episode - this.episodeType = episode.episodeType - this.libraryItemId = libraryItem.id - this.episodeId = episode.id - this.trackIndex = 0 - this.fullPath = episode.audioFile.metadata.path - } - - /** - * - * @param {import('../objects/LibraryItem')} libraryItem - * @param {string} serverAddress - * @param {string} slug - * @param {import('../objects/files/AudioTrack')} audioTrack - * @param {Object} meta - * @param {boolean} useChapterTitles - * @param {string} [pubDateOverride] Used for series & collections to ensure correct episode order - */ - setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, useChapterTitles, pubDateOverride = null) { - // Example: Fri, 04 Feb 2015 00:00:00 GMT - let timeOffset = isNaN(audioTrack.index) ? 0 : Number(audioTrack.index) * 1000 // Offset pubdate to ensure correct order - let episodeId = uuidv4() - - // e.g. Track 1 will have a pub date before Track 2 - const audiobookPubDate = pubDateOverride || date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]') - - const contentFileExtension = Path.extname(audioTrack.metadata.filename) - const contentUrl = `/feed/${slug}/item/${episodeId}/media${contentFileExtension}` - const media = libraryItem.media - const mediaMetadata = media.metadata - - let title = audioTrack.title - if (libraryItem.media.tracks.length == 1) { - // If audiobook is a single file, use book title instead of chapter/file title - title = libraryItem.media.metadata.title - } else { - if (useChapterTitles) { - // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title - const matchingChapter = libraryItem.media.chapters.find((ch) => Math.abs(ch.start - audioTrack.startOffset) < 1) - if (matchingChapter?.title) title = matchingChapter.title - } - } - - this.id = episodeId - this.title = title - this.description = mediaMetadata.description || '' - this.enclosure = { - url: `${contentUrl}`, - type: audioTrack.mimeType, - size: audioTrack.metadata.size - } - this.pubDate = audiobookPubDate - this.link = meta.link - this.author = meta.author - this.explicit = mediaMetadata.explicit - this.duration = audioTrack.duration - this.libraryItemId = libraryItem.id - this.episodeId = null - this.trackIndex = audioTrack.index - this.fullPath = audioTrack.metadata.path - } - - getRSSData(hostPrefix) { - return { - title: this.title, - description: this.description || '', - url: `${hostPrefix}${this.link}`, - guid: `${hostPrefix}${this.enclosure.url}`, - author: this.author, - date: this.pubDate, - enclosure: { - url: `${hostPrefix}${this.enclosure.url}`, - type: this.enclosure.type, - size: this.enclosure.size - }, - custom_elements: [ - { 'itunes:author': this.author }, - { 'itunes:duration': secondsToTimestamp(this.duration) }, - { 'itunes:summary': this.description || '' }, - { - 'itunes:explicit': !!this.explicit - }, - { 'itunes:episodeType': this.episodeType }, - { 'itunes:season': this.season }, - { 'itunes:episode': this.episode } - ] - } - } } module.exports = FeedEpisode diff --git a/server/objects/FeedMeta.js b/server/objects/FeedMeta.js index e439fe8f..61fccaa3 100644 --- a/server/objects/FeedMeta.js +++ b/server/objects/FeedMeta.js @@ -59,42 +59,5 @@ class FeedMeta { ownerEmail: this.ownerEmail } } - - getRSSData(hostPrefix) { - const blockTags = [{ 'itunes:block': 'yes' }, { 'googleplay:block': 'yes' }] - return { - title: this.title, - description: this.description || '', - generator: 'Audiobookshelf', - feed_url: `${hostPrefix}${this.feedUrl}`, - site_url: `${hostPrefix}${this.link}`, - image_url: `${hostPrefix}${this.imageUrl}`, - custom_namespaces: { - itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd', - psc: 'http://podlove.org/simple-chapters', - podcast: 'https://podcastindex.org/namespace/1.0', - googleplay: 'http://www.google.com/schemas/play-podcasts/1.0' - }, - custom_elements: [ - { language: this.language || 'en' }, - { author: this.author || 'advplyr' }, - { 'itunes:author': this.author || 'advplyr' }, - { 'itunes:summary': this.description || '' }, - { 'itunes:type': this.type }, - { - 'itunes:image': { - _attr: { - href: `${hostPrefix}${this.imageUrl}` - } - } - }, - { - 'itunes:owner': [{ 'itunes:name': this.ownerName || this.author || '' }, { 'itunes:email': this.ownerEmail || '' }] - }, - { 'itunes:explicit': !!this.explicit }, - ...(this.preventIndexing ? blockTags : []) - ] - } - } } module.exports = FeedMeta From de8a9304d21dc53b607beee4396e3abf685652ac Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 17:05:57 -0600 Subject: [PATCH 8/9] Remove unused old feed methods --- server/Database.js | 15 ----- server/models/Feed.js | 112 ----------------------------------- server/models/FeedEpisode.js | 33 ----------- 3 files changed, 160 deletions(-) diff --git a/server/Database.js b/server/Database.js index 95e13c6b..afb09dae 100644 --- a/server/Database.js +++ b/server/Database.js @@ -444,21 +444,6 @@ class Database { return updated } - async createFeed(oldFeed) { - if (!this.sequelize) return false - await this.models.feed.fullCreateFromOld(oldFeed) - } - - updateFeed(oldFeed) { - if (!this.sequelize) return false - return this.models.feed.fullUpdateFromOld(oldFeed) - } - - async removeFeed(feedId) { - if (!this.sequelize) return false - await this.models.feed.removeById(feedId) - } - async createBulkBookAuthors(bookAuthors) { if (!this.sequelize) return false await this.models.bookAuthor.bulkCreate(bookAuthors) diff --git a/server/models/Feed.js b/server/models/Feed.js index f6ee4e3c..5aaa4856 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -1,7 +1,6 @@ const Path = require('path') const { DataTypes, Model } = require('sequelize') const oldFeed = require('../objects/Feed') -const areEquivalent = require('../utils/areEquivalent') const Logger = require('../Logger') const RSS = require('../libs/rss') @@ -143,20 +142,6 @@ class Feed extends Model { ) } - /** - * Find all library item ids that have an open feed (used in library filter) - * @returns {Promise} array of library item ids - */ - static async findAllLibraryItemIds() { - const feeds = await this.findAll({ - attributes: ['entityId'], - where: { - entityType: 'libraryItem' - } - }) - return feeds.map((f) => f.entityId).filter((f) => f) || [] - } - /** * Find feed where and return oldFeed * @param {Object} where sequelize where object @@ -174,103 +159,6 @@ class Feed extends Model { return this.getOldFeed(feedExpanded) } - static async fullCreateFromOld(oldFeed) { - const feedObj = this.getFromOld(oldFeed) - const newFeed = await this.create(feedObj) - - if (oldFeed.episodes?.length) { - for (const oldFeedEpisode of oldFeed.episodes) { - const feedEpisode = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) - feedEpisode.feedId = newFeed.id - await this.sequelize.models.feedEpisode.create(feedEpisode) - } - } - } - - static async fullUpdateFromOld(oldFeed) { - const oldFeedEpisodes = oldFeed.episodes || [] - const feedObj = this.getFromOld(oldFeed) - - const existingFeed = await this.findByPk(feedObj.id, { - include: this.sequelize.models.feedEpisode - }) - if (!existingFeed) return false - - let hasUpdates = false - - // Remove and update existing feed episodes - for (const feedEpisode of existingFeed.feedEpisodes) { - const oldFeedEpisode = oldFeedEpisodes.find((ep) => ep.id === feedEpisode.id) - // Episode removed - if (!oldFeedEpisode) { - feedEpisode.destroy() - } else { - let episodeHasUpdates = false - const oldFeedEpisodeCleaned = this.sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) - for (const key in oldFeedEpisodeCleaned) { - if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) { - episodeHasUpdates = true - } - } - if (episodeHasUpdates) { - await feedEpisode.update(oldFeedEpisodeCleaned) - hasUpdates = true - } - } - } - - // Add new feed episodes - for (const episode of oldFeedEpisodes) { - if (!existingFeed.feedEpisodes.some((fe) => fe.id === episode.id)) { - await this.sequelize.models.feedEpisode.createFromOld(feedObj.id, episode) - hasUpdates = true - } - } - - let feedHasUpdates = false - for (const key in feedObj) { - let existingValue = existingFeed[key] - if (existingValue instanceof Date) existingValue = existingValue.valueOf() - - if (!areEquivalent(existingValue, feedObj[key])) { - feedHasUpdates = true - } - } - - if (feedHasUpdates) { - await existingFeed.update(feedObj) - hasUpdates = true - } - - return hasUpdates - } - - static getFromOld(oldFeed) { - const oldFeedMeta = oldFeed.meta || {} - return { - id: oldFeed.id, - slug: oldFeed.slug, - entityType: oldFeed.entityType, - entityId: oldFeed.entityId, - entityUpdatedAt: oldFeed.entityUpdatedAt, - serverAddress: oldFeed.serverAddress, - feedURL: oldFeed.feedUrl, - coverPath: oldFeed.coverPath || null, - imageURL: oldFeedMeta.imageUrl, - siteURL: oldFeedMeta.link, - title: oldFeedMeta.title, - description: oldFeedMeta.description, - author: oldFeedMeta.author, - podcastType: oldFeedMeta.type || null, - language: oldFeedMeta.language || null, - ownerName: oldFeedMeta.ownerName || null, - ownerEmail: oldFeedMeta.ownerEmail || null, - explicit: !!oldFeedMeta.explicit, - preventIndexing: !!oldFeedMeta.preventIndexing, - userId: oldFeed.userId - } - } - /** * * @param {string} userId diff --git a/server/models/FeedEpisode.js b/server/models/FeedEpisode.js index b14f1b7b..31b26800 100644 --- a/server/models/FeedEpisode.js +++ b/server/models/FeedEpisode.js @@ -47,39 +47,6 @@ class FeedEpisode extends Model { this.updatedAt } - /** - * Create feed episode from old model - * - * @param {string} feedId - * @param {Object} oldFeedEpisode - * @returns {Promise} - */ - static createFromOld(feedId, oldFeedEpisode) { - const newEpisode = this.getFromOld(oldFeedEpisode) - newEpisode.feedId = feedId - return this.create(newEpisode) - } - - static getFromOld(oldFeedEpisode) { - return { - id: oldFeedEpisode.id, - title: oldFeedEpisode.title, - author: oldFeedEpisode.author, - description: oldFeedEpisode.description, - siteURL: oldFeedEpisode.link, - enclosureURL: oldFeedEpisode.enclosure?.url || null, - enclosureType: oldFeedEpisode.enclosure?.type || null, - enclosureSize: oldFeedEpisode.enclosure?.size || null, - pubDate: oldFeedEpisode.pubDate, - season: oldFeedEpisode.season || null, - episode: oldFeedEpisode.episode || null, - episodeType: oldFeedEpisode.episodeType || null, - duration: oldFeedEpisode.duration, - filePath: oldFeedEpisode.fullPath, - explicit: !!oldFeedEpisode.explicit - } - } - /** * * @param {import('./LibraryItem').LibraryItemExpanded} libraryItemExpanded From b39268ccb0fb5fae85cfcc2847be0971f7792b8b Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 15 Dec 2024 17:54:36 -0600 Subject: [PATCH 9/9] Remove old Feed/FeedEpisode/FeedMeta objects --- server/controllers/LibraryController.js | 2 +- server/controllers/LibraryItemController.js | 2 +- server/controllers/RSSFeedController.js | 10 +-- server/controllers/SeriesController.js | 2 +- server/managers/RssFeedManager.js | 55 +++++++++--- server/models/Collection.js | 4 +- server/models/Feed.js | 83 +++---------------- server/models/FeedEpisode.js | 4 +- server/models/LibraryItem.js | 2 +- server/models/Playlist.js | 7 -- server/objects/Feed.js | 77 ----------------- server/objects/FeedEpisode.js | 67 --------------- server/objects/FeedMeta.js | 63 -------------- server/utils/queries/libraryFilters.js | 14 ++-- .../utils/queries/libraryItemsBookFilters.js | 12 +-- .../queries/libraryItemsPodcastFilters.js | 4 +- server/utils/queries/seriesFilters.js | 2 +- 17 files changed, 84 insertions(+), 326 deletions(-) delete mode 100644 server/objects/Feed.js delete mode 100644 server/objects/FeedEpisode.js delete mode 100644 server/objects/FeedMeta.js diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index da9859f2..6e7fbea3 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -762,7 +762,7 @@ class LibraryController { if (include.includes('rssfeed')) { const feedObj = await RssFeedManager.findFeedForEntityId(seriesJson.id) - seriesJson.rssFeed = feedObj?.toJSONMinified() || null + seriesJson.rssFeed = feedObj?.toOldJSONMinified() || null } res.json(seriesJson) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 5fac31aa..f2a4383e 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -51,7 +51,7 @@ class LibraryItemController { if (includeEntities.includes('rssfeed')) { const feedData = await RssFeedManager.findFeedForEntityId(item.id) - item.rssFeed = feedData?.toJSONMinified() || null + item.rssFeed = feedData?.toOldJSONMinified() || null } if (item.mediaType === 'book' && req.user.isAdminOrUp && includeEntities.includes('share')) { diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index e1c9c514..e0016421 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -25,8 +25,8 @@ class RSSFeedController { async getAll(req, res) { const feeds = await RssFeedManager.getFeeds() res.json({ - feeds: feeds.map((f) => f.toJSON()), - minified: feeds.map((f) => f.toJSONMinified()) + feeds: feeds.map((f) => f.toOldJSON()), + minified: feeds.map((f) => f.toOldJSONMinified()) }) } @@ -63,7 +63,7 @@ class RSSFeedController { } // Check that this slug is not being used for another feed (slug will also be the Feed id) - if (await RssFeedManager.findFeedBySlug(reqBody.slug)) { + if (await RssFeedManager.checkExistsBySlug(reqBody.slug)) { Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`) return res.status(400).send('Slug already in use') } @@ -97,7 +97,7 @@ class RSSFeedController { } // Check that this slug is not being used for another feed (slug will also be the Feed id) - if (await RssFeedManager.findFeedBySlug(reqBody.slug)) { + if (await RssFeedManager.checkExistsBySlug(reqBody.slug)) { Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`) return res.status(400).send('Slug already in use') } @@ -140,7 +140,7 @@ class RSSFeedController { } // Check that this slug is not being used for another feed (slug will also be the Feed id) - if (await RssFeedManager.findFeedBySlug(reqBody.slug)) { + if (await RssFeedManager.checkExistsBySlug(reqBody.slug)) { Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${reqBody.slug}" is already in use`) return res.status(400).send('Slug already in use') } diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index f8aef05c..21c93f33 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -55,7 +55,7 @@ class SeriesController { if (include.includes('rssfeed')) { const feedObj = await RssFeedManager.findFeedForEntityId(seriesJson.id) - seriesJson.rssFeed = feedObj?.toJSONMinified() || null + seriesJson.rssFeed = feedObj?.toOldJSONMinified() || null } res.json(seriesJson) diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 335df110..03d3c167 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -53,19 +53,29 @@ class RssFeedManager { /** * Find open feed for an entity (e.g. collection id, playlist id, library item id) * @param {string} entityId - * @returns {Promise} oldFeed + * @returns {Promise} */ findFeedForEntityId(entityId) { - return Database.feedModel.findOneOld({ entityId }) + return Database.feedModel.findOne({ + where: { + entityId + } + }) } /** - * Find open feed for a slug + * * @param {string} slug - * @returns {Promise} oldFeed + * @returns {Promise} */ - findFeedBySlug(slug) { - return Database.feedModel.findOneOld({ slug }) + checkExistsBySlug(slug) { + return Database.feedModel + .count({ + where: { + slug + } + }) + .then((count) => count > 0) } /** @@ -169,7 +179,17 @@ class RssFeedManager { * @param {Response} res */ async getFeedItem(req, res) { - const feed = await this.findFeedBySlug(req.params.slug) + const feed = await Database.feedModel.findOne({ + where: { + slug: req.params.slug + }, + attributes: ['id', 'slug'], + include: { + model: Database.feedEpisodeModel, + attributes: ['id', 'filePath'] + } + }) + if (!feed) { Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`) res.sendStatus(404) @@ -191,7 +211,12 @@ class RssFeedManager { * @param {Response} res */ async getFeedCover(req, res) { - const feed = await this.findFeedBySlug(req.params.slug) + const feed = await Database.feedModel.findOne({ + where: { + slug: req.params.slug + }, + attributes: ['coverPath'] + }) if (!feed) { Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`) res.sendStatus(404) @@ -338,10 +363,16 @@ class RssFeedManager { } } - async getFeeds() { - const feeds = await Database.models.feed.getOldFeeds() - Logger.info(`[RssFeedManager] Fetched all feeds`) - return feeds + /** + * + * @returns {Promise} + */ + getFeeds() { + return Database.feedModel.findAll({ + include: { + model: Database.feedEpisodeModel + } + }) } } module.exports = new RssFeedManager() diff --git a/server/models/Collection.js b/server/models/Collection.js index 558ae843..e01ad90a 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -112,7 +112,7 @@ class Collection extends Model { // Map feed if found if (c.feeds?.length) { - collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(c.feeds[0]) + collectionExpanded.rssFeed = c.feeds[0].toOldJSON() } return collectionExpanded @@ -348,7 +348,7 @@ class Collection extends Model { if (include?.includes('rssfeed')) { const feeds = await this.getFeeds() if (feeds?.length) { - collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0]) + collectionExpanded.rssFeed = feeds[0].toOldJSON() } } diff --git a/server/models/Feed.js b/server/models/Feed.js index 5aaa4856..d8f8553c 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -1,6 +1,5 @@ const Path = require('path') const { DataTypes, Model } = require('sequelize') -const oldFeed = require('../objects/Feed') const Logger = require('../Logger') const RSS = require('../libs/rss') @@ -74,60 +73,6 @@ class Feed extends Model { this.feedEpisodes } - static async getOldFeeds() { - const feeds = await this.findAll({ - include: { - model: this.sequelize.models.feedEpisode - } - }) - return feeds.map((f) => this.getOldFeed(f)) - } - - /** - * Get old feed from Feed and optionally Feed with FeedEpisodes - * @param {Feed} feedExpanded - * @returns {oldFeed} - */ - static getOldFeed(feedExpanded) { - const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) || [] - - // Sort episodes by pubDate. Newest to oldest for episodic, oldest to newest for serial - if (feedExpanded.podcastType === 'episodic') { - episodes.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)) - } else { - episodes.sort((a, b) => new Date(a.pubDate) - new Date(b.pubDate)) - } - - return new oldFeed({ - id: feedExpanded.id, - slug: feedExpanded.slug, - userId: feedExpanded.userId, - entityType: feedExpanded.entityType, - entityId: feedExpanded.entityId, - entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null, - coverPath: feedExpanded.coverPath || null, - meta: { - title: feedExpanded.title, - description: feedExpanded.description, - author: feedExpanded.author, - imageUrl: feedExpanded.imageURL, - feedUrl: feedExpanded.feedURL, - link: feedExpanded.siteURL, - explicit: feedExpanded.explicit, - type: feedExpanded.podcastType, - language: feedExpanded.language, - preventIndexing: feedExpanded.preventIndexing, - ownerName: feedExpanded.ownerName, - ownerEmail: feedExpanded.ownerEmail - }, - serverAddress: feedExpanded.serverAddress, - feedUrl: feedExpanded.feedURL, - episodes, - createdAt: feedExpanded.createdAt.valueOf(), - updatedAt: feedExpanded.updatedAt.valueOf() - }) - } - /** * @param {string} feedId * @returns {Promise} - true if feed was removed @@ -142,23 +87,6 @@ class Feed extends Model { ) } - /** - * Find feed where and return oldFeed - * @param {Object} where sequelize where object - * @returns {Promise} oldFeed - */ - static async findOneOld(where) { - if (!where) return null - const feedExpanded = await this.findOne({ - where, - include: { - model: this.sequelize.models.feedEpisode - } - }) - if (!feedExpanded) return null - return this.getOldFeed(feedExpanded) - } - /** * * @param {string} userId @@ -663,6 +591,17 @@ class Feed extends Model { return rssfeed.xml() } + /** + * + * @param {string} id + * @returns {string} + */ + getEpisodePath(id) { + const episode = this.feedEpisodes.find((ep) => ep.id === id) + if (!episode) return null + return episode.filePath + } + toOldJSON() { const episodes = this.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) return { diff --git a/server/models/FeedEpisode.js b/server/models/FeedEpisode.js index 31b26800..3bf0ff85 100644 --- a/server/models/FeedEpisode.js +++ b/server/models/FeedEpisode.js @@ -55,12 +55,14 @@ class FeedEpisode extends Model { * @param {import('./PodcastEpisode')} episode */ static getFeedEpisodeObjFromPodcastEpisode(libraryItemExpanded, feed, slug, episode) { + const episodeId = uuidv4() return { + id: episodeId, title: episode.title, author: feed.author, description: episode.description, siteURL: feed.siteURL, - enclosureURL: `/feed/${slug}/item/${episode.id}/media${Path.extname(episode.audioFile.metadata.filename)}`, + enclosureURL: `/feed/${slug}/item/${episodeId}/media${Path.extname(episode.audioFile.metadata.filename)}`, enclosureType: episode.audioFile.mimeType, enclosureSize: episode.audioFile.metadata.size, pubDate: episode.pubDate, diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 0c8df302..8ebed1d5 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -568,7 +568,7 @@ class LibraryItem extends Model { oldLibraryItem.media.metadata.series = li.series } if (li.rssFeed) { - oldLibraryItem.rssFeed = this.sequelize.models.feed.getOldFeed(li.rssFeed).toJSONMinified() + oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() } if (li.media.numEpisodes) { oldLibraryItem.media.numEpisodes = li.media.numEpisodes diff --git a/server/models/Playlist.js b/server/models/Playlist.js index fbc5f96a..490e8087 100644 --- a/server/models/Playlist.js +++ b/server/models/Playlist.js @@ -84,13 +84,6 @@ class Playlist extends Model { const playlistExpanded = oldPlaylist.toJSONExpanded(libraryItems) - if (include?.includes('rssfeed')) { - const feeds = await this.getFeeds() - if (feeds?.length) { - playlistExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0]) - } - } - return playlistExpanded } diff --git a/server/objects/Feed.js b/server/objects/Feed.js deleted file mode 100644 index 4da7a995..00000000 --- a/server/objects/Feed.js +++ /dev/null @@ -1,77 +0,0 @@ -const FeedMeta = require('./FeedMeta') -const FeedEpisode = require('./FeedEpisode') - -class Feed { - constructor(feed) { - this.id = null - this.slug = null - this.userId = null - this.entityType = null - this.entityId = null - this.entityUpdatedAt = null - - this.coverPath = null - this.serverAddress = null - this.feedUrl = null - - this.meta = null - this.episodes = null - - this.createdAt = null - this.updatedAt = null - - if (feed) { - this.construct(feed) - } - } - - construct(feed) { - this.id = feed.id - this.slug = feed.slug - this.userId = feed.userId - this.entityType = feed.entityType - this.entityId = feed.entityId - this.entityUpdatedAt = feed.entityUpdatedAt - this.coverPath = feed.coverPath - this.serverAddress = feed.serverAddress - this.feedUrl = feed.feedUrl - this.meta = new FeedMeta(feed.meta) - this.episodes = feed.episodes.map((ep) => new FeedEpisode(ep)) - this.createdAt = feed.createdAt - this.updatedAt = feed.updatedAt - } - - toJSON() { - return { - id: this.id, - slug: this.slug, - userId: this.userId, - entityType: this.entityType, - entityId: this.entityId, - coverPath: this.coverPath, - serverAddress: this.serverAddress, - feedUrl: this.feedUrl, - meta: this.meta.toJSON(), - episodes: this.episodes.map((ep) => ep.toJSON()), - createdAt: this.createdAt, - updatedAt: this.updatedAt - } - } - - toJSONMinified() { - return { - id: this.id, - entityType: this.entityType, - entityId: this.entityId, - feedUrl: this.feedUrl, - meta: this.meta.toJSONMinified() - } - } - - getEpisodePath(id) { - var episode = this.episodes.find((ep) => ep.id === id) - if (!episode) return null - return episode.fullPath - } -} -module.exports = Feed diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js deleted file mode 100644 index 4438eb52..00000000 --- a/server/objects/FeedEpisode.js +++ /dev/null @@ -1,67 +0,0 @@ -class FeedEpisode { - constructor(episode) { - this.id = null - - this.title = null - this.description = null - this.enclosure = null - this.pubDate = null - this.link = null - this.author = null - this.explicit = null - this.duration = null - this.season = null - this.episode = null - this.episodeType = null - - this.libraryItemId = null - this.episodeId = null - this.trackIndex = null - this.fullPath = null - - if (episode) { - this.construct(episode) - } - } - - construct(episode) { - this.id = episode.id - this.title = episode.title - this.description = episode.description - this.enclosure = episode.enclosure ? { ...episode.enclosure } : null - this.pubDate = episode.pubDate - this.link = episode.link - this.author = episode.author - this.explicit = episode.explicit - this.duration = episode.duration - this.season = episode.season - this.episode = episode.episode - this.episodeType = episode.episodeType - this.libraryItemId = episode.libraryItemId - this.episodeId = episode.episodeId || null - this.trackIndex = episode.trackIndex || 0 - this.fullPath = episode.fullPath - } - - toJSON() { - return { - id: this.id, - title: this.title, - description: this.description, - enclosure: this.enclosure ? { ...this.enclosure } : null, - pubDate: this.pubDate, - link: this.link, - author: this.author, - explicit: this.explicit, - duration: this.duration, - season: this.season, - episode: this.episode, - episodeType: this.episodeType, - libraryItemId: this.libraryItemId, - episodeId: this.episodeId, - trackIndex: this.trackIndex, - fullPath: this.fullPath - } - } -} -module.exports = FeedEpisode diff --git a/server/objects/FeedMeta.js b/server/objects/FeedMeta.js deleted file mode 100644 index 61fccaa3..00000000 --- a/server/objects/FeedMeta.js +++ /dev/null @@ -1,63 +0,0 @@ -class FeedMeta { - constructor(meta) { - this.title = null - this.description = null - this.author = null - this.imageUrl = null - this.feedUrl = null - this.link = null - this.explicit = null - this.type = null - this.language = null - this.preventIndexing = null - this.ownerName = null - this.ownerEmail = null - - if (meta) { - this.construct(meta) - } - } - - construct(meta) { - this.title = meta.title - this.description = meta.description - this.author = meta.author - this.imageUrl = meta.imageUrl - this.feedUrl = meta.feedUrl - this.link = meta.link - this.explicit = meta.explicit - this.type = meta.type - this.language = meta.language - this.preventIndexing = meta.preventIndexing - this.ownerName = meta.ownerName - this.ownerEmail = meta.ownerEmail - } - - toJSON() { - return { - title: this.title, - description: this.description, - author: this.author, - imageUrl: this.imageUrl, - feedUrl: this.feedUrl, - link: this.link, - explicit: this.explicit, - type: this.type, - language: this.language, - preventIndexing: this.preventIndexing, - ownerName: this.ownerName, - ownerEmail: this.ownerEmail - } - } - - toJSONMinified() { - return { - title: this.title, - description: this.description, - preventIndexing: this.preventIndexing, - ownerName: this.ownerName, - ownerEmail: this.ownerEmail - } - } -} -module.exports = FeedMeta diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index bdddde75..57ca48ba 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -54,7 +54,7 @@ module.exports = { items: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { - oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() + oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() } if (li.mediaItemShare) { oldLibraryItem.mediaItemShare = li.mediaItemShare @@ -91,7 +91,7 @@ module.exports = { libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { - oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() + oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() } if (li.size && !oldLibraryItem.media.size) { oldLibraryItem.media.size = li.size @@ -109,7 +109,7 @@ module.exports = { libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { - oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() + oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() } if (li.size && !oldLibraryItem.media.size) { oldLibraryItem.media.size = li.size @@ -138,7 +138,7 @@ module.exports = { libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { - oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() + oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() } if (li.series) { oldLibraryItem.media.metadata.series = li.series @@ -168,7 +168,7 @@ module.exports = { items: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { - oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() + oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() } if (li.mediaItemShare) { oldLibraryItem.mediaItemShare = li.mediaItemShare @@ -279,7 +279,7 @@ module.exports = { const oldSeries = s.toOldJSON() if (s.feeds?.length) { - oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified() + oldSeries.rssFeed = s.feeds[0].toOldJSONMinified() } // TODO: Sort books by sequence in query @@ -375,7 +375,7 @@ module.exports = { libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { - oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() + oldLibraryItem.rssFeed = li.rssFeed.toOldJSONMinified() } if (li.mediaItemShare) { oldLibraryItem.mediaItemShare = li.mediaItemShare diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index b2784f5d..ccce5304 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -615,8 +615,8 @@ module.exports = { } } - if (libraryItem.feeds?.length) { - libraryItem.rssFeed = libraryItem.feeds[0] + if (bookExpanded.libraryItem.feeds?.length) { + libraryItem.rssFeed = bookExpanded.libraryItem.feeds[0] } if (includeMediaItemShare) { @@ -766,8 +766,8 @@ module.exports = { name: s.name, sequence: s.bookSeries[bookIndex].sequence } - if (libraryItem.feeds?.length) { - libraryItem.rssFeed = libraryItem.feeds[0] + if (s.bookSeries[bookIndex].book.libraryItem.feeds?.length) { + libraryItem.rssFeed = s.bookSeries[bookIndex].book.libraryItem.feeds[0] } libraryItem.media = book return libraryItem @@ -900,8 +900,8 @@ module.exports = { delete book.libraryItem libraryItem.media = book - if (libraryItem.feeds?.length) { - libraryItem.rssFeed = libraryItem.feeds[0] + if (bookExpanded.libraryItem.feeds?.length) { + libraryItem.rssFeed = bookExpanded.libraryItem.feeds[0] } return libraryItem diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 2f259efc..c7c0914b 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -180,8 +180,8 @@ module.exports = { delete podcast.libraryItem - if (libraryItem.feeds?.length) { - libraryItem.rssFeed = libraryItem.feeds[0] + if (podcastExpanded.libraryItem.feeds?.length) { + libraryItem.rssFeed = podcastExpanded.libraryItem.feeds[0] } if (podcast.numEpisodesIncomplete) { libraryItem.numEpisodesIncomplete = podcast.numEpisodesIncomplete diff --git a/server/utils/queries/seriesFilters.js b/server/utils/queries/seriesFilters.js index c293f1df..2e0e2346 100644 --- a/server/utils/queries/seriesFilters.js +++ b/server/utils/queries/seriesFilters.js @@ -182,7 +182,7 @@ module.exports = { } if (s.feeds?.length) { - oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified() + oldSeries.rssFeed = s.feeds[0].toOldJSONMinified() } // TODO: Sort books by sequence in query