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 @@ + + + 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 @@ -
- - - - - - - - - - - +
+
+
+

{{$strings.HeaderHostedRSSFeeds}}

+
+
+

{{$strings.HeaderExternalFeedURLHealthChecker}}

+
+
+
+ + +
- + @@ -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
{{ $strings.LabelTitle }}{{ $strings.HeaderEpisodes }}