diff --git a/client/components/widgets/FeedHealthyIndicator.vue b/client/components/widgets/FeedHealthyIndicator.vue
new file mode 100644
index 000000000..f7e34211e
--- /dev/null
+++ b/client/components/widgets/FeedHealthyIndicator.vue
@@ -0,0 +1,28 @@
+
+
+
+ cloud_done
+
+
+
+
+ cloud_off
+
+
+
+
+
diff --git a/client/components/widgets/PodcastDetailsEdit.vue b/client/components/widgets/PodcastDetailsEdit.vue
index 389ca8945..b2e7f7221 100644
--- a/client/components/widgets/PodcastDetailsEdit.vue
+++ b/client/components/widgets/PodcastDetailsEdit.vue
@@ -10,7 +10,15 @@
-
+
+ {{ $strings.LabelFeedLastSuccessfulCheck }}: {{$dateDistanceFromNow(details.lastSuccessfulFetchAt)}}
@@ -71,7 +79,9 @@ export default {
itunesArtistId: null,
explicit: false,
language: null,
- type: null
+ type: null,
+ feedHealthy: false,
+ lastSuccessfulFetchAt: null
},
newTags: []
}
@@ -242,6 +252,8 @@ export default {
this.details.language = this.mediaMetadata.language || ''
this.details.explicit = !!this.mediaMetadata.explicit
this.details.type = this.mediaMetadata.type || 'episodic'
+ this.details.feedHealthy = !!this.mediaMetadata.feedHealthy
+ this.details.lastSuccessfulFetchAt = this.mediaMetadata.lastSuccessfulFetchAt || null
this.newTags = [...(this.media.tags || [])]
},
diff --git a/client/pages/config/rss-feeds.vue b/client/pages/config/rss-feeds.vue
index 68117a859..5b4859a76 100644
--- a/client/pages/config/rss-feeds.vue
+++ b/client/pages/config/rss-feeds.vue
@@ -9,61 +9,187 @@
-
-
+
+
+
+
+
+
+
-
+
@@ -71,9 +197,18 @@
export default {
data() {
return {
+ showOpenedFeedsView: true,
showFeedModal: false,
selectedFeed: null,
- feeds: []
+ feeds: [],
+ feedSubscriptions: [],
+ feedsSearch: null,
+ feedsSearchTimeout: null,
+ feedsSearchText: null,
+ feedSubscriptionsSearch: null,
+ feedSubscriptionsSearchTimeout: null,
+ feedSubscriptionsSearchText: null,
+ feedSubscriptionsShowOnlyUnhealthy: false,
}
},
computed: {
@@ -82,13 +217,63 @@ export default {
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
- }
+ },
+ noCoverUrl() {
+ return `${this.$config.routerBasePath}/Logo.png`
+ },
+ bookCoverAspectRatio() {
+ return this.$store.getters['libraries/getBookCoverAspectRatio']
+ },
+ feedsList() {
+ return this.feeds.filter((feed) => {
+ if (!this.feedsSearchText) return true
+ return feed?.meta?.title?.toLowerCase().includes(this.feedsSearchText) || feed?.slug?.toLowerCase().includes(this.feedsSearchText)
+ })
+ },
+ feedSubscriptionsSorted() {
+ return this.feedSubscriptionsShowOnlyUnhealthy
+ ? this.feedSubscriptions.filter(feedSubscription => !feedSubscription.metadata.feedHealthy)
+ : this.feedSubscriptions;
+ },
+ feedSubscriptionsList() {
+ return this.feedSubscriptionsSorted.filter((feedSubscription) => {
+ if (!this.feedSubscriptionsSearchText) return true
+ if (this.feedSubscriptionsShowOnlyUnhealthy && feedSubscription?.metadata?.feedHealthy) return false
+ return feedSubscription?.metadata?.title?.toLowerCase().includes(this.feedSubscriptionsSearchText) ||
+ feedSubscription?.metadata?.feedUrl?.toLowerCase().includes(this.feedSubscriptionsSearchText)
+ })
+ },
},
methods: {
+ feedsSubmit() {},
+ feedsInputUpdate() {
+ clearTimeout(this.feedsSearchTimeout)
+ this.feedsSearchTimeout = setTimeout(() => {
+ if (!this.feedsSearch || !this.feedsSearch.trim()) {
+ this.feedsSearchText = ''
+ return
+ }
+ this.feedsSearchText = this.feedsSearch.toLowerCase().trim()
+ }, 500)
+ },
+ feedSubscriptionsSubmit() {},
+ feedSubscriptionsInputUpdate() {
+ clearTimeout(this.feedSubscriptionsSearchTimeout)
+ this.feedSubscriptionsSearchTimeout = setTimeout(() => {
+ if (!this.feedSubscriptionsSearch || !this.feedSubscriptionsSearch.trim()) {
+ this.feedSubscriptionsSearchText = ''
+ return
+ }
+ this.feedSubscriptionsSearchText = this.feedSubscriptionsSearch.toLowerCase().trim()
+ }, 500)
+ },
showFeed(feed) {
this.selectedFeed = feed
this.showFeedModal = true
},
+ copyToClipboard(str) {
+ this.$copyToClipboard(str, this)
+ },
deleteFeedClick(feed) {
const payload = {
message: this.$strings.MessageConfirmCloseFeed,
@@ -104,19 +289,19 @@ export default {
deleteFeed(feed) {
this.processing = true
this.$axios
- .$post(`/api/feeds/${feed.id}/close`)
- .then(() => {
- this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
- this.show = false
- this.loadFeeds()
- })
- .catch((error) => {
- console.error('Failed to close RSS feed', error)
- this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
- })
- .finally(() => {
- this.processing = false
- })
+ .$post(`/api/feeds/${feed.id}/close`)
+ .then(() => {
+ this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
+ this.show = false
+ this.loadFeeds()
+ })
+ .catch((error) => {
+ console.error('Failed to close RSS feed', error)
+ this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
+ })
+ .finally(() => {
+ this.processing = false
+ })
},
getEntityType(entityType) {
if (entityType === 'libraryItem') return this.$strings.LabelItem
@@ -125,9 +310,40 @@ export default {
return this.$strings.LabelUnknown
},
coverUrl(feed) {
- if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
+ if (!feed.coverPath) return this.noCoverUrl
return `${feed.feedUrl}/cover`
},
+ nextRun(cronExpression) {
+ if (!cronExpression) return ''
+ const parsed = this.$getNextScheduledDate(cronExpression)
+ return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || ''
+ },
+ async forceRecheckFeed(podcast) {
+ podcast.isLoading = true
+ let podcastResult;
+
+ try {
+ podcastResult = await this.$axios.$get(`/api/podcasts/${podcast.id}/check-feed-url`)
+
+ if (!podcastResult?.feedHealthy) {
+ this.$toast.error('Podcast feed url is not healthy')
+ } else {
+ this.$toast.success('Podcast feed url is healthy')
+ }
+
+ podcast.lastEpisodeCheck = Date.parse(podcastResult.lastEpisodeCheck)
+ if (podcastResult.lastSuccessfulFetchAt) {
+ podcast.metadata.lastSuccessfulFetchAt = Date.parse(podcastResult.lastSuccessfulFetchAt)
+ }
+ podcast.metadata.feedHealthy = podcastResult.feedHealthy
+ } catch (error) {
+ console.error('Podcast feed url is not healthy', error)
+ this.$toast.error('Podcast feed url is not healthy')
+ podcastResult = null
+ } finally {
+ podcast.isLoading = false
+ }
+ },
async loadFeeds() {
const data = await this.$axios.$get(`/api/feeds`).catch((err) => {
console.error('Failed to load RSS feeds', err)
@@ -139,8 +355,21 @@ export default {
}
this.feeds = data.feeds
},
+ async loadPodcastsWithExternalFeedSubscriptions() {
+ const data = await this.$axios.$get(`/api/podcasts/external-podcast-feeds-status`).catch((err) => {
+ console.error('Failed to load podcasts with external feed subscriptions', err)
+ return null
+ })
+ if (!data) {
+ this.$toast.error('Failed to load podcasts with external feed subscriptions')
+ return
+ }
+
+ this.feedSubscriptions = data.podcasts.map(podcast => ({...podcast, isLoading: false}));
+ },
init() {
this.loadFeeds()
+ this.loadPodcastsWithExternalFeedSubscriptions()
}
},
mounted() {
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue
index 1baf521c7..60ff91b22 100644
--- a/client/pages/item/_id/index.vue
+++ b/client/pages/item/_id/index.vue
@@ -474,16 +474,16 @@ export default {
return this.$toast.error(this.$strings.ToastNoRSSFeed)
}
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)
this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
return null
})
this.fetchingRSSFeed = false
- if (!payload) return
+ if (!payload || !payload.data) return
- console.log('Podcast feed', payload)
- const podcastfeed = payload.podcast
+ console.log('Podcast feed', payload.data)
+ const podcastfeed = payload.data.podcast
if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
this.$toast.info(this.$strings.ToastPodcastNoEpisodesInFeed)
return
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index 75069cd33..51450ecaf 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -22,6 +22,7 @@
"ButtonCloseSession": "Close Open Session",
"ButtonCollections": "Collections",
"ButtonConfigureScanner": "Configure Scanner",
+ "ButtonCopyFeedURL": "Copy Feed URL",
"ButtonCreate": "Create",
"ButtonCreateBackup": "Create Backup",
"ButtonDelete": "Delete",
@@ -32,6 +33,7 @@
"ButtonEnable": "Enable",
"ButtonFireAndFail": "Fire and Fail",
"ButtonFireOnTest": "Fire onTest event",
+ "ButtonForceReCheckFeed": "Force Re-Check Feed",
"ButtonForceReScan": "Force Re-Scan",
"ButtonFullPath": "Full Path",
"ButtonHide": "Hide",
@@ -137,8 +139,10 @@
"HeaderEpisodes": "Episodes",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
+ "HeaderExternalFeedURLHealthChecker": "External RSS Feed Health Check",
"HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters",
+ "HeaderHostedRSSFeeds": "ABS Hosted RSS Feed",
"HeaderIgnoredFiles": "Ignored Files",
"HeaderItemFiles": "Item Files",
"HeaderItemMetadataUtils": "Item Metadata Utils",
@@ -292,6 +296,7 @@
"LabelDeviceInfo": "Device Info",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDirectory": "Directory",
+ "LabelDisabled": "Disabled",
"LabelDiscFromFilename": "Disc from Filename",
"LabelDiscFromMetadata": "Disc from Metadata",
"LabelDiscover": "Discover",
@@ -314,6 +319,7 @@
"LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable",
+ "LabelEnabled": "Enabled",
"LabelEncodingBackupLocation": "A backup of your original audio files will be stored in:",
"LabelEncodingChaptersNotEmbedded": "Chapters are not embedded in multi-track audiobooks.",
"LabelEncodingClearItemCache": "Make sure to periodically purge items cache.",
@@ -340,7 +346,14 @@
"LabelExplicitChecked": "Explicit (checked)",
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelExportOPML": "Export OPML",
+ "LabelFeedHealthy": "Feed Healthy",
+ "LabelFeedLastChecked": "Last Checked",
+ "LabelFeedLastSuccessfulCheck": "Last Successful Check",
+ "LabelFeedNextAutomaticCheck": "Next Automatic Check",
+ "LabelFeedNotWorking": "Feed is returning errors, check the logs for more information",
+ "LabelFeedShowOnlyUnhealthy": "Show only unhealthy",
"LabelFeedURL": "Feed URL",
+ "LabelFeedWorking": "Feed working as expected",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File",
"LabelFileBirthtime": "File Birthtime",
@@ -779,6 +792,7 @@
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
"MessageNoAudioTracks": "No audio tracks",
"MessageNoAuthors": "No Authors",
+ "MessageNoAvailable": "N/A",
"MessageNoBackups": "No Backups",
"MessageNoBookmarks": "No Bookmarks",
"MessageNoChapters": "No Chapters",
@@ -898,6 +912,7 @@
"PlaceholderNewPlaylist": "New playlist name",
"PlaceholderSearch": "Search..",
"PlaceholderSearchEpisode": "Search episode..",
+ "PlaceholderSearchTitle": "Search title..",
"StatsAuthorsAdded": "authors added",
"StatsBooksAdded": "books added",
"StatsBooksAdditional": "Some additions include…",
@@ -916,6 +931,7 @@
"StatsTopNarrators": "TOP NARRATORS",
"StatsTotalDuration": "With a total duration of…",
"StatsYearInReview": "YEAR IN REVIEW",
+ "ToastAccountUpdateFailed": "Failed to update account",
"ToastAccountUpdateSuccess": "Account updated",
"ToastAppriseUrlRequired": "Must enter an Apprise URL",
"ToastAsinRequired": "ASIN is required",
diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js
index 3610c2ea7..664ca1e77 100644
--- a/server/controllers/PodcastController.js
+++ b/server/controllers/PodcastController.js
@@ -150,6 +150,44 @@ class PodcastController {
res.json({ podcast })
}
+ async getPodcastsWithExternalFeedsSubscriptions(req, res) {
+ const podcasts = await Database.podcastModel.getAllIWithFeedSubscriptions()
+ res.json({
+ podcasts
+ })
+ }
+
+ async checkPodcastFeed(req, res) {
+ const libraryItem = req.libraryItem
+ const podcast = await getPodcastFeed(libraryItem.media.metadata.feedUrl)
+
+ if (!podcast) {
+ this.podcastManager.setFeedHealthStatus(libraryItem.media.id, false)
+ return res.status(404).send('Podcast RSS feed request failed or invalid response data')
+ }
+
+ this.podcastManager.setFeedHealthStatus(libraryItem.media.id, true)
+ res.json({ podcast })
+ }
+
+ async checkPodcastFeedUrl(req, res) {
+ const podcastId = req.params.id;
+
+ try {
+ const podcast = await Database.podcastModel.findByPk(req.params.id)
+
+ const podcastResult = await getPodcastFeed(podcast.feedURL);
+ const podcastNewStatus = await this.podcastManager.setFeedHealthStatus(podcastId, !!podcastResult);
+
+ Logger.info(podcastNewStatus);
+
+ return res.json(podcastNewStatus);
+ } catch (error) {
+ Logger.error(`[PodcastController] checkPodcastFeed: Error checking podcast feed for podcast ${podcastId}`, error)
+ res.status(500).json({ error: 'An error occurred while checking the podcast feed.' });
+ }
+ }
+
/**
* POST: /api/podcasts/opml
*
diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js
index 0a32e3cad..3948d9f5c 100644
--- a/server/managers/PodcastManager.js
+++ b/server/managers/PodcastManager.js
@@ -267,7 +267,7 @@ class PodcastManager {
const dateToCheckForEpisodesAfter = latestEpisodePublishedAt || lastEpisodeCheckDate
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?.length || 'N/A'} episodes found`)
if (!newEpisodes) {
@@ -282,13 +282,18 @@ class PodcastManager {
} else {
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) {
delete this.failedCheckMap[libraryItem.id]
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
this.downloadPodcastEpisodes(libraryItem, newEpisodes, true)
+ libraryItem.media.metadata.lastSuccessfulFetchAt = Date.now()
+ libraryItem.media.metadata.feedHealthy = true
} else {
delete this.failedCheckMap[libraryItem.id]
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()
@@ -305,7 +310,7 @@ class PodcastManager {
}
const feed = await getPodcastFeed(podcastLibraryItem.media.metadata.feedUrl)
if (!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
}
@@ -322,12 +327,18 @@ class PodcastManager {
async checkAndDownloadNewEpisodes(libraryItem, maxEpisodesToDownload) {
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
- var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck, maxEpisodesToDownload)
- if (newEpisodes.length) {
+ let newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck, maxEpisodesToDownload)
+ 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`)
this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
+ libraryItem.media.metadata.lastSuccessfulFetchAt = Date.now()
+ libraryItem.media.metadata.feedHealthy = true
} else {
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()
@@ -338,6 +349,22 @@ class PodcastManager {
return newEpisodes
}
+ async setFeedHealthStatus(podcastId, isHealthy) {
+ const podcast = await Database.podcastModel.findByPk(podcastId)
+
+ if (!podcast) return
+
+ podcast.feedHealthy = isHealthy
+ if (isHealthy) {
+ podcast.lastSuccessfulFetchAt = Date.now()
+ }
+ podcast.lastEpisodeCheck = Date.now()
+ podcast.updatedAt = Date.now()
+ await podcast.save()
+
+ return {lastEpisodeCheck: podcast.lastEpisodeCheck, lastSuccessfulFetchAt: podcast.lastSuccessfulFetchAt, feedHealthy: podcast.feedHealthy}
+ }
+
async findEpisode(rssFeedUrl, searchTitle) {
const feed = await getPodcastFeed(rssFeedUrl).catch(() => {
return null
diff --git a/server/models/Podcast.js b/server/models/Podcast.js
index 60f879d0e..0193384b6 100644
--- a/server/models/Podcast.js
+++ b/server/models/Podcast.js
@@ -57,6 +57,10 @@ class Podcast extends Model {
this.createdAt
/** @type {Date} */
this.updatedAt
+ /** @type {Date} */
+ this.lastSuccessfulFetchAt
+ /** @type {boolean} */
+ this.feedHealthy
}
static getOldPodcast(libraryItemExpanded) {
@@ -78,7 +82,9 @@ class Podcast extends Model {
itunesArtistId: podcastExpanded.itunesArtistId,
explicit: podcastExpanded.explicit,
language: podcastExpanded.language,
- type: podcastExpanded.podcastType
+ type: podcastExpanded.podcastType,
+ lastSuccessfulFetchAt: podcastExpanded.lastSuccessfulFetchAt?.valueOf() || null,
+ feedHealthy: !!podcastExpanded.feedHealthy
},
coverPath: podcastExpanded.coverPath,
tags: podcastExpanded.tags,
@@ -115,10 +121,18 @@ class Podcast extends Model {
maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload,
coverPath: oldPodcast.coverPath,
tags: oldPodcast.tags,
- genres: oldPodcastMetadata.genres
+ genres: oldPodcastMetadata.genres,
+ lastSuccessfulFetchAt: oldPodcastMetadata.lastSuccessfulFetchAt,
+ feedHealthy: !!oldPodcastMetadata.feedHealthy
}
}
+ static async getAllIWithFeedSubscriptions() {
+ const podcasts = await this.findAll()
+ const podcastsFiltered = podcasts.filter(p => p.dataValues.feedURL !== null);
+ return podcastsFiltered.map(p => this.getOldPodcast({media: p.dataValues}))
+ }
+
getAbsMetadataJson() {
return {
tags: this.tags || [],
@@ -171,8 +185,10 @@ class Podcast extends Model {
maxNewEpisodesToDownload: DataTypes.INTEGER,
coverPath: DataTypes.STRING,
tags: DataTypes.JSON,
- genres: DataTypes.JSON
- },
+ genres: DataTypes.JSON,
+ lastSuccessfulFetchAt: DataTypes.DATE,
+ feedHealthy: DataTypes.BOOLEAN
+ },
{
sequelize,
modelName: 'podcast'
diff --git a/server/objects/metadata/PodcastMetadata.js b/server/objects/metadata/PodcastMetadata.js
index 8300e93a6..42b1eab69 100644
--- a/server/objects/metadata/PodcastMetadata.js
+++ b/server/objects/metadata/PodcastMetadata.js
@@ -16,6 +16,8 @@ class PodcastMetadata {
this.explicit = false
this.language = null
this.type = null
+ this.lastSuccessfulFetchAt = null
+ this.feedHealthy = null
if (metadata) {
this.construct(metadata)
@@ -36,6 +38,8 @@ class PodcastMetadata {
this.explicit = metadata.explicit
this.language = metadata.language || null
this.type = metadata.type || 'episodic'
+ this.lastSuccessfulFetchAt = metadata.lastSuccessfulFetchAt || null
+ this.feedHealthy = metadata.feedHealthy || null
}
toJSON() {
@@ -52,7 +56,9 @@ class PodcastMetadata {
itunesArtistId: this.itunesArtistId,
explicit: this.explicit,
language: this.language,
- type: this.type
+ type: this.type,
+ lastSuccessfulFetchAt: this.lastSuccessfulFetchAt,
+ feedHealthy: this.feedHealthy
}
}
@@ -71,7 +77,9 @@ class PodcastMetadata {
itunesArtistId: this.itunesArtistId,
explicit: this.explicit,
language: this.language,
- type: this.type
+ type: this.type,
+ lastSuccessfulFetchAt: this.lastSuccessfulFetchAt,
+ feedHealthy: this.feedHealthy
}
}
@@ -107,6 +115,8 @@ class PodcastMetadata {
if (mediaMetadata.genres && mediaMetadata.genres.length) {
this.genres = [...mediaMetadata.genres]
}
+ this.lastSuccessfulFetchAt = mediaMetadata.lastSuccessfulFetchAt || null
+ this.feedHealthy = mediaMetadata.feedHealthy || null
}
update(payload) {
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index a92796e8e..7a8c311fe 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -243,6 +243,9 @@ class ApiRouter {
//
this.router.post('/podcasts', PodcastController.create.bind(this))
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
+ this.router.get('/podcasts/external-podcast-feeds-status', PodcastController.getPodcastsWithExternalFeedsSubscriptions.bind(this))
+ this.router.get('/podcasts/:id/feed', PodcastController.middleware.bind(this), PodcastController.checkPodcastFeed.bind(this))
+ this.router.get('/podcasts/:id/check-feed-url', PodcastController.checkPodcastFeedUrl.bind(this))
this.router.post('/podcasts/opml/parse', PodcastController.getFeedsFromOPMLText.bind(this))
this.router.post('/podcasts/opml/create', PodcastController.bulkCreatePodcastsFromOpmlFeedUrls.bind(this))
this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this))
diff --git a/server/utils/migrations/dbMigration.js b/server/utils/migrations/dbMigration.js
index 8337f5aab..c9ca541c9 100644
--- a/server/utils/migrations/dbMigration.js
+++ b/server/utils/migrations/dbMigration.js
@@ -187,7 +187,9 @@ function migratePodcast(oldLibraryItem, LibraryItem) {
updatedAt: LibraryItem.updatedAt,
coverPath: oldPodcast.coverPath,
tags: oldPodcast.tags,
- genres: oldPodcastMetadata.genres
+ genres: oldPodcastMetadata.genres,
+ lastSuccessfulFetchAt: oldPodcastMetadata.lastSuccessfulFetchAt || null,
+ feedHealthy: !!oldPodcastMetadata.feedHealthy || null
}
_newRecords.podcast = Podcast
oldDbIdMap.podcasts[oldLibraryItem.id] = Podcast.id