Add a retention timer for podcast
This commit is contained in:
parent
c54221fba2
commit
d890c11a9b
@ -180,6 +180,11 @@ public class JellypodController : ControllerBase
|
|||||||
podcast.MaxEpisodesToKeep = request.MaxEpisodesToKeep.Value;
|
podcast.MaxEpisodesToKeep = request.MaxEpisodesToKeep.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.MaxEpisodeAgeDays.HasValue)
|
||||||
|
{
|
||||||
|
podcast.MaxEpisodeAgeDays = request.MaxEpisodeAgeDays.Value;
|
||||||
|
}
|
||||||
|
|
||||||
await _storageService.UpdatePodcastAsync(podcast).ConfigureAwait(false);
|
await _storageService.UpdatePodcastAsync(podcast).ConfigureAwait(false);
|
||||||
return Ok(podcast);
|
return Ok(podcast);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,4 +14,9 @@ public class UpdatePodcastRequest
|
|||||||
/// Gets or sets the maximum episodes to keep.
|
/// Gets or sets the maximum episodes to keep.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MaxEpisodesToKeep { get; set; }
|
public int? MaxEpisodesToKeep { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum age in days for episodes (0 = use global, -1 = unlimited).
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxEpisodeAgeDays { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
GlobalAutoDownloadEnabled = true;
|
GlobalAutoDownloadEnabled = true;
|
||||||
MaxConcurrentDownloads = 2;
|
MaxConcurrentDownloads = 2;
|
||||||
MaxEpisodesPerPodcast = 50;
|
MaxEpisodesPerPodcast = 50;
|
||||||
|
MaxEpisodeAgeDays = 0;
|
||||||
CreatePodcastFolders = true;
|
CreatePodcastFolders = true;
|
||||||
DownloadNewEpisodesOnly = true;
|
DownloadNewEpisodesOnly = true;
|
||||||
PostDownloadScriptPath = string.Empty;
|
PostDownloadScriptPath = string.Empty;
|
||||||
@ -49,6 +50,12 @@ public class PluginConfiguration : BasePluginConfiguration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxEpisodesPerPodcast { get; set; }
|
public int MaxEpisodesPerPodcast { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum age in days for downloaded episodes (0 = unlimited).
|
||||||
|
/// Episodes older than this will be automatically deleted.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxEpisodeAgeDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether to create subfolders for each podcast.
|
/// Gets or sets a value indicating whether to create subfolders for each podcast.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -164,6 +164,15 @@
|
|||||||
Maximum episodes to keep downloaded per podcast (0 = unlimited)
|
Maximum episodes to keep downloaded per podcast (0 = unlimited)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inputContainer">
|
||||||
|
<label class="inputLabel inputLabelUnfocused" for="MaxEpisodeAgeDays">
|
||||||
|
Max Episode Age (days)
|
||||||
|
</label>
|
||||||
|
<input id="MaxEpisodeAgeDays" name="MaxEpisodeAgeDays" type="number" is="emby-input" min="0" />
|
||||||
|
<div class="fieldDescription">
|
||||||
|
Automatically delete episodes downloaded more than this many days ago (0 = unlimited)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Post-Download Processing -->
|
<!-- Post-Download Processing -->
|
||||||
@ -251,6 +260,7 @@
|
|||||||
document.querySelector('#GlobalAutoDownloadEnabled').checked = config.GlobalAutoDownloadEnabled;
|
document.querySelector('#GlobalAutoDownloadEnabled').checked = config.GlobalAutoDownloadEnabled;
|
||||||
document.querySelector('#MaxConcurrentDownloads').value = config.MaxConcurrentDownloads;
|
document.querySelector('#MaxConcurrentDownloads').value = config.MaxConcurrentDownloads;
|
||||||
document.querySelector('#MaxEpisodesPerPodcast').value = config.MaxEpisodesPerPodcast;
|
document.querySelector('#MaxEpisodesPerPodcast').value = config.MaxEpisodesPerPodcast;
|
||||||
|
document.querySelector('#MaxEpisodeAgeDays').value = config.MaxEpisodeAgeDays;
|
||||||
document.querySelector('#CreatePodcastFolders').checked = config.CreatePodcastFolders;
|
document.querySelector('#CreatePodcastFolders').checked = config.CreatePodcastFolders;
|
||||||
document.querySelector('#PostDownloadScriptPath').value = config.PostDownloadScriptPath || '';
|
document.querySelector('#PostDownloadScriptPath').value = config.PostDownloadScriptPath || '';
|
||||||
document.querySelector('#PostDownloadScriptTimeout').value = config.PostDownloadScriptTimeout || 60;
|
document.querySelector('#PostDownloadScriptTimeout').value = config.PostDownloadScriptTimeout || 60;
|
||||||
@ -432,6 +442,7 @@
|
|||||||
config.GlobalAutoDownloadEnabled = document.querySelector('#GlobalAutoDownloadEnabled').checked;
|
config.GlobalAutoDownloadEnabled = document.querySelector('#GlobalAutoDownloadEnabled').checked;
|
||||||
config.MaxConcurrentDownloads = parseInt(document.querySelector('#MaxConcurrentDownloads').value, 10);
|
config.MaxConcurrentDownloads = parseInt(document.querySelector('#MaxConcurrentDownloads').value, 10);
|
||||||
config.MaxEpisodesPerPodcast = parseInt(document.querySelector('#MaxEpisodesPerPodcast').value, 10);
|
config.MaxEpisodesPerPodcast = parseInt(document.querySelector('#MaxEpisodesPerPodcast').value, 10);
|
||||||
|
config.MaxEpisodeAgeDays = parseInt(document.querySelector('#MaxEpisodeAgeDays').value, 10);
|
||||||
config.CreatePodcastFolders = document.querySelector('#CreatePodcastFolders').checked;
|
config.CreatePodcastFolders = document.querySelector('#CreatePodcastFolders').checked;
|
||||||
config.PostDownloadScriptPath = document.querySelector('#PostDownloadScriptPath').value;
|
config.PostDownloadScriptPath = document.querySelector('#PostDownloadScriptPath').value;
|
||||||
config.PostDownloadScriptTimeout = parseInt(document.querySelector('#PostDownloadScriptTimeout').value, 10);
|
config.PostDownloadScriptTimeout = parseInt(document.querySelector('#PostDownloadScriptTimeout').value, 10);
|
||||||
|
|||||||
@ -69,6 +69,12 @@ public class Podcast
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxEpisodesToKeep { get; set; }
|
public int MaxEpisodesToKeep { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum age in days for downloaded episodes.
|
||||||
|
/// 0 = use global setting, -1 = unlimited, greater than 0 = specific days.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxEpisodeAgeDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of episodes.
|
/// Gets or sets the list of episodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Plugin.Jellypod.Models;
|
||||||
using Jellyfin.Plugin.Jellypod.Services;
|
using Jellyfin.Plugin.Jellypod.Services;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -117,7 +118,22 @@ public class PodcastUpdateTask : IScheduledTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
processedCount++;
|
processedCount++;
|
||||||
progress.Report((double)processedCount / totalPodcasts * 100);
|
progress.Report((double)processedCount / totalPodcasts * 90 / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run cleanup for expired episodes
|
||||||
|
_logger.LogInformation("Starting episode retention cleanup");
|
||||||
|
foreach (var podcast in podcasts)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CleanupExpiredEpisodesAsync(podcast, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to cleanup expired episodes for: {Title}", podcast.Title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
@ -139,4 +155,78 @@ public class PodcastUpdateTask : IScheduledTask
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cleans up episodes that exceed the retention policy.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="podcast">The podcast to clean up.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>A task representing the asynchronous operation.</returns>
|
||||||
|
private async Task CleanupExpiredEpisodesAsync(Podcast podcast, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var config = Plugin.Instance?.Configuration;
|
||||||
|
|
||||||
|
// Determine effective max age for this podcast
|
||||||
|
int effectiveMaxAgeDays;
|
||||||
|
if (podcast.MaxEpisodeAgeDays == -1)
|
||||||
|
{
|
||||||
|
// Per-podcast override: unlimited
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (podcast.MaxEpisodeAgeDays > 0)
|
||||||
|
{
|
||||||
|
// Per-podcast specific value
|
||||||
|
effectiveMaxAgeDays = podcast.MaxEpisodeAgeDays;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use global setting (podcast.MaxEpisodeAgeDays == 0)
|
||||||
|
effectiveMaxAgeDays = config?.MaxEpisodeAgeDays ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 means unlimited
|
||||||
|
if (effectiveMaxAgeDays <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cutoffDate = DateTime.UtcNow.AddDays(-effectiveMaxAgeDays);
|
||||||
|
var expiredEpisodes = podcast.Episodes
|
||||||
|
.Where(e => e.Status == EpisodeStatus.Downloaded
|
||||||
|
&& e.DownloadedDate.HasValue
|
||||||
|
&& e.DownloadedDate.Value < cutoffDate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (expiredEpisodes.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Found {Count} expired episodes for {Podcast} (older than {Days} days)",
|
||||||
|
expiredEpisodes.Count,
|
||||||
|
podcast.Title,
|
||||||
|
effectiveMaxAgeDays);
|
||||||
|
|
||||||
|
foreach (var episode in expiredEpisodes)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _downloadService.DeleteEpisodeFileAsync(episode).ConfigureAwait(false);
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Deleted expired episode: {Title} (downloaded {Date})",
|
||||||
|
episode.Title,
|
||||||
|
episode.DownloadedDate);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to delete expired episode: {Title}", episode.Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes to storage
|
||||||
|
await _storageService.UpdatePodcastAsync(podcast).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user