audiobookshelf/server/managers/RssFeedManager.js

323 lines
10 KiB
JavaScript
Raw Normal View History

2024-12-08 15:05:33 +01:00
const { Request, Response } = require('express')
const Path = require('path')
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
2023-07-05 01:14:44 +02:00
const Database = require('../Database')
2022-07-06 02:53:01 +02:00
const fs = require('../libs/fsExtra')
const Feed = require('../objects/Feed')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
class RssFeedManager {
constructor() {}
async validateFeedEntity(feedObj) {
if (feedObj.entityType === 'collection') {
2023-08-20 20:34:03 +02:00
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') {
2023-08-20 20:34:03 +02:00
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
}
2023-07-17 23:48:46 +02:00
/**
* Validate all feeds and remove invalid
*/
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) {
// Remove invalid feeds
if (!(await this.validateFeedEntity(feed))) {
2023-07-06 01:18:37 +02:00
await Database.removeFeed(feed.id)
}
}
}
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
*/
findFeedForEntityId(entityId) {
2023-08-20 20:34:03 +02:00
return Database.feedModel.findOneOld({ entityId })
}
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
*/
findFeedBySlug(slug) {
2023-08-20 20:34:03 +02:00
return Database.feedModel.findOneOld({ slug })
}
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
*/
findFeed(id) {
2023-08-20 20:34:03 +02:00
return Database.feedModel.findByPkOld(id)
2022-12-26 23:58:36 +01:00
}
2024-12-08 15:05:33 +01:00
/**
* GET: /feed/:slug
*
* @param {Request} req
* @param {Response} res
*/
async getFeed(req, res) {
2023-07-17 23:48:46 +02:00
const feed = await this.findFeedBySlug(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}"`)
2023-07-21 23:59:00 +02:00
feed.updateFromItem(libraryItem)
2023-07-05 01:14:44 +02:00
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)
2023-07-05 01:14:44 +02:00
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)
2023-07-05 01:14:44 +02:00
await Database.updateFeed(feed)
}
}
}
const xml = feed.buildXml(req.originalHostPrefix)
res.set('Content-Type', 'text/xml')
res.send(xml)
}
2024-12-08 15:05:33 +01:00
/**
* GET: /feed/:slug/item/:episodeId/*
*
* @param {Request} req
* @param {Response} res
*/
2023-07-17 23:48:46 +02:00
async getFeedItem(req, res) {
const feed = await this.findFeedBySlug(req.params.slug)
if (!feed) {
Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`)
res.sendStatus(404)
return
}
2022-12-26 23:58:36 +01:00
const episodePath = feed.getEpisodePath(req.params.episodeId)
if (!episodePath) {
Logger.error(`[RssFeedManager] Feed episode not found ${req.params.episodeId}`)
res.sendStatus(404)
return
}
res.sendFile(episodePath)
}
2024-12-08 15:05:33 +01:00
/**
* GET: /feed/:slug/cover*
*
* @param {Request} req
* @param {Response} res
*/
2023-07-17 23:48:46 +02:00
async getFeedCover(req, res) {
const feed = await this.findFeedBySlug(req.params.slug)
if (!feed) {
Logger.debug(`[RssFeedManager] Feed not found ${req.params.slug}`)
res.sendStatus(404)
return
}
if (!feed.coverPath) {
res.sendStatus(404)
return
}
const extname = Path.extname(feed.coverPath).toLowerCase().slice(1)
res.type(`image/${extname}`)
2022-12-26 23:58:36 +01:00
const readStream = fs.createReadStream(feed.coverPath)
readStream.pipe(res)
}
/**
*
* @param {string} userId
* @param {*} libraryItem
* @param {*} options
* @returns
*/
async openFeedForItem(userId, libraryItem, 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
const feed = new Feed()
feed.setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail)
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())
return feed
}
/**
*
* @param {string} userId
* @param {*} collectionExpanded
* @param {*} options
* @returns
*/
async openFeedForCollection(userId, 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
const feed = new Feed()
feed.setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
2023-07-05 01:14:44 +02:00
await Database.createFeed(feed)
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
return feed
}
/**
*
* @param {string} userId
* @param {*} seriesExpanded
* @param {*} options
* @returns
*/
async openFeedForSeries(userId, 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
const feed = new Feed()
feed.setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail)
Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`)
2023-07-05 01:14:44 +02:00
await Database.createFeed(feed)
SocketAuthority.emitter('rss_feed_open', feed.toJSONMinified())
return feed
}
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())
Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
}
async closeRssFeed(req, res) {
2023-07-17 23:48:46 +02:00
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)
}
2023-07-17 23:48:46 +02:00
async closeFeedForEntityId(entityId) {
const feed = await this.findFeedForEntityId(entityId)
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
}
}
2023-02-25 14:20:26 +01:00
module.exports = RssFeedManager