diff --git a/client/components/ui/TextInputWithLabel.vue b/client/components/ui/TextInputWithLabel.vue
index c251555d..509274e1 100644
--- a/client/components/ui/TextInputWithLabel.vue
+++ b/client/components/ui/TextInputWithLabel.vue
@@ -1,8 +1,10 @@
-
- {{ label }}{{ note }}
-
+
+
+ {{ label }}{{ note }}
+
+
diff --git a/client/components/widgets/PodcastDetailsEdit.vue b/client/components/widgets/PodcastDetailsEdit.vue
index 305cd571..6470b603 100644
--- a/client/components/widgets/PodcastDetailsEdit.vue
+++ b/client/components/widgets/PodcastDetailsEdit.vue
@@ -40,8 +40,20 @@
-
-
+
+
+
+
+
+
+
+
+ Max episodes to keep
+ info_outlined
+
+
+
+
@@ -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() {
diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js
index b87c0bf0..f0a05d5e 100644
--- a/server/managers/PodcastManager.js
+++ b/server/managers/PodcastManager.js
@@ -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}"`)
}
diff --git a/server/objects/PodcastEpisodeDownload.js b/server/objects/PodcastEpisodeDownload.js
index 09b9d71d..5a005869 100644
--- a/server/objects/PodcastEpisodeDownload.js
+++ b/server/objects/PodcastEpisodeDownload.js
@@ -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()
}
diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js
index 0dd3cafb..bf6d0f67 100644
--- a/server/objects/mediaTypes/Podcast.js
+++ b/server/objects/mediaTypes/Podcast.js
@@ -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()
diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js
index f487a209..6915c287 100644
--- a/server/utils/fileUtils.js
+++ b/server/utils/fileUtils.js
@@ -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
+ })
}
\ No newline at end of file