Fix:Podcast download new episode check to compare both GUID and enclosure URL for existing episodes #2986

This commit is contained in:
advplyr 2024-05-18 09:33:48 -05:00
parent ab3a137db9
commit 6d89721371
3 changed files with 60 additions and 50 deletions

View File

@ -32,7 +32,7 @@ class PodcastManager {
} }
getEpisodeDownloadsInQueue(libraryItemId) { getEpisodeDownloadsInQueue(libraryItemId) {
return this.downloadQueue.filter(d => d.libraryItemId === libraryItemId) return this.downloadQueue.filter((d) => d.libraryItemId === libraryItemId)
} }
clearDownloadQueue(libraryItemId = null) { clearDownloadQueue(libraryItemId = null) {
@ -44,12 +44,12 @@ class PodcastManager {
} else { } else {
var itemDownloads = this.getEpisodeDownloadsInQueue(libraryItemId) var itemDownloads = this.getEpisodeDownloadsInQueue(libraryItemId)
Logger.info(`[PodcastManager] Clearing downloads in queue for item "${libraryItemId}" (${itemDownloads.length})`) Logger.info(`[PodcastManager] Clearing downloads in queue for item "${libraryItemId}" (${itemDownloads.length})`)
this.downloadQueue = this.downloadQueue.filter(d => d.libraryItemId !== libraryItemId) this.downloadQueue = this.downloadQueue.filter((d) => d.libraryItemId !== libraryItemId)
} }
} }
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) { async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
let index = Math.max(...libraryItem.media.episodes.filter(ep => ep.index == null || isNaN(ep.index)).map(ep => Number(ep.index))) + 1 let index = Math.max(...libraryItem.media.episodes.filter((ep) => ep.index == null || isNaN(ep.index)).map((ep) => Number(ep.index))) + 1
for (const ep of episodesToDownload) { for (const ep of episodesToDownload) {
const newPe = new PodcastEpisode() const newPe = new PodcastEpisode()
newPe.setData(ep, index++) newPe.setData(ep, index++)
@ -72,7 +72,7 @@ class PodcastManager {
const taskDescription = `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".` const taskDescription = `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".`
const taskData = { const taskData = {
libraryId: podcastEpisodeDownload.libraryId, libraryId: podcastEpisodeDownload.libraryId,
libraryItemId: podcastEpisodeDownload.libraryItemId, libraryItemId: podcastEpisodeDownload.libraryItemId
} }
const task = TaskManager.createAndAddTask('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData) const task = TaskManager.createAndAddTask('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData)
@ -104,10 +104,12 @@ class PodcastManager {
}) })
} else { } else {
// Download episode only // Download episode only
success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath).then(() => true).catch((error) => { success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath)
Logger.error(`[PodcastManager] Podcast Episode download failed`, error) .then(() => true)
return false .catch((error) => {
}) Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
return false
})
} }
if (success) { if (success) {
@ -156,7 +158,7 @@ class PodcastManager {
podcastEpisode.audioFile = audioFile podcastEpisode.audioFile = audioFile
if (audioFile.chapters?.length) { if (audioFile.chapters?.length) {
podcastEpisode.chapters = audioFile.chapters.map(ch => ({ ...ch })) podcastEpisode.chapters = audioFile.chapters.map((ch) => ({ ...ch }))
} }
libraryItem.media.addPodcastEpisode(podcastEpisode) libraryItem.media.addPodcastEpisode(podcastEpisode)
@ -181,7 +183,8 @@ class PodcastManager {
podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded() podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded()
SocketAuthority.emitter('episode_added', podcastEpisodeExpanded) SocketAuthority.emitter('episode_added', podcastEpisodeExpanded)
if (this.currentDownload.isAutoDownload) { // Notifications only for auto downloaded episodes if (this.currentDownload.isAutoDownload) {
// Notifications only for auto downloaded episodes
this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode) this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
} }
@ -191,12 +194,14 @@ class PodcastManager {
async removeOldestEpisode(libraryItem, episodeIdJustDownloaded) { async removeOldestEpisode(libraryItem, episodeIdJustDownloaded) {
var smallestPublishedAt = 0 var smallestPublishedAt = 0
var oldestEpisode = null var oldestEpisode = null
libraryItem.media.episodesWithPubDate.filter(ep => ep.id !== episodeIdJustDownloaded).forEach((ep) => { libraryItem.media.episodesWithPubDate
if (!smallestPublishedAt || ep.publishedAt < smallestPublishedAt) { .filter((ep) => ep.id !== episodeIdJustDownloaded)
smallestPublishedAt = ep.publishedAt .forEach((ep) => {
oldestEpisode = ep if (!smallestPublishedAt || ep.publishedAt < smallestPublishedAt) {
} smallestPublishedAt = ep.publishedAt
}) oldestEpisode = ep
}
})
// TODO: Should we check for open playback sessions for this episode? // TODO: Should we check for open playback sessions for this episode?
// TODO: remove all user progress for this episode // TODO: remove all user progress for this episode
if (oldestEpisode?.audioFile) { if (oldestEpisode?.audioFile) {
@ -246,7 +251,8 @@ class PodcastManager {
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload) var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem, dateToCheckForEpisodesAfter, libraryItem.media.maxNewEpisodesToDownload)
Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes?.length || 'N/A'} episodes found`) Logger.debug(`[PodcastManager] runEpisodeCheck: ${newEpisodes?.length || 'N/A'} episodes found`)
if (!newEpisodes) { // Failed if (!newEpisodes) {
// Failed
// Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download // Allow up to MaxFailedEpisodeChecks failed attempts before disabling auto download
if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0 if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
this.failedCheckMap[libraryItem.id]++ this.failedCheckMap[libraryItem.id]++
@ -285,7 +291,7 @@ class PodcastManager {
} }
// Filter new and not already has // Filter new and not already has
let newEpisodes = feed.episodes.filter(ep => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url)) let newEpisodes = feed.episodes.filter((ep) => ep.publishedAt > dateToCheckForEpisodesAfter && !podcastLibraryItem.media.checkHasEpisodeByFeedEpisode(ep))
if (maxNewEpisodes > 0) { if (maxNewEpisodes > 0) {
newEpisodes = newEpisodes.slice(0, maxNewEpisodes) newEpisodes = newEpisodes.slice(0, maxNewEpisodes)
@ -322,7 +328,7 @@ class PodcastManager {
} }
const matches = [] const matches = []
feed.episodes.forEach(ep => { feed.episodes.forEach((ep) => {
if (!ep.title) return if (!ep.title) return
const epTitle = ep.title.toLowerCase().trim() const epTitle = ep.title.toLowerCase().trim()
@ -370,7 +376,7 @@ class PodcastManager {
/** /**
* OPML file string for podcasts in a library * OPML file string for podcasts in a library
* @param {import('../models/Podcast')[]} podcasts * @param {import('../models/Podcast')[]} podcasts
* @returns {string} XML string * @returns {string} XML string
*/ */
generateOPMLFileText(podcasts) { generateOPMLFileText(podcasts) {
@ -383,7 +389,7 @@ class PodcastManager {
return { return {
currentDownload: _currentDownload?.toJSONForClient(), currentDownload: _currentDownload?.toJSONForClient(),
queue: this.downloadQueue.filter(item => !libraryId || item.libraryId === libraryId).map(item => item.toJSONForClient()) queue: this.downloadQueue.filter((item) => !libraryId || item.libraryId === libraryId).map((item) => item.toJSONForClient())
} }
} }
} }

View File

@ -1,4 +1,4 @@
const uuidv4 = require("uuid").v4 const uuidv4 = require('uuid').v4
const { areEquivalent, copyValue } = require('../../utils/index') const { areEquivalent, copyValue } = require('../../utils/index')
const AudioFile = require('../files/AudioFile') const AudioFile = require('../files/AudioFile')
const AudioTrack = require('../files/AudioTrack') const AudioTrack = require('../files/AudioTrack')
@ -47,7 +47,7 @@ class PodcastEpisode {
this.enclosure = episode.enclosure ? { ...episode.enclosure } : null this.enclosure = episode.enclosure ? { ...episode.enclosure } : null
this.guid = episode.guid || null this.guid = episode.guid || null
this.pubDate = episode.pubDate this.pubDate = episode.pubDate
this.chapters = episode.chapters?.map(ch => ({ ...ch })) || [] this.chapters = episode.chapters?.map((ch) => ({ ...ch })) || []
this.audioFile = episode.audioFile ? new AudioFile(episode.audioFile) : null this.audioFile = episode.audioFile ? new AudioFile(episode.audioFile) : null
this.publishedAt = episode.publishedAt this.publishedAt = episode.publishedAt
this.addedAt = episode.addedAt this.addedAt = episode.addedAt
@ -74,7 +74,7 @@ class PodcastEpisode {
enclosure: this.enclosure ? { ...this.enclosure } : null, enclosure: this.enclosure ? { ...this.enclosure } : null,
guid: this.guid, guid: this.guid,
pubDate: this.pubDate, pubDate: this.pubDate,
chapters: this.chapters.map(ch => ({ ...ch })), chapters: this.chapters.map((ch) => ({ ...ch })),
audioFile: this.audioFile?.toJSON() || null, audioFile: this.audioFile?.toJSON() || null,
publishedAt: this.publishedAt, publishedAt: this.publishedAt,
addedAt: this.addedAt, addedAt: this.addedAt,
@ -98,7 +98,7 @@ class PodcastEpisode {
enclosure: this.enclosure ? { ...this.enclosure } : null, enclosure: this.enclosure ? { ...this.enclosure } : null,
guid: this.guid, guid: this.guid,
pubDate: this.pubDate, pubDate: this.pubDate,
chapters: this.chapters.map(ch => ({ ...ch })), chapters: this.chapters.map((ch) => ({ ...ch })),
audioFile: this.audioFile?.toJSON() || null, audioFile: this.audioFile?.toJSON() || null,
audioTrack: this.audioTrack?.toJSON() || null, audioTrack: this.audioTrack?.toJSON() || null,
publishedAt: this.publishedAt, publishedAt: this.publishedAt,
@ -121,7 +121,9 @@ class PodcastEpisode {
get duration() { get duration() {
return this.audioFile?.duration || 0 return this.audioFile?.duration || 0
} }
get size() { return this.audioFile?.metadata.size || 0 } get size() {
return this.audioFile?.metadata.size || 0
}
get enclosureUrl() { get enclosureUrl() {
return this.enclosure?.url || null return this.enclosure?.url || null
} }
@ -151,9 +153,9 @@ class PodcastEpisode {
let hasUpdates = false let hasUpdates = false
for (const key in this.toJSON()) { for (const key in this.toJSON()) {
let newValue = payload[key] let newValue = payload[key]
if (newValue === "") newValue = null if (newValue === '') newValue = null
let existingValue = this[key] let existingValue = this[key]
if (existingValue === "") existingValue = null if (existingValue === '') existingValue = null
if (newValue != undefined && !areEquivalent(newValue, existingValue)) { if (newValue != undefined && !areEquivalent(newValue, existingValue)) {
this[key] = copyValue(newValue) this[key] = copyValue(newValue)
@ -177,7 +179,7 @@ class PodcastEpisode {
} }
checkEqualsEnclosureUrl(url) { checkEqualsEnclosureUrl(url) {
if (!this.enclosure || !this.enclosure.url) return false if (!this.enclosure?.url) return false
return this.enclosure.url == url return this.enclosure.url == url
} }
} }

View File

@ -58,7 +58,7 @@ class Podcast {
metadata: this.metadata.toJSON(), metadata: this.metadata.toJSON(),
coverPath: this.coverPath, coverPath: this.coverPath,
tags: [...this.tags], tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()), episodes: this.episodes.map((e) => e.toJSON()),
autoDownloadEpisodes: this.autoDownloadEpisodes, autoDownloadEpisodes: this.autoDownloadEpisodes,
autoDownloadSchedule: this.autoDownloadSchedule, autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck, lastEpisodeCheck: this.lastEpisodeCheck,
@ -90,7 +90,7 @@ class Podcast {
metadata: this.metadata.toJSONExpanded(), metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath, coverPath: this.coverPath,
tags: [...this.tags], tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSONExpanded()), episodes: this.episodes.map((e) => e.toJSONExpanded()),
autoDownloadEpisodes: this.autoDownloadEpisodes, autoDownloadEpisodes: this.autoDownloadEpisodes,
autoDownloadSchedule: this.autoDownloadSchedule, autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck, lastEpisodeCheck: this.lastEpisodeCheck,
@ -121,7 +121,7 @@ class Podcast {
get size() { get size() {
var total = 0 var total = 0
this.episodes.forEach((ep) => total += ep.size) this.episodes.forEach((ep) => (total += ep.size))
return total return total
} }
get hasMediaEntities() { get hasMediaEntities() {
@ -129,7 +129,7 @@ class Podcast {
} }
get duration() { get duration() {
let total = 0 let total = 0
this.episodes.forEach((ep) => total += ep.duration) this.episodes.forEach((ep) => (total += ep.duration))
return total return total
} }
get numTracks() { get numTracks() {
@ -145,7 +145,7 @@ class Podcast {
return largestPublishedAt return largestPublishedAt
} }
get episodesWithPubDate() { get episodesWithPubDate() {
return this.episodes.filter(ep => !!ep.publishedAt) return this.episodes.filter((ep) => !!ep.publishedAt)
} }
update(payload) { update(payload) {
@ -169,7 +169,7 @@ class Podcast {
} }
updateEpisode(id, payload) { updateEpisode(id, payload) {
var episode = this.episodes.find(ep => ep.id == id) var episode = this.episodes.find((ep) => ep.id == id)
if (!episode) return false if (!episode) return false
return episode.update(payload) return episode.update(payload)
} }
@ -182,15 +182,15 @@ class Podcast {
} }
removeFileWithInode(inode) { removeFileWithInode(inode) {
const hasEpisode = this.episodes.some(ep => ep.audioFile.ino === inode) const hasEpisode = this.episodes.some((ep) => ep.audioFile.ino === inode)
if (hasEpisode) { if (hasEpisode) {
this.episodes = this.episodes.filter(ep => ep.audioFile.ino !== inode) this.episodes = this.episodes.filter((ep) => ep.audioFile.ino !== inode)
} }
return hasEpisode return hasEpisode
} }
findFileWithInode(inode) { findFileWithInode(inode) {
var episode = this.episodes.find(ep => ep.audioFile.ino === inode) var episode = this.episodes.find((ep) => ep.audioFile.ino === inode)
if (episode) return episode.audioFile if (episode) return episode.audioFile
return null return null
} }
@ -208,21 +208,23 @@ class Podcast {
} }
checkHasEpisode(episodeId) { checkHasEpisode(episodeId) {
return this.episodes.some(ep => ep.id === episodeId) return this.episodes.some((ep) => ep.id === episodeId)
} }
checkHasEpisodeByFeedUrl(url) { checkHasEpisodeByFeedEpisode(feedEpisode) {
return this.episodes.some(ep => ep.checkEqualsEnclosureUrl(url)) const guid = feedEpisode.guid
const url = feedEpisode.enclosure.url
return this.episodes.some((ep) => (ep.guid && ep.guid === guid) || ep.checkEqualsEnclosureUrl(url))
} }
// Only checks container format // Only checks container format
checkCanDirectPlay(payload, episodeId) { checkCanDirectPlay(payload, episodeId) {
var episode = this.episodes.find(ep => ep.id === episodeId) var episode = this.episodes.find((ep) => ep.id === episodeId)
if (!episode) return false if (!episode) return false
return episode.checkCanDirectPlay(payload) return episode.checkCanDirectPlay(payload)
} }
getDirectPlayTracklist(episodeId) { getDirectPlayTracklist(episodeId) {
var episode = this.episodes.find(ep => ep.id === episodeId) var episode = this.episodes.find((ep) => ep.id === episodeId)
if (!episode) return false if (!episode) return false
return episode.getDirectPlayTracklist() return episode.getDirectPlayTracklist()
} }
@ -241,15 +243,15 @@ class Podcast {
} }
removeEpisode(episodeId) { removeEpisode(episodeId) {
const episode = this.episodes.find(ep => ep.id === episodeId) const episode = this.episodes.find((ep) => ep.id === episodeId)
if (episode) { if (episode) {
this.episodes = this.episodes.filter(ep => ep.id !== episodeId) this.episodes = this.episodes.filter((ep) => ep.id !== episodeId)
} }
return episode return episode
} }
getPlaybackTitle(episodeId) { getPlaybackTitle(episodeId) {
var episode = this.episodes.find(ep => ep.id == episodeId) var episode = this.episodes.find((ep) => ep.id == episodeId)
if (!episode) return this.metadata.title if (!episode) return this.metadata.title
return episode.title return episode.title
} }
@ -259,7 +261,7 @@ class Podcast {
} }
getEpisodeDuration(episodeId) { getEpisodeDuration(episodeId) {
var episode = this.episodes.find(ep => ep.id == episodeId) var episode = this.episodes.find((ep) => ep.id == episodeId)
if (!episode) return 0 if (!episode) return 0
return episode.duration return episode.duration
} }
@ -268,13 +270,13 @@ class Podcast {
if (!episodeId) return null if (!episodeId) return null
// Support old episode ids for mobile downloads // Support old episode ids for mobile downloads
if (episodeId.startsWith('ep_')) return this.episodes.find(ep => ep.oldEpisodeId == episodeId) if (episodeId.startsWith('ep_')) return this.episodes.find((ep) => ep.oldEpisodeId == episodeId)
return this.episodes.find(ep => ep.id == episodeId) return this.episodes.find((ep) => ep.id == episodeId)
} }
getChapters(episodeId) { getChapters(episodeId) {
return this.getEpisode(episodeId)?.chapters?.map(ch => ({ ...ch })) || [] return this.getEpisode(episodeId)?.chapters?.map((ch) => ({ ...ch })) || []
} }
} }
module.exports = Podcast module.exports = Podcast