Add:Podcast option to quick match all unmatched episodes

This commit is contained in:
advplyr 2023-01-04 18:13:46 -06:00
parent 1609f1a499
commit 49c581ed35
4 changed files with 88 additions and 15 deletions

View File

@ -1,8 +1,9 @@
<template> <template>
<div class="w-full py-6"> <div class="w-full py-6">
<p class="text-lg mb-2 font-semibold md:hidden">{{ $strings.HeaderEpisodes }}</p>
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p> <p class="text-lg mb-0 font-semibold hidden md:block">{{ $strings.HeaderEpisodes }}</p>
<div class="flex-grow" /> <div class="flex-grow hidden md:block" />
<template v-if="isSelectionMode"> <template v-if="isSelectionMode">
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom"> <ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" /> <ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
@ -11,8 +12,10 @@
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn> <ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
</template> </template>
<template v-else> <template v-else>
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-32 md:w-36 h-9 ml-1 sm:ml-4" /> <controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 sm:ml-4" />
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-32 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" /> <controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
<div class="flex-grow md:hidden" />
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
</template> </template>
</div> </div>
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
@ -42,15 +45,27 @@ export default {
showPodcastRemoveModal: false, showPodcastRemoveModal: false,
selectedEpisodes: [], selectedEpisodes: [],
episodesToRemove: [], episodesToRemove: [],
processing: false processing: false,
quickMatchingEpisodes: false
} }
}, },
watch: { watch: {
libraryItem() { libraryItem: {
this.init() handler() {
this.init()
}
} }
}, },
computed: { computed: {
contextMenuItems() {
if (!this.userIsAdminOrUp) return []
return [
{
text: 'Quick match all episodes',
action: 'quick-match-episodes'
}
]
},
sortItems() { sortItems() {
return [ return [
{ {
@ -94,8 +109,8 @@ export default {
isSelectionMode() { isSelectionMode() {
return this.selectedEpisodes.length > 0 return this.selectedEpisodes.length > 0
}, },
userCanUpdate() { userIsAdminOrUp() {
return this.$store.getters['user/getUserCanUpdate'] return this.$store.getters['user/getIsAdminOrUp']
}, },
media() { media() {
return this.libraryItem.media || {} return this.libraryItem.media || {}
@ -131,6 +146,44 @@ export default {
} }
}, },
methods: { methods: {
contextMenuAction(action) {
if (action === 'quick-match-episodes') {
if (this.quickMatchingEpisodes) return
this.quickMatchAllEpisodes()
}
},
quickMatchAllEpisodes() {
if (!this.mediaMetadata.feedUrl) {
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
return
}
this.quickMatchingEpisodes = true
const payload = {
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
callback: (confirmed) => {
if (confirmed) {
this.$axios
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
.then((data) => {
if (data.numEpisodesUpdated) {
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
} else {
this.$toast.info('No changes were made')
}
})
.catch((error) => {
console.error('Failed to request match episodes', error)
this.$toast.error('Failed to match episodes')
})
}
this.quickMatchingEpisodes = false
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
addToPlaylist(episode) { addToPlaylist(episode) {
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }]) this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
this.$store.commit('globals/setShowPlaylistsModal', true) this.$store.commit('globals/setShowPlaylistsModal', true)

View File

@ -173,7 +173,7 @@ class PodcastController {
async downloadEpisodes(req, res) { async downloadEpisodes(req, res) {
if (!req.user.isAdminOrUp) { if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user) Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
return res.sendStatus(500) return res.sendStatus(403)
} }
var libraryItem = req.libraryItem var libraryItem = req.libraryItem
@ -186,8 +186,27 @@ class PodcastController {
res.sendStatus(200) res.sendStatus(200)
} }
// POST: api/podcasts/:id/match-episodes
async quickMatchEpisodes(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
return res.sendStatus(403)
}
const overrideDetails = req.query.override === '1'
const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
if (episodesUpdated) {
await this.db.updateLibraryItem(req.libraryItem)
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
}
res.json({
numEpisodesUpdated: episodesUpdated
})
}
async updateEpisode(req, res) { async updateEpisode(req, res) {
var libraryItem = req.libraryItem const libraryItem = req.libraryItem
var episodeId = req.params.episodeId var episodeId = req.params.episodeId
if (!libraryItem.media.checkHasEpisode(episodeId)) { if (!libraryItem.media.checkHasEpisode(episodeId)) {
@ -237,7 +256,7 @@ class PodcastController {
} }
middleware(req, res, next) { middleware(req, res, next) {
var item = this.db.libraryItems.find(li => li.id === req.params.id) const item = this.db.libraryItems.find(li => li.id === req.params.id)
if (!item || !item.media) return res.sendStatus(404) if (!item || !item.media) return res.sendStatus(404)
if (!item.isPodcast) { if (!item.isPodcast) {

View File

@ -229,6 +229,7 @@ class ApiRouter {
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.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.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.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this))
this.router.post('/podcasts/:id/match-episodes', PodcastController.middleware.bind(this), PodcastController.quickMatchEpisodes.bind(this))
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.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)) this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this))

View File

@ -880,15 +880,15 @@ class Scanner {
return false return false
} }
var episodesWereUpdated = false let numEpisodesUpdated = 0
for (const episode of episodesToQuickMatch) { for (const episode of episodesToQuickMatch) {
const episodeMatches = findMatchingEpisodesInFeed(feed, episode.title) const episodeMatches = findMatchingEpisodesInFeed(feed, episode.title)
if (episodeMatches && episodeMatches.length) { if (episodeMatches && episodeMatches.length) {
const wasUpdated = this.updateEpisodeWithMatch(libraryItem, episode, episodeMatches[0].episode, options) const wasUpdated = this.updateEpisodeWithMatch(libraryItem, episode, episodeMatches[0].episode, options)
if (wasUpdated) episodesWereUpdated = true if (wasUpdated) numEpisodesUpdated++
} }
} }
return episodesWereUpdated return numEpisodesUpdated
} }
updateEpisodeWithMatch(libraryItem, episode, episodeToMatch, options = {}) { updateEpisodeWithMatch(libraryItem, episode, episodeToMatch, options = {}) {