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 })