From 516c5c33087ea08b53f88d8c952cfe9fc9fbc4a7 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 31 Jul 2022 13:12:37 -0500 Subject: [PATCH] Add:Podcast episode match tab and find episode by title api route --- client/components/cards/BookMatchCard.vue | 6 +- .../components/modals/podcast/EditEpisode.vue | 135 ++++----------- .../modals/podcast/tabs/EpisodeDetails.vue | 136 +++++++++++++++ .../modals/podcast/tabs/EpisodeMatch.vue | 156 ++++++++++++++++++ client/layouts/default.vue | 6 + server/controllers/PodcastController.js | 21 ++- server/managers/PodcastManager.js | 34 +++- server/routers/ApiRouter.js | 1 + 8 files changed, 391 insertions(+), 104 deletions(-) create mode 100644 client/components/modals/podcast/tabs/EpisodeDetails.vue create mode 100644 client/components/modals/podcast/tabs/EpisodeMatch.vue diff --git a/client/components/cards/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue index d404d1b7..36b47370 100644 --- a/client/components/cards/BookMatchCard.vue +++ b/client/components/cards/BookMatchCard.vue @@ -1,5 +1,5 @@ -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- Submit -
+
+ +
+ +
+
@@ -41,25 +22,19 @@ export default { data() { return { processing: false, - newEpisode: { - season: null, - episode: null, - episodeType: null, - title: null, - subtitle: null, - description: null, - pubDate: null, - publishedAt: null - }, - pubDateInput: null - } - }, - watch: { - episode: { - immediate: true, - handler(newVal) { - if (newVal) this.init() - } + selectedTab: 'details', + tabs: [ + { + id: 'details', + title: 'Details', + component: 'modals-podcast-tabs-episode-details' + }, + { + id: 'match', + title: 'Match', + component: 'modals-podcast-tabs-episode-match' + } + ] } }, computed: { @@ -77,67 +52,29 @@ export default { episode() { return this.$store.state.globals.selectedEpisode }, - episodeId() { - return this.episode ? this.episode.id : null - }, title() { if (!this.libraryItem) return '' return this.libraryItem.media.metadata.title || 'Unknown' + }, + tabComponentName() { + var _tab = this.tabs.find((t) => t.id === this.selectedTab) + return _tab ? _tab.component : '' } }, methods: { - updatePubDate(val) { - if (val) { - this.newEpisode.pubDate = this.$formatJsDate(new Date(val), 'E, d MMM yyyy HH:mm:ssxx') - this.newEpisode.publishedAt = new Date(val).valueOf() - } else { - this.newEpisode.pubDate = null - this.newEpisode.publishedAt = null - } - }, - init() { - this.newEpisode.season = this.episode.season || '' - this.newEpisode.episode = this.episode.episode || '' - this.newEpisode.episodeType = this.episode.episodeType || '' - this.newEpisode.title = this.episode.title || '' - this.newEpisode.subtitle = this.episode.subtitle || '' - this.newEpisode.description = this.episode.description || '' - this.newEpisode.pubDate = this.episode.pubDate || '' - this.newEpisode.publishedAt = this.episode.publishedAt - - this.pubDateInput = this.episode.pubDate ? this.$formatJsDate(new Date(this.episode.pubDate), "yyyy-MM-dd'T'HH:mm") : null - }, - getUpdatePayload() { - var updatePayload = {} - for (const key in this.newEpisode) { - if (this.newEpisode[key] != this.episode[key]) { - updatePayload[key] = this.newEpisode[key] - } - } - return updatePayload - }, - submit() { - const payload = this.getUpdatePayload() - if (!Object.keys(payload).length) { - return this.$toast.info('No updates were made') - } - - this.processing = true - this.$axios - .$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload) - .then(() => { - this.processing = false - this.$toast.success('Podcast episode updated') - this.show = false - }) - .catch((error) => { - var errorMsg = error.response && error.response.data ? error.response.data : 'Failed update episode' - console.error('Failed update episode', error) - this.processing = false - this.$toast.error(errorMsg) - }) + selectTab(tab) { + this.selectedTab = tab } }, mounted() {} } + + \ No newline at end of file diff --git a/client/components/modals/podcast/tabs/EpisodeDetails.vue b/client/components/modals/podcast/tabs/EpisodeDetails.vue new file mode 100644 index 00000000..5bb5f3e0 --- /dev/null +++ b/client/components/modals/podcast/tabs/EpisodeDetails.vue @@ -0,0 +1,136 @@ + + + \ No newline at end of file diff --git a/client/components/modals/podcast/tabs/EpisodeMatch.vue b/client/components/modals/podcast/tabs/EpisodeMatch.vue new file mode 100644 index 00000000..194deca6 --- /dev/null +++ b/client/components/modals/podcast/tabs/EpisodeMatch.vue @@ -0,0 +1,156 @@ + + + \ No newline at end of file diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 29414485..9e5ab945 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -211,6 +211,12 @@ export default { libraryItemUpdated(libraryItem) { if (this.$store.state.selectedLibraryItem && this.$store.state.selectedLibraryItem.id === libraryItem.id) { this.$store.commit('setSelectedLibraryItem', libraryItem) + if (this.$store.state.globals.selectedEpisode && libraryItem.mediaType === 'podcast') { + const episode = libraryItem.media.episodes.find((ep) => ep.id === this.$store.state.globals.selectedEpisode.id) + if (episode) { + this.$store.commit('globals/setSelectedEpisode', episode) + } + } } this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem) this.$store.commit('libraries/updateFilterDataWithItem', libraryItem) diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 33b600f9..d633c8ed 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -164,6 +164,25 @@ class PodcastController { }) } + async findEpisode(req, res) { + const rssFeedUrl = req.libraryItem.media.metadata.feedUrl + if (!rssFeedUrl) { + Logger.error(`[PodcastController] findEpisode: Podcast has no feed url`) + return res.status(500).send('Podcast does not have an RSS feed URL') + } + + var searchTitle = req.query.title + if (!searchTitle) { + return res.sendStatus(500) + } + searchTitle = searchTitle.toLowerCase().trim() + + const episodes = await this.podcastManager.findEpisode(rssFeedUrl, searchTitle) + res.json({ + episodes: episodes || [] + }) + } + async downloadEpisodes(req, res) { if (!req.user.isAdminOrUp) { Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user) @@ -185,7 +204,7 @@ class PodcastController { var episodeId = req.params.episodeId if (!libraryItem.media.checkHasEpisode(episodeId)) { - return res.status(500).send('Episode not found') + return res.status(404).send('Episode not found') } var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body) diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 16078648..e0c16585 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -6,6 +6,7 @@ const { parsePodcastRssFeedXml } = require('../utils/podcastUtils') const Logger = require('../Logger') const { downloadFile } = require('../utils/fileUtils') +const { levenshteinDistance } = require('../utils/index') const opmlParser = require('../utils/parsers/parseOPML') const prober = require('../utils/prober') const LibraryFile = require('../objects/files/LibraryFile') @@ -259,6 +260,37 @@ class PodcastManager { return newEpisodes } + async findEpisode(rssFeedUrl, searchTitle) { + const feed = await this.getPodcastFeed(rssFeedUrl).catch(() => { + return null + }) + if (!feed || !feed.episodes) { + return null + } + + const matches = [] + feed.episodes.forEach(ep => { + if (!ep.title) return + + const epTitle = ep.title.toLowerCase().trim() + if (epTitle === searchTitle) { + matches.push({ + episode: ep, + levenshtein: 0 + }) + } else { + const levenshtein = levenshteinDistance(searchTitle, epTitle, true) + if (levenshtein <= 6 && epTitle.length > levenshtein) { + matches.push({ + episode: ep, + levenshtein + }) + } + } + }) + return matches.sort((a, b) => a.levenshtein - b.levenshtein) + } + getPodcastFeed(feedUrl, excludeEpisodeMetadata = false) { Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}"`) return axios.get(feedUrl, { timeout: 5000 }).then(async (data) => { @@ -273,7 +305,7 @@ class PodcastManager { } return payload.podcast }).catch((error) => { - console.error('Failed', error) + Logger.error('[PodcastManager] getPodcastFeed Error', error) return false }) } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 4b415280..69abeb62 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -189,6 +189,7 @@ class ApiRouter { 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/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this)) + this.router.get('/podcasts/:id/search-episode', PodcastController.middleware.bind(this), PodcastController.findEpisode.bind(this)) this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this)) this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this)) this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this))