Add:Podcast auto-download option to delete an episode if it exceeds X max episodes to keep #903

This commit is contained in:
advplyr 2022-08-15 17:35:13 -05:00
parent 2c0c53bbf1
commit 7a69afdcd9
6 changed files with 85 additions and 14 deletions

View File

@ -1,8 +1,10 @@
<template>
<div class="w-full">
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
</p>
<slot>
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
</p>
</slot>
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" @blur="inputBlurred" />
</div>
</template>

View File

@ -40,8 +40,20 @@
</div>
</div>
<div class="flex-grow px-1 pt-6">
<ui-checkbox v-model="autoDownloadEpisodes" label="Auto Download New Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
<div class="flex items-center px-1 pt-6">
<div class="w-1/2 px-1 py-5">
<ui-checkbox v-model="autoDownloadEpisodes" label="Auto Download New Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div>
<div v-if="autoDownloadEpisodes" class="w-1/2 px-1">
<ui-text-input-with-label ref="maxEpisodesToKeep" v-model="maxEpisodesToKeep" type="number" class="max-w-48">
<ui-tooltip direction="bottom" text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes.">
<p class="text-sm">
Max episodes to keep
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</ui-text-input-with-label>
</div>
</div>
</form>
</div>
@ -72,6 +84,7 @@ export default {
language: null
},
autoDownloadEpisodes: false,
maxEpisodesToKeep: 0,
newTags: []
}
},
@ -199,6 +212,9 @@ export default {
if (this.media.autoDownloadEpisodes !== this.autoDownloadEpisodes) {
updatePayload.autoDownloadEpisodes = !!this.autoDownloadEpisodes
}
if (this.autoDownloadEpisodes && !isNaN(this.maxEpisodesToKeep) && Number(this.maxEpisodesToKeep) != this.media.maxEpisodesToKeep) {
updatePayload.maxEpisodesToKeep = Number(this.maxEpisodesToKeep)
}
return {
updatePayload,
@ -220,6 +236,7 @@ export default {
this.details.explicit = !!this.mediaMetadata.explicit
this.autoDownloadEpisodes = !!this.media.autoDownloadEpisodes
this.maxEpisodesToKeep = this.media.maxEpisodesToKeep || 0
this.newTags = [...(this.media.tags || [])]
},
submitForm() {

View File

@ -5,7 +5,7 @@ const axios = require('axios')
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
const Logger = require('../Logger')
const { downloadFile } = require('../utils/fileUtils')
const { downloadFile, removeFile } = require('../utils/fileUtils')
const { levenshteinDistance } = require('../utils/index')
const opmlParser = require('../utils/parsers/parseOPML')
const prober = require('../utils/prober')
@ -56,14 +56,14 @@ class PodcastManager {
}
}
async downloadPodcastEpisodes(libraryItem, episodesToDownload) {
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
var index = libraryItem.media.episodes.length + 1
episodesToDownload.forEach((ep) => {
var newPe = new PodcastEpisode()
newPe.setData(ep, index++)
newPe.libraryItemId = libraryItem.id
var newPeDl = new PodcastEpisodeDownload()
newPeDl.setData(newPe, libraryItem)
newPeDl.setData(newPe, libraryItem, isAutoDownload)
this.startPodcastEpisodeDownload(newPeDl)
})
}
@ -131,12 +131,46 @@ class PodcastManager {
libraryItem.isInvalid = false
}
libraryItem.libraryFiles.push(libraryFile)
// Check setting maxEpisodesToKeep and remove episode if necessary
if (this.currentDownload.isAutoDownload) { // only applies for auto-downloaded episodes
if (libraryItem.media.maxEpisodesToKeep && libraryItem.media.episodesWithPubDate.length > libraryItem.media.maxEpisodesToKeep) {
Logger.info(`[PodcastManager] # of episodes (${libraryItem.media.episodesWithPubDate.length}) exceeds max episodes to keep (${libraryItem.media.maxEpisodesToKeep})`)
await this.removeOldestEpisode(libraryItem, podcastEpisode.id)
}
}
libraryItem.updatedAt = Date.now()
await this.db.updateLibraryItem(libraryItem)
this.emitter('item_updated', libraryItem.toJSONExpanded())
return true
}
async removeOldestEpisode(libraryItem, episodeIdJustDownloaded) {
var smallestPublishedAt = 0
var oldestEpisode = null
libraryItem.media.episodesWithPubDate.filter(ep => ep.id !== episodeIdJustDownloaded).forEach((ep) => {
if (!smallestPublishedAt || ep.publishedAt < smallestPublishedAt) {
smallestPublishedAt = ep.publishedAt
oldestEpisode = ep
}
})
// TODO: Should we check for open playback sessions for this episode?
// TODO: remove all user progress for this episode
if (oldestEpisode && oldestEpisode.audioFile) {
Logger.info(`[PodcastManager] Deleting oldest episode "${oldestEpisode.title}"`)
const successfullyDeleted = await removeFile(oldestEpisode.audioFile.metadata.path)
if (successfullyDeleted) {
libraryItem.media.removeEpisode(oldestEpisode.id)
libraryItem.removeLibraryFile(oldestEpisode.audioFile.ino)
return true
} else {
Logger.warn(`[PodcastManager] Failed to remove oldest episode "${oldestEpisode.title}"`)
}
}
return false
}
async getLibraryFile(path, relPath) {
var newLibFile = new LibraryFile()
await newLibFile.setDataFromPath(path, relPath)
@ -211,7 +245,7 @@ class PodcastManager {
} 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)
this.downloadPodcastEpisodes(libraryItem, newEpisodes, true)
} else {
delete this.failedCheckMap[libraryItem.id]
Logger.debug(`[PodcastManager] No new episodes for "${libraryItem.media.metadata.title}"`)
@ -248,7 +282,7 @@ class PodcastManager {
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, libraryItem.media.lastEpisodeCheck)
if (newEpisodes.length) {
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
this.downloadPodcastEpisodes(libraryItem, newEpisodes)
this.downloadPodcastEpisodes(libraryItem, newEpisodes, false)
} else {
Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`)
}

View File

@ -9,6 +9,7 @@ class PodcastEpisodeDownload {
this.url = null
this.libraryItem = null
this.isAutoDownload = false
this.isDownloading = false
this.isFinished = false
this.failed = false
@ -46,11 +47,12 @@ class PodcastEpisodeDownload {
return this.libraryItem ? this.libraryItem.id : null
}
setData(podcastEpisode, libraryItem) {
setData(podcastEpisode, libraryItem, isAutoDownload) {
this.id = getId('epdl')
this.podcastEpisode = podcastEpisode
this.url = podcastEpisode.enclosure.url
this.libraryItem = libraryItem
this.isAutoDownload = isAutoDownload
this.createdAt = Date.now()
}

View File

@ -19,6 +19,7 @@ class Podcast {
this.autoDownloadEpisodes = false
this.lastEpisodeCheck = 0
this.maxEpisodesToKeep = 0
this.lastCoverSearch = null
this.lastCoverSearchQuery = null
@ -40,6 +41,7 @@ class Podcast {
})
this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
}
toJSON() {
@ -50,7 +52,8 @@ class Podcast {
tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()),
autoDownloadEpisodes: this.autoDownloadEpisodes,
lastEpisodeCheck: this.lastEpisodeCheck
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep
}
}
@ -62,6 +65,7 @@ class Podcast {
numEpisodes: this.episodes.length,
autoDownloadEpisodes: this.autoDownloadEpisodes,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
size: this.size
}
}
@ -75,6 +79,7 @@ class Podcast {
episodes: this.episodes.map(e => e.toJSONExpanded()),
autoDownloadEpisodes: this.autoDownloadEpisodes,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
size: this.size
}
}
@ -113,6 +118,9 @@ class Podcast {
})
return largestPublishedAt
}
get episodesWithPubDate() {
return this.episodes.filter(ep => !!ep.publishedAt)
}
update(payload) {
var json = this.toJSON()

View File

@ -16,7 +16,7 @@ async function getFileStat(path) {
birthtime: stat.birthtime
}
} catch (err) {
console.error('Failed to stat', err)
Logger.error('[fileUtils] Failed to stat', err)
return false
}
}
@ -33,7 +33,7 @@ async function getFileTimestampsWithIno(path) {
ino: String(stat.ino)
}
} catch (err) {
console.error('Failed to getFileTimestampsWithIno', err)
Logger.error('[fileUtils] Failed to getFileTimestampsWithIno', err)
return false
}
}
@ -219,4 +219,12 @@ module.exports.getAudioMimeTypeFromExtname = (extname) => {
const formatUpper = extname.slice(1).toUpperCase()
if (AudioMimeType[formatUpper]) return AudioMimeType[formatUpper]
return null
}
module.exports.removeFile = (path) => {
if (!path) return false
return fs.remove(path).then(() => true).catch((error) => {
Logger.error(`[fileUtils] Failed remove file "${path}"`, error)
return false
})
}