diff --git a/client/components/widgets/PodcastDetailsEdit.vue b/client/components/widgets/PodcastDetailsEdit.vue
index 4c2fd739a..c93d7f84c 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: []
}
@@ -229,6 +239,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 28dba6704..d62cf6d08 100644
--- a/client/pages/config/rss-feeds.vue
+++ b/client/pages/config/rss-feeds.vue
@@ -1,61 +1,180 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -63,9 +182,18 @@
export default {
data() {
return {
+ showIncomingFeedsView: false,
showFeedModal: false,
selectedFeed: null,
- feeds: []
+ feeds: [],
+ incomingFeeds: [],
+ feedsSearch: null,
+ feedsSearchTimeout: null,
+ feedsSearchText: null,
+ incomingFeedsSearch: null,
+ incomingFeedsSearchTimeout: null,
+ incomingFeedsSearchText: null,
+ incomingFeedShowOnlyUnhealthy: false,
}
},
computed: {
@@ -74,13 +202,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)
+ })
+ },
+ incomingFeedsSorted() {
+ return this.incomingFeedShowOnlyUnhealthy
+ ? this.incomingFeeds.filter(incomingFeed => !incomingFeed.metadata.feedHealthy)
+ : this.incomingFeeds;
+ },
+ incomingFeedsList() {
+ return this.incomingFeedsSorted.filter((incomingFeed) => {
+ if (!this.incomingFeedsSearchText) return true
+ if (this.incomingFeedShowOnlyUnhealthy && incomingFeed?.metadata?.feedHealthy) return false
+ return incomingFeed?.metadata?.title?.toLowerCase().includes(this.incomingFeedsSearchText) ||
+ incomingFeed?.metadata?.feedUrl?.toLowerCase().includes(this.incomingFeedsSearchText)
+ })
+ },
},
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)
+ },
+ incomingFeedsSubmit() {},
+ incomingFeedsInputUpdate() {
+ clearTimeout(this.incomingFeedsSearchTimeout)
+ this.incomingFeedsSearchTimeout = setTimeout(() => {
+ if (!this.incomingFeedsSearch || !this.incomingFeedsSearch.trim()) {
+ this.incomingFeedsSearchText = ''
+ return
+ }
+ this.incomingFeedsSearchText = this.incomingFeedsSearch.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,
@@ -96,19 +274,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
@@ -117,9 +295,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)
@@ -131,8 +340,21 @@ export default {
}
this.feeds = data.feeds
},
+ async loadIncomingFeeds() {
+ const data = await this.$axios.$get(`/api/podcasts/incomingFeeds`).catch((err) => {
+ console.error('Failed to load incoming RSS feeds', err)
+ return null
+ })
+ if (!data) {
+ this.$toast.error('Failed to load incoming RSS feeds')
+ return
+ }
+
+ this.incomingFeeds = data.podcasts.map(podcast => ({...podcast, isLoading: false}));
+ },
init() {
this.loadFeeds()
+ this.loadIncomingFeeds()
}
},
mounted() {
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index ae018d4bb..92bd6337e 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -17,6 +17,7 @@
"ButtonCloseFeed": "Close Feed",
"ButtonCollections": "Collections",
"ButtonConfigureScanner": "Configure Scanner",
+ "ButtonCopyFeedURL": "Copy Feed URL",
"ButtonCreate": "Create",
"ButtonCreateBackup": "Create Backup",
"ButtonDelete": "Delete",
@@ -24,6 +25,7 @@
"ButtonEdit": "Edit",
"ButtonEditChapters": "Edit Chapters",
"ButtonEditPodcast": "Edit Podcast",
+ "ButtonForceReCheckFeed": "Force Re-Check Feed",
"ButtonForceReScan": "Force Re-Scan",
"ButtonFullPath": "Full Path",
"ButtonHide": "Hide",
@@ -246,7 +248,14 @@
"LabelEpisodeType": "Episode Type",
"LabelExample": "Example",
"LabelExplicit": "Explicit",
+ "LabelFeedHealthy": "Feed Healthy",
"LabelFeedURL": "Feed URL",
+ "LabelFeedLastChecked": "Last Checked",
+ "LabelFeedLastSuccessfulCheck": "Last Successful Check",
+ "LabelFeedShowOnlyUnhealthy": "Show only unhealthy",
+ "LabelFeedNextAutomaticCheck": "Next Automatic Check",
+ "LabelFeedNotWorking": "Feed is returning errors, check the logs for more information",
+ "LabelFeedWorking": "Feed working as expected",
"LabelFile": "File",
"LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified",
@@ -572,6 +581,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",
@@ -642,6 +652,7 @@
"PlaceholderNewPlaylist": "New playlist name",
"PlaceholderSearch": "Search..",
"PlaceholderSearchEpisode": "Search episode..",
+ "PlaceholderSearchTitle": "Search title..",
"ToastAccountUpdateFailed": "Failed to update account",
"ToastAccountUpdateSuccess": "Account updated",
"ToastAuthorImageRemoveFailed": "Failed to remove image",
@@ -713,4 +724,4 @@
"ToastSocketFailedToConnect": "Socket failed to connect",
"ToastUserDeleteFailed": "Failed to delete user",
"ToastUserDeleteSuccess": "User deleted"
-}
\ No newline at end of file
+}
diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js
index 6f1138fb5..0b16ce719 100644
--- a/server/controllers/PodcastController.js
+++ b/server/controllers/PodcastController.js
@@ -127,14 +127,32 @@ class PodcastController {
const podcast = await getPodcastFeed(libraryItem.media.metadata.feedUrl)
if (!podcast) {
- this.podcastManager.setFeedHealthStatus(libraryItem, false)
+ 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, true)
+ 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.' });
+ }
+ }
+
async getFeedsFromOPMLText(req, res) {
if (!req.body.opmlText) {
return res.sendStatus(400)
diff --git a/server/models/Podcast.js b/server/models/Podcast.js
index a1c58d4bb..d5dd5c65a 100644
--- a/server/models/Podcast.js
+++ b/server/models/Podcast.js
@@ -1,170 +1,171 @@
-const { DataTypes, Model } = require('sequelize')
+const {DataTypes, Model} = require('sequelize')
class Podcast extends Model {
- constructor(values, options) {
- super(values, options)
+ constructor(values, options) {
+ super(values, options)
- /** @type {string} */
- this.id
- /** @type {string} */
- this.title
- /** @type {string} */
- this.titleIgnorePrefix
- /** @type {string} */
- this.author
- /** @type {string} */
- this.releaseDate
- /** @type {string} */
- this.feedURL
- /** @type {string} */
- this.imageURL
- /** @type {string} */
- this.description
- /** @type {string} */
- this.itunesPageURL
- /** @type {string} */
- this.itunesId
- /** @type {string} */
- this.itunesArtistId
- /** @type {string} */
- this.language
- /** @type {string} */
- this.podcastType
- /** @type {boolean} */
- this.explicit
- /** @type {boolean} */
- this.autoDownloadEpisodes
- /** @type {string} */
- this.autoDownloadSchedule
- /** @type {Date} */
- this.lastEpisodeCheck
- /** @type {number} */
- this.maxEpisodesToKeep
- /** @type {string} */
- this.coverPath
- /** @type {string[]} */
- this.tags
- /** @type {string[]} */
- this.genres
- /** @type {Date} */
- this.createdAt
- /** @type {Date} */
- this.updatedAt
- /** @type {Date} */
- this.lastSuccessfulFetchAt
- /** @type {boolean} */
- this.feedHealthy
- }
-
- static getOldPodcast(libraryItemExpanded) {
- const podcastExpanded = libraryItemExpanded.media
- const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id).toJSON()).sort((a, b) => a.index - b.index)
- return {
- id: podcastExpanded.id,
- libraryItemId: libraryItemExpanded.id,
- metadata: {
- title: podcastExpanded.title,
- author: podcastExpanded.author,
- description: podcastExpanded.description,
- releaseDate: podcastExpanded.releaseDate,
- genres: podcastExpanded.genres,
- feedUrl: podcastExpanded.feedURL,
- imageUrl: podcastExpanded.imageURL,
- itunesPageUrl: podcastExpanded.itunesPageURL,
- itunesId: podcastExpanded.itunesId,
- itunesArtistId: podcastExpanded.itunesArtistId,
- explicit: podcastExpanded.explicit,
- language: podcastExpanded.language,
- type: podcastExpanded.podcastType,
- lastSuccessfulFetchAt: podcastExpanded.lastSuccessfulFetchAt?.valueOf() || null,
- feedHealthy: !!podcastExpanded.feedHealthy
- },
- coverPath: podcastExpanded.coverPath,
- tags: podcastExpanded.tags,
- episodes: podcastEpisodes || [],
- autoDownloadEpisodes: podcastExpanded.autoDownloadEpisodes,
- autoDownloadSchedule: podcastExpanded.autoDownloadSchedule,
- lastEpisodeCheck: podcastExpanded.lastEpisodeCheck?.valueOf() || null,
- maxEpisodesToKeep: podcastExpanded.maxEpisodesToKeep,
- maxNewEpisodesToDownload: podcastExpanded.maxNewEpisodesToDownload
+ /** @type {string} */
+ this.id
+ /** @type {string} */
+ this.title
+ /** @type {string} */
+ this.titleIgnorePrefix
+ /** @type {string} */
+ this.author
+ /** @type {string} */
+ this.releaseDate
+ /** @type {string} */
+ this.feedURL
+ /** @type {string} */
+ this.imageURL
+ /** @type {string} */
+ this.description
+ /** @type {string} */
+ this.itunesPageURL
+ /** @type {string} */
+ this.itunesId
+ /** @type {string} */
+ this.itunesArtistId
+ /** @type {string} */
+ this.language
+ /** @type {string} */
+ this.podcastType
+ /** @type {boolean} */
+ this.explicit
+ /** @type {boolean} */
+ this.autoDownloadEpisodes
+ /** @type {string} */
+ this.autoDownloadSchedule
+ /** @type {Date} */
+ this.lastEpisodeCheck
+ /** @type {number} */
+ this.maxEpisodesToKeep
+ /** @type {string} */
+ this.coverPath
+ /** @type {string[]} */
+ this.tags
+ /** @type {string[]} */
+ this.genres
+ /** @type {Date} */
+ this.createdAt
+ /** @type {Date} */
+ this.updatedAt
+ /** @type {Date} */
+ this.lastSuccessfulFetchAt
+ /** @type {boolean} */
+ this.feedHealthy
}
- }
- static getFromOld(oldPodcast) {
- const oldPodcastMetadata = oldPodcast.metadata
- return {
- id: oldPodcast.id,
- title: oldPodcastMetadata.title,
- titleIgnorePrefix: oldPodcastMetadata.titleIgnorePrefix,
- author: oldPodcastMetadata.author,
- releaseDate: oldPodcastMetadata.releaseDate,
- feedURL: oldPodcastMetadata.feedUrl,
- imageURL: oldPodcastMetadata.imageUrl,
- description: oldPodcastMetadata.description,
- itunesPageURL: oldPodcastMetadata.itunesPageUrl,
- itunesId: oldPodcastMetadata.itunesId,
- itunesArtistId: oldPodcastMetadata.itunesArtistId,
- language: oldPodcastMetadata.language,
- podcastType: oldPodcastMetadata.type,
- explicit: !!oldPodcastMetadata.explicit,
- autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes,
- autoDownloadSchedule: oldPodcast.autoDownloadSchedule,
- lastEpisodeCheck: oldPodcast.lastEpisodeCheck,
- maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep,
- maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload,
- coverPath: oldPodcast.coverPath,
- tags: oldPodcast.tags,
- genres: oldPodcastMetadata.genres,
- lastSuccessfulFetchAt: oldPodcastMetadata.lastSuccessfulFetchAt,
- feedHealthy: !!oldPodcastMetadata.feedHealthy
+ static getOldPodcast(libraryItemExpanded) {
+ const podcastExpanded = libraryItemExpanded.media
+ const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id).toJSON()).sort((a, b) => a.index - b.index)
+ return {
+ id: podcastExpanded.id,
+ libraryItemId: libraryItemExpanded.id,
+ metadata: {
+ title: podcastExpanded.title,
+ author: podcastExpanded.author,
+ description: podcastExpanded.description,
+ releaseDate: podcastExpanded.releaseDate,
+ genres: podcastExpanded.genres,
+ feedUrl: podcastExpanded.feedURL,
+ imageUrl: podcastExpanded.imageURL,
+ itunesPageUrl: podcastExpanded.itunesPageURL,
+ itunesId: podcastExpanded.itunesId,
+ itunesArtistId: podcastExpanded.itunesArtistId,
+ explicit: podcastExpanded.explicit,
+ language: podcastExpanded.language,
+ type: podcastExpanded.podcastType,
+ lastSuccessfulFetchAt: podcastExpanded.lastSuccessfulFetchAt?.valueOf() || null,
+ feedHealthy: !!podcastExpanded.feedHealthy
+ },
+ coverPath: podcastExpanded.coverPath,
+ tags: podcastExpanded.tags,
+ episodes: podcastEpisodes || [],
+ autoDownloadEpisodes: podcastExpanded.autoDownloadEpisodes,
+ autoDownloadSchedule: podcastExpanded.autoDownloadSchedule,
+ lastEpisodeCheck: podcastExpanded.lastEpisodeCheck?.valueOf() || null,
+ maxEpisodesToKeep: podcastExpanded.maxEpisodesToKeep,
+ maxNewEpisodesToDownload: podcastExpanded.maxNewEpisodesToDownload
+ }
}
- }
- static async getAllIncomingFeeds(){
- const podcasts = await this.findAll()
- return podcasts.map(p => this.getFromOld({metadata: p.dataValues}))
- }
+ static getFromOld(oldPodcast) {
+ const oldPodcastMetadata = oldPodcast.metadata
+ return {
+ id: oldPodcast.id,
+ title: oldPodcastMetadata.title,
+ titleIgnorePrefix: oldPodcastMetadata.titleIgnorePrefix,
+ author: oldPodcastMetadata.author,
+ releaseDate: oldPodcastMetadata.releaseDate,
+ feedURL: oldPodcastMetadata.feedUrl,
+ imageURL: oldPodcastMetadata.imageUrl,
+ description: oldPodcastMetadata.description,
+ itunesPageURL: oldPodcastMetadata.itunesPageUrl,
+ itunesId: oldPodcastMetadata.itunesId,
+ itunesArtistId: oldPodcastMetadata.itunesArtistId,
+ language: oldPodcastMetadata.language,
+ podcastType: oldPodcastMetadata.type,
+ explicit: !!oldPodcastMetadata.explicit,
+ autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes,
+ autoDownloadSchedule: oldPodcast.autoDownloadSchedule,
+ lastEpisodeCheck: oldPodcast.lastEpisodeCheck,
+ maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep,
+ maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload,
+ coverPath: oldPodcast.coverPath,
+ tags: oldPodcast.tags,
+ genres: oldPodcastMetadata.genres,
+ lastSuccessfulFetchAt: oldPodcastMetadata.lastSuccessfulFetchAt,
+ feedHealthy: !!oldPodcastMetadata.feedHealthy
+ }
+ }
- /**
- * Initialize model
- * @param {import('../Database').sequelize} sequelize
- */
- static init(sequelize) {
- super.init({
- id: {
- type: DataTypes.UUID,
- defaultValue: DataTypes.UUIDV4,
- primaryKey: true
- },
- title: DataTypes.STRING,
- titleIgnorePrefix: DataTypes.STRING,
- author: DataTypes.STRING,
- releaseDate: DataTypes.STRING,
- feedURL: DataTypes.STRING,
- imageURL: DataTypes.STRING,
- description: DataTypes.TEXT,
- itunesPageURL: DataTypes.STRING,
- itunesId: DataTypes.STRING,
- itunesArtistId: DataTypes.STRING,
- language: DataTypes.STRING,
- podcastType: DataTypes.STRING,
- explicit: DataTypes.BOOLEAN,
+ static async getAllIncomingFeeds() {
+ const podcasts = await this.findAll()
+ const podcastsFiltered = podcasts.filter(p => p.dataValues.feedURL !== null);
+ return podcastsFiltered.map(p => this.getOldPodcast({media: p.dataValues}))
+ }
- autoDownloadEpisodes: DataTypes.BOOLEAN,
- autoDownloadSchedule: DataTypes.STRING,
- lastEpisodeCheck: DataTypes.DATE,
- maxEpisodesToKeep: DataTypes.INTEGER,
- maxNewEpisodesToDownload: DataTypes.INTEGER,
- coverPath: DataTypes.STRING,
- tags: DataTypes.JSON,
- genres: DataTypes.JSON,
- lastSuccessfulFetchAt: DataTypes.DATE,
- feedHealthy: DataTypes.BOOLEAN
- }, {
- sequelize,
- modelName: 'podcast'
- })
- }
+ /**
+ * Initialize model
+ * @param {import('../Database').sequelize} sequelize
+ */
+ static init(sequelize) {
+ super.init({
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ title: DataTypes.STRING,
+ titleIgnorePrefix: DataTypes.STRING,
+ author: DataTypes.STRING,
+ releaseDate: DataTypes.STRING,
+ feedURL: DataTypes.STRING,
+ imageURL: DataTypes.STRING,
+ description: DataTypes.TEXT,
+ itunesPageURL: DataTypes.STRING,
+ itunesId: DataTypes.STRING,
+ itunesArtistId: DataTypes.STRING,
+ language: DataTypes.STRING,
+ podcastType: DataTypes.STRING,
+ explicit: DataTypes.BOOLEAN,
+
+ autoDownloadEpisodes: DataTypes.BOOLEAN,
+ autoDownloadSchedule: DataTypes.STRING,
+ lastEpisodeCheck: DataTypes.DATE,
+ maxEpisodesToKeep: DataTypes.INTEGER,
+ maxNewEpisodesToDownload: DataTypes.INTEGER,
+ coverPath: DataTypes.STRING,
+ tags: DataTypes.JSON,
+ genres: DataTypes.JSON,
+ lastSuccessfulFetchAt: DataTypes.DATE,
+ feedHealthy: DataTypes.BOOLEAN
+ }, {
+ sequelize,
+ modelName: 'podcast'
+ })
+ }
}
module.exports = Podcast
diff --git a/server/objects/metadata/PodcastMetadata.js b/server/objects/metadata/PodcastMetadata.js
index 2c371c6cb..3655d78c7 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
}
}
@@ -120,6 +128,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 f58c0d3ae..9ee2c75be 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -229,6 +229,7 @@ class ApiRouter {
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
this.router.get('/podcasts/incomingFeeds', PodcastController.getPodcastsWithIncomingFeeds.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', PodcastController.getFeedsFromOPMLText.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))