mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-28 09:38:56 +01:00
Add:Podcast auto-download option to delete an episode if it exceeds X max episodes to keep #903
This commit is contained in:
parent
2c0c53bbf1
commit
7a69afdcd9
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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}"`)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user