2022-05-02 21:41:59 +02:00
|
|
|
const Path = require('path')
|
2022-11-24 22:53:58 +01:00
|
|
|
|
|
|
|
const Logger = require('../Logger')
|
|
|
|
const SocketAuthority = require('../SocketAuthority')
|
2023-07-05 01:14:44 +02:00
|
|
|
const Database = require('../Database')
|
2022-11-24 22:53:58 +01:00
|
|
|
|
2022-07-06 02:53:01 +02:00
|
|
|
const fs = require('../libs/fsExtra')
|
2022-06-08 01:29:43 +02:00
|
|
|
const Feed = require('../objects/Feed')
|
2023-08-13 22:10:26 +02:00
|
|
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
2022-05-02 21:41:59 +02:00
|
|
|
|
|
|
|
class RssFeedManager {
|
2023-07-07 00:07:10 +02:00
|
|
|
constructor() { }
|
2022-08-06 02:23:18 +02:00
|
|
|
|
2023-07-22 23:18:55 +02:00
|
|
|
async validateFeedEntity(feedObj) {
|
2022-12-31 21:08:34 +01:00
|
|
|
if (feedObj.entityType === 'collection') {
|
2023-08-20 20:34:03 +02:00
|
|
|
const collection = await Database.collectionModel.getOldById(feedObj.entityId)
|
2023-07-22 23:18:55 +02:00
|
|
|
if (!collection) {
|
2022-12-31 21:08:34 +01:00
|
|
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
} else if (feedObj.entityType === 'libraryItem') {
|
2023-08-20 20:34:03 +02:00
|
|
|
const libraryItemExists = await Database.libraryItemModel.checkExistsById(feedObj.entityId)
|
2023-08-13 22:10:26 +02:00
|
|
|
if (!libraryItemExists) {
|
2022-12-31 21:08:34 +01:00
|
|
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Library item "${feedObj.entityId}" not found`)
|
|
|
|
return false
|
|
|
|
}
|
2022-12-31 23:58:19 +01:00
|
|
|
} else if (feedObj.entityType === 'series') {
|
2023-09-03 17:49:02 +02:00
|
|
|
const series = await Database.seriesModel.getOldById(feedObj.entityId)
|
2023-08-13 22:10:26 +02:00
|
|
|
if (!series) {
|
|
|
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`)
|
2022-12-31 23:58:19 +01:00
|
|
|
return false
|
|
|
|
}
|
2022-12-31 21:08:34 +01:00
|
|
|
} else {
|
|
|
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Invalid entityType "${feedObj.entityType}"`)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-07-17 23:48:46 +02:00
|
|
|
/**
|
|
|
|
* Validate all feeds and remove invalid
|
|
|
|
*/
|
2022-06-08 01:29:43 +02:00
|
|
|
async init() {
|
2023-08-20 20:34:03 +02:00
|
|
|
const feeds = await Database.feedModel.getOldFeeds()
|
2023-07-17 23:48:46 +02:00
|
|
|
for (const feed of feeds) {
|
2022-12-31 21:08:34 +01:00
|
|
|
// Remove invalid feeds
|
2023-07-22 23:18:55 +02:00
|
|
|
if (!await this.validateFeedEntity(feed)) {
|
2023-07-06 01:18:37 +02:00
|
|
|
await Database.removeFeed(feed.id)
|
2022-12-31 21:08:34 +01:00
|
|
|
}
|
2022-06-08 01:29:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-17 23:48:46 +02:00
|
|
|
/**
|
|
|
|
* Find open feed for an entity (e.g. collection id, playlist id, library item id)
|
2023-08-22 18:42:55 +02:00
|
|
|
* @param {string} entityId
|
2023-07-17 23:48:46 +02:00
|
|
|
* @returns {Promise<objects.Feed>} oldFeed
|
|
|
|
*/
|
2022-12-28 01:03:31 +01:00
|
|
|
findFeedForEntityId(entityId) {
|
2023-08-20 20:34:03 +02:00
|
|
|
return Database.feedModel.findOneOld({ entityId })
|
2022-05-02 23:42:30 +02:00
|
|
|
}
|
|
|
|
|
2023-07-17 23:48:46 +02:00
|
|
|
/**
|
|
|
|
* Find open feed for a slug
|
2023-08-22 18:42:55 +02:00
|
|
|
* @param {string} slug
|
2023-07-17 23:48:46 +02:00
|
|
|
* @returns {Promise<objects.Feed>} oldFeed
|
|
|
|
*/
|
2023-07-07 00:07:10 +02:00
|
|
|
findFeedBySlug(slug) {
|
2023-08-20 20:34:03 +02:00
|
|
|
return Database.feedModel.findOneOld({ slug })
|
2023-07-07 00:07:10 +02:00
|
|
|
}
|
|
|
|
|
2023-07-17 23:48:46 +02:00
|
|
|
/**
|
|
|
|
* Find open feed for a slug
|
2023-08-22 18:42:55 +02:00
|
|
|
* @param {string} slug
|
2023-07-17 23:48:46 +02:00
|
|
|
* @returns {Promise<objects.Feed>} oldFeed
|
|
|
|
*/
|
2023-07-07 00:07:10 +02:00
|
|
|
findFeed(id) {
|
2023-08-20 20:34:03 +02:00
|
|
|
return Database.feedModel.findByPkOld(id)
|
2022-12-26 23:58:36 +01:00
|
|
|
}
|
|
|
|
|
2022-08-28 22:41:51 +02:00
|
|
|
async getFeed(req, res) {
|
2023-07-17 23:48:46 +02:00
|
|
|
const feed = await this.findFeedBySlug(req.params.slug)
|
2022-06-08 01:29:43 +02:00
|
|
|
if (!feed) {
|
2023-07-07 00:07:10 +02:00
|
|
|
Logger.warn(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
2022-05-02 21:41:59 +02:00
|
|
|
res.sendStatus(404)
|
|
|
|
return
|
|
|
|
}
|
2022-06-08 02:25:14 +02:00
|
|
|
|
2022-12-31 23:58:19 +01:00
|
|
|
// Check if feed needs to be updated
|
2023-01-28 21:58:10 +01:00
|
|
|
if (feed.entityType === 'libraryItem') {
|
2023-09-04 23:33:55 +02:00
|
|
|
const libraryItem = await Database.libraryItemModel.getOldById(feed.entityId)
|
2023-01-28 21:58:10 +01:00
|
|
|
|
|
|
|
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)) {
|
2022-08-28 22:41:51 +02:00
|
|
|
Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
|
2023-07-21 23:59:00 +02:00
|
|
|
|
2022-08-28 22:41:51 +02:00
|
|
|
feed.updateFromItem(libraryItem)
|
2023-07-05 01:14:44 +02:00
|
|
|
await Database.updateFeed(feed)
|
2022-08-28 22:41:51 +02:00
|
|
|
}
|
2022-12-27 00:48:39 +01:00
|
|
|
} else if (feed.entityType === 'collection') {
|
2023-08-20 20:34:03 +02:00
|
|
|
const collection = await Database.collectionModel.findByPk(feed.entityId)
|
2022-12-27 00:48:39 +01:00
|
|
|
if (collection) {
|
2023-08-13 22:10:26 +02:00
|
|
|
const collectionExpanded = await collection.getOldJsonExpanded()
|
2022-12-29 01:08:03 +01:00
|
|
|
|
|
|
|
// Find most recently updated item in collection
|
|
|
|
let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate
|
|
|
|
collectionExpanded.books.forEach((libraryItem) => {
|
|
|
|
if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) {
|
|
|
|
mostRecentlyUpdatedAt = libraryItem.updatedAt
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt) {
|
2022-12-27 00:48:39 +01:00
|
|
|
Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`)
|
|
|
|
|
|
|
|
feed.updateFromCollection(collectionExpanded)
|
2023-07-05 01:14:44 +02:00
|
|
|
await Database.updateFeed(feed)
|
2022-12-27 00:48:39 +01:00
|
|
|
}
|
|
|
|
}
|
2022-12-31 23:58:19 +01:00
|
|
|
} else if (feed.entityType === 'series') {
|
2023-09-03 17:49:02 +02:00
|
|
|
const series = await Database.seriesModel.getOldById(feed.entityId)
|
2022-12-31 23:58:19 +01:00
|
|
|
if (series) {
|
|
|
|
const seriesJson = series.toJSON()
|
2023-08-13 22:10:26 +02:00
|
|
|
|
2022-12-31 23:58:19 +01:00
|
|
|
// Get books in series that have audio tracks
|
2023-08-13 22:10:26 +02:00
|
|
|
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks)
|
2022-12-31 23:58:19 +01:00
|
|
|
|
|
|
|
// 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)
|
2023-07-05 01:14:44 +02:00
|
|
|
await Database.updateFeed(feed)
|
2022-12-31 23:58:19 +01:00
|
|
|
}
|
|
|
|
}
|
2022-08-28 22:41:51 +02:00
|
|
|
}
|
|
|
|
|
2022-12-26 23:58:36 +01:00
|
|
|
const xml = feed.buildXml()
|
2022-05-02 21:41:59 +02:00
|
|
|
res.set('Content-Type', 'text/xml')
|
|
|
|
res.send(xml)
|
|
|
|
}
|
|
|
|
|
2023-07-17 23:48:46 +02:00
|
|
|
async getFeedItem(req, res) {
|
|
|
|
const feed = await this.findFeedBySlug(req.params.slug)
|
2022-06-08 01:29:43 +02:00
|
|
|
if (!feed) {
|
2023-07-07 00:07:10 +02:00
|
|
|
Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
2022-05-02 21:41:59 +02:00
|
|
|
res.sendStatus(404)
|
|
|
|
return
|
|
|
|
}
|
2022-12-26 23:58:36 +01:00
|
|
|
const episodePath = feed.getEpisodePath(req.params.episodeId)
|
2022-06-08 01:29:43 +02:00
|
|
|
if (!episodePath) {
|
|
|
|
Logger.error(`[RssFeedManager] Feed episode not found ${req.params.episodeId}`)
|
|
|
|
res.sendStatus(404)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
res.sendFile(episodePath)
|
2022-05-02 21:41:59 +02:00
|
|
|
}
|
|
|
|
|
2023-07-17 23:48:46 +02:00
|
|
|
async getFeedCover(req, res) {
|
|
|
|
const feed = await this.findFeedBySlug(req.params.slug)
|
2022-06-08 01:29:43 +02:00
|
|
|
if (!feed) {
|
2023-07-07 00:07:10 +02:00
|
|
|
Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`)
|
2022-05-02 23:42:30 +02:00
|
|
|
res.sendStatus(404)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-08 01:29:43 +02:00
|
|
|
if (!feed.coverPath) {
|
2022-05-02 23:42:30 +02:00
|
|
|
res.sendStatus(404)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-07-19 01:39:51 +02:00
|
|
|
const extname = Path.extname(feed.coverPath).toLowerCase().slice(1)
|
2022-05-02 23:42:30 +02:00
|
|
|
res.type(`image/${extname}`)
|
2022-12-26 23:58:36 +01:00
|
|
|
const readStream = fs.createReadStream(feed.coverPath)
|
2022-05-02 23:42:30 +02:00
|
|
|
readStream.pipe(res)
|
|
|
|
}
|
|
|
|
|
2022-06-08 01:29:43 +02:00
|
|
|
async openFeedForItem(user, libraryItem, options) {
|
2022-05-02 21:41:59 +02:00
|
|
|
const serverAddress = options.serverAddress
|
2022-05-04 01:52:34 +02:00
|
|
|
const slug = options.slug
|
2023-02-25 15:53:09 +01:00
|
|
|
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
|
|
|
|
const ownerName = options.metadataDetails?.ownerName
|
|
|
|
const ownerEmail = options.metadataDetails?.ownerEmail
|
2022-05-04 01:52:34 +02:00
|
|
|
|
2022-06-08 01:29:43 +02:00
|
|
|
const feed = new Feed()
|
2023-02-25 14:20:26 +01:00
|
|
|
feed.setFromItem(user.id, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail)
|
2022-06-08 01:29:43 +02:00
|
|
|
|
2023-07-07 00:07:10 +02:00
|
|
|
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
2023-07-05 01:14:44 +02:00
|
|
|
await Database.createFeed(feed)
|
2022-12-22 23:26:11 +01:00
|
|
|
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
2022-06-08 01:29:43 +02:00
|
|
|
return feed
|
2022-05-02 21:41:59 +02:00
|
|
|
}
|
2022-05-02 23:42:30 +02:00
|
|
|
|
2022-12-27 00:48:39 +01:00
|
|
|
async openFeedForCollection(user, collectionExpanded, options) {
|
|
|
|
const serverAddress = options.serverAddress
|
|
|
|
const slug = options.slug
|
2023-02-25 15:53:09 +01:00
|
|
|
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
|
|
|
|
const ownerName = options.metadataDetails?.ownerName
|
|
|
|
const ownerEmail = options.metadataDetails?.ownerEmail
|
2022-12-27 00:48:39 +01:00
|
|
|
|
|
|
|
const feed = new Feed()
|
2023-02-25 14:20:26 +01:00
|
|
|
feed.setFromCollection(user.id, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
|
2022-12-27 00:48:39 +01:00
|
|
|
|
2023-07-07 00:07:10 +02:00
|
|
|
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
2023-07-05 01:14:44 +02:00
|
|
|
await Database.createFeed(feed)
|
2022-12-27 00:48:39 +01:00
|
|
|
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
|
|
|
return feed
|
2022-05-02 23:42:30 +02:00
|
|
|
}
|
|
|
|
|
2022-12-31 23:58:19 +01:00
|
|
|
async openFeedForSeries(user, seriesExpanded, options) {
|
|
|
|
const serverAddress = options.serverAddress
|
|
|
|
const slug = options.slug
|
2023-02-25 15:53:09 +01:00
|
|
|
const preventIndexing = options.metadataDetails?.preventIndexing ?? true
|
|
|
|
const ownerName = options.metadataDetails?.ownerName
|
|
|
|
const ownerEmail = options.metadataDetails?.ownerEmail
|
2022-12-31 23:58:19 +01:00
|
|
|
|
|
|
|
const feed = new Feed()
|
2023-02-25 14:20:26 +01:00
|
|
|
feed.setFromSeries(user.id, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
|
2022-12-31 23:58:19 +01:00
|
|
|
|
2023-07-07 00:07:10 +02:00
|
|
|
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
|
2023-07-05 01:14:44 +02:00
|
|
|
await Database.createFeed(feed)
|
2022-12-31 23:58:19 +01:00
|
|
|
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
|
|
|
|
return feed
|
|
|
|
}
|
|
|
|
|
2022-12-31 21:08:34 +01:00
|
|
|
async handleCloseFeed(feed) {
|
|
|
|
if (!feed) return
|
2023-07-05 01:14:44 +02:00
|
|
|
await Database.removeFeed(feed.id)
|
2022-12-22 23:26:11 +01:00
|
|
|
SocketAuthority.emitter('rss_feed_closed', feed.toJSONMinified())
|
2022-06-08 01:29:43 +02:00
|
|
|
Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
|
2022-05-02 23:42:30 +02:00
|
|
|
}
|
2022-12-31 21:08:34 +01:00
|
|
|
|
2023-07-07 00:07:10 +02:00
|
|
|
async closeRssFeed(req, res) {
|
2023-07-17 23:48:46 +02:00
|
|
|
const feed = await this.findFeed(req.params.id)
|
2023-07-07 00:07:10 +02:00
|
|
|
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)
|
2022-12-31 21:08:34 +01:00
|
|
|
}
|
|
|
|
|
2023-07-17 23:48:46 +02:00
|
|
|
async closeFeedForEntityId(entityId) {
|
|
|
|
const feed = await this.findFeedForEntityId(entityId)
|
2022-12-31 21:08:34 +01:00
|
|
|
if (!feed) return
|
|
|
|
return this.handleCloseFeed(feed)
|
|
|
|
}
|
2023-08-22 18:42:55 +02:00
|
|
|
|
|
|
|
async getFeeds() {
|
|
|
|
const feeds = await Database.models.feed.getOldFeeds()
|
|
|
|
Logger.info(`[RssFeedManager] Fetched all feeds`)
|
|
|
|
return feeds
|
|
|
|
}
|
2022-05-02 21:41:59 +02:00
|
|
|
}
|
2023-02-25 14:20:26 +01:00
|
|
|
module.exports = RssFeedManager
|