Add infra to handle with feed healthy

This commit is contained in:
mfcar 2023-09-23 20:38:03 +01:00
parent 102c90c4e8
commit 2b3c3cd088
No known key found for this signature in database
6 changed files with 412 additions and 365 deletions

View file

@ -485,16 +485,16 @@ export default {
return this.$toast.error('Podcast does not have an RSS Feed') return this.$toast.error('Podcast does not have an RSS Feed')
} }
this.fetchingRSSFeed = true this.fetchingRSSFeed = true
var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => { var payload = await this.$axios.get(`/api/podcasts/${this.libraryItemId}/feed`).catch((error) => {
console.error('Failed to get feed', error) console.error('Failed to get feed', error)
this.$toast.error('Failed to get podcast feed') this.$toast.error('Failed to get podcast feed')
return null return null
}) })
this.fetchingRSSFeed = false this.fetchingRSSFeed = false
if (!payload) return if (!payload || !payload.data) return
console.log('Podcast feed', payload) console.log('Podcast feed', payload.data)
const podcastfeed = payload.podcast const podcastfeed = payload.data.podcast
if (!podcastfeed.episodes || !podcastfeed.episodes.length) { if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
this.$toast.info('No episodes found in RSS feed') this.$toast.info('No episodes found in RSS feed')
return return

View file

@ -115,6 +115,19 @@ class PodcastController {
res.json({ podcast }) res.json({ podcast })
} }
async checkPodcastFeed(req, res) {
const libraryItem = req.libraryItem
const podcast = await getPodcastFeed(libraryItem.media.metadata.feedUrl)
if (!podcast) {
this.podcastManager.setFeedHealthStatus(libraryItem, false)
return res.status(404).send('Podcast RSS feed request failed or invalid response data')
}
this.podcastManager.setFeedHealthStatus(libraryItem, true)
res.json({ podcast })
}
async getFeedsFromOPMLText(req, res) { async getFeedsFromOPMLText(req, res) {
if (!req.body.opmlText) { if (!req.body.opmlText) {
return res.sendStatus(400) return res.sendStatus(400)

View file

@ -245,7 +245,7 @@ class PodcastManager {
const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`) Logger.debug(`[PodcastManager] runEpisodeCheck: "${libraryItem.media.metadata.title}" checking for episodes after ${new Date(dateToCheckForEpisodesAfter)}`)
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload) let newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload)
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes ? newEpisodes.length : 'N/A'} episodes found`) Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes ? newEpisodes.length : 'N/A'} episodes found`)
if (!newEpisodes) { // Failed if (!newEpisodes) { // Failed
@ -259,13 +259,18 @@ class PodcastManager {
} else { } else {
Logger.warn(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}"`) Logger.warn(`[PodcastManager] runEpisodeCheck ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}"`)
} }
libraryItem.media.metadata.feedHealthy = false
} else if (newEpisodes.length) { } else if (newEpisodes.length) {
delete this.failedCheckMap[libraryItem.id] delete this.failedCheckMap[libraryItem.id]
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`) Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
this.downloadPodcastEpisodes(libraryItem, newEpisodes, true) this.downloadPodcastEpisodes(libraryItem, newEpisodes, true)
libraryItem.media.metadata.lastSuccessfulFetchAt = Date.now()
libraryItem.media.metadata.feedHealthy = true
} else { } else {
delete this.failedCheckMap[libraryItem.id] delete this.failedCheckMap[libraryItem.id]
Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`) Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`)
libraryItem.media.metadata.lastSuccessfulFetchAt = Date.now()
libraryItem.media.metadata.feedHealthy = true
} }
libraryItem.media.lastEpisodeCheck = Date.now() libraryItem.media.lastEpisodeCheck = Date.now()
@ -282,7 +287,7 @@ class PodcastManager {
} }
var feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl) var feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl)
if (!feed || !feed.episodes) { if (!feed || !feed.episodes) {
Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id})`, feed) Logger.error(`[PodcastManager] checkPodcastForNewEpisodes invalid feed payload for ${podcastLibraryItem.media.metadata.title} (ID: ${podcastLibraryItem.id}, URL: ${podcastLibraryItem.media.metadata.feedUrl})`, feed)
return false return false
} }
@ -299,12 +304,18 @@ class PodcastManager {
async checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload) { async checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload) {
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0) const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`) Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck, maxEpisodesToDownload) let newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck, maxEpisodesToDownload)
if (newEpisodes.length) { if (!newEpisodes) {
libraryItem.media.metadata.feedHealthy = false
} else if (newEpisodes.length) {
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`) Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
this.downloadPodcastEpisodes(libraryItem, newEpisodes, false) this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
libraryItem.media.metadata.lastSuccessfulFetchAt = Date.now()
libraryItem.media.metadata.feedHealthy = true
} else { } else {
Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`) Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`)
libraryItem.media.metadata.lastSuccessfulFetchAt = Date.now()
libraryItem.media.metadata.feedHealthy = true
} }
libraryItem.media.lastEpisodeCheck = Date.now() libraryItem.media.lastEpisodeCheck = Date.now()
@ -315,6 +326,15 @@ class PodcastManager {
return newEpisodes return newEpisodes
} }
setFeedHealthStatus(libraryItem, isHealthy) {
libraryItem.media.metadata.feedHealthy = isHealthy
if (isHealthy) {
libraryItem.media.lastSuccessfulFetchAt = Date.now()
}
libraryItem.updatedAt = Date.now()
Database.updateLibraryItem(libraryItem)
}
async findEpisode(rssFeedUrl, searchTitle) { async findEpisode(rssFeedUrl, searchTitle) {
const feed = await getPodcastFeed(rssFeedUrl).catch(() => { const feed = await getPodcastFeed(rssFeedUrl).catch(() => {
return null return null
@ -389,4 +409,5 @@ class PodcastManager {
} }
} }
} }
module.exports = PodcastManager module.exports = PodcastManager

View file

@ -50,6 +50,10 @@ class Podcast extends Model {
this.createdAt this.createdAt
/** @type {Date} */ /** @type {Date} */
this.updatedAt this.updatedAt
/** @type {Date} */
this.lastSuccessfulFetchAt
/** @type {boolean} */
this.feedHealthy
} }
static getOldPodcast(libraryItemExpanded) { static getOldPodcast(libraryItemExpanded) {
@ -71,7 +75,9 @@ class Podcast extends Model {
itunesArtistId: podcastExpanded.itunesArtistId, itunesArtistId: podcastExpanded.itunesArtistId,
explicit: podcastExpanded.explicit, explicit: podcastExpanded.explicit,
language: podcastExpanded.language, language: podcastExpanded.language,
type: podcastExpanded.podcastType type: podcastExpanded.podcastType,
lastSuccessfulFetchAt: podcastExpanded.lastSuccessfulFetchAt?.valueOf() || null,
feedHealthy: !!podcastExpanded.feedHealthy
}, },
coverPath: podcastExpanded.coverPath, coverPath: podcastExpanded.coverPath,
tags: podcastExpanded.tags, tags: podcastExpanded.tags,
@ -108,7 +114,9 @@ class Podcast extends Model {
maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload, maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload,
coverPath: oldPodcast.coverPath, coverPath: oldPodcast.coverPath,
tags: oldPodcast.tags, tags: oldPodcast.tags,
genres: oldPodcastMetadata.genres genres: oldPodcastMetadata.genres,
lastSuccessfulFetchAt: oldPodcastMetadata.lastSuccessfulFetchAt,
feedHealthy: !!oldPodcastMetadata.feedHealthy
} }
} }
@ -144,7 +152,9 @@ class Podcast extends Model {
maxNewEpisodesToDownload: DataTypes.INTEGER, maxNewEpisodesToDownload: DataTypes.INTEGER,
coverPath: DataTypes.STRING, coverPath: DataTypes.STRING,
tags: DataTypes.JSON, tags: DataTypes.JSON,
genres: DataTypes.JSON genres: DataTypes.JSON,
lastSuccessfulFetchAt: DataTypes.DATE,
feedHealthy: DataTypes.BOOLEAN
}, { }, {
sequelize, sequelize,
modelName: 'podcast' modelName: 'podcast'

View file

@ -226,6 +226,7 @@ class ApiRouter {
// //
this.router.post('/podcasts', PodcastController.create.bind(this)) this.router.post('/podcasts', PodcastController.create.bind(this))
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this)) this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
this.router.get('/podcasts/:id/feed', PodcastController.middleware.bind(this), PodcastController.checkPodcastFeed.bind(this))
this.router.post('/podcasts/opml', PodcastController.getFeedsFromOPMLText.bind(this)) this.router.post('/podcasts/opml', PodcastController.getFeedsFromOPMLText.bind(this))
this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this)) this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this))
this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this)) this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))

View file

@ -198,7 +198,9 @@ function migratePodcast(oldLibraryItem, LibraryItem) {
updatedAt: LibraryItem.updatedAt, updatedAt: LibraryItem.updatedAt,
coverPath: oldPodcast.coverPath, coverPath: oldPodcast.coverPath,
tags: oldPodcast.tags, tags: oldPodcast.tags,
genres: oldPodcastMetadata.genres genres: oldPodcastMetadata.genres,
lastSuccessfulFetchAt: oldPodcastMetadata.lastSuccessfulFetchAt || null,
feedHealthy: !!oldPodcastMetadata.feedHealthy || null
} }
_newRecords.podcast = Podcast _newRecords.podcast = Podcast
oldDbIdMap.podcasts[oldLibraryItem.id] = Podcast.id oldDbIdMap.podcasts[oldLibraryItem.id] = Podcast.id