mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-10 11:21:36 +00:00
Add infra to handle with feed healthy
This commit is contained in:
parent
102c90c4e8
commit
2b3c3cd088
6 changed files with 412 additions and 365 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ const Database = require('../Database')
|
||||||
|
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
const { getPodcastFeed } = require('../utils/podcastUtils')
|
const {getPodcastFeed} = require('../utils/podcastUtils')
|
||||||
const { removeFile, downloadFile } = require('../utils/fileUtils')
|
const {removeFile, downloadFile} = require('../utils/fileUtils')
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const {levenshteinDistance} = require('../utils/index')
|
||||||
const opmlParser = require('../utils/parsers/parseOPML')
|
const opmlParser = require('../utils/parsers/parseOPML')
|
||||||
const opmlGenerator = require('../utils/generators/opmlGenerator')
|
const opmlGenerator = require('../utils/generators/opmlGenerator')
|
||||||
const prober = require('../utils/prober')
|
const prober = require('../utils/prober')
|
||||||
|
|
@ -158,7 +158,7 @@ class PodcastManager {
|
||||||
podcastEpisode.audioFile = audioFile
|
podcastEpisode.audioFile = audioFile
|
||||||
|
|
||||||
if (audioFile.chapters?.length) {
|
if (audioFile.chapters?.length) {
|
||||||
podcastEpisode.chapters = audioFile.chapters.map(ch => ({ ...ch }))
|
podcastEpisode.chapters = audioFile.chapters.map(ch => ({...ch}))
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
libraryItem.media.addPodcastEpisode(podcastEpisode)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue