233 lines
8.1 KiB
C#
233 lines
8.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Plugin.Jellypod.Models;
|
|
using Jellyfin.Plugin.Jellypod.Services;
|
|
using MediaBrowser.Model.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Jellyfin.Plugin.Jellypod.ScheduledTasks;
|
|
|
|
/// <summary>
|
|
/// Scheduled task that updates all podcast feeds and downloads new episodes.
|
|
/// </summary>
|
|
public class PodcastUpdateTask : IScheduledTask
|
|
{
|
|
private readonly ILogger<PodcastUpdateTask> _logger;
|
|
private readonly IRssFeedService _rssFeedService;
|
|
private readonly IPodcastStorageService _storageService;
|
|
private readonly IPodcastDownloadService _downloadService;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PodcastUpdateTask"/> class.
|
|
/// </summary>
|
|
/// <param name="logger">Logger instance.</param>
|
|
/// <param name="rssFeedService">RSS feed service.</param>
|
|
/// <param name="storageService">Storage service.</param>
|
|
/// <param name="downloadService">Download service.</param>
|
|
public PodcastUpdateTask(
|
|
ILogger<PodcastUpdateTask> logger,
|
|
IRssFeedService rssFeedService,
|
|
IPodcastStorageService storageService,
|
|
IPodcastDownloadService downloadService)
|
|
{
|
|
_logger = logger;
|
|
_rssFeedService = rssFeedService;
|
|
_storageService = storageService;
|
|
_downloadService = downloadService;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string Name => "Update Podcast Feeds";
|
|
|
|
/// <inheritdoc />
|
|
public string Key => "JellypodUpdateFeeds";
|
|
|
|
/// <inheritdoc />
|
|
public string Description => "Checks all subscribed podcasts for new episodes and downloads them.";
|
|
|
|
/// <inheritdoc />
|
|
public string Category => "Jellypod";
|
|
|
|
/// <inheritdoc />
|
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("Starting podcast feed update task");
|
|
|
|
var podcasts = await _storageService.GetAllPodcastsAsync().ConfigureAwait(false);
|
|
var totalPodcasts = podcasts.Count;
|
|
var processedCount = 0;
|
|
|
|
foreach (var podcast in podcasts)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
try
|
|
{
|
|
_logger.LogDebug("Updating podcast: {Title}", podcast.Title);
|
|
|
|
var updatedPodcast = await _rssFeedService.FetchPodcastAsync(podcast.FeedUrl, cancellationToken).ConfigureAwait(false);
|
|
if (updatedPodcast != null)
|
|
{
|
|
// Find new episodes (by GUID)
|
|
var existingGuids = podcast.Episodes
|
|
.Select(e => e.EpisodeGuid)
|
|
.Where(g => !string.IsNullOrEmpty(g))
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
var newEpisodes = updatedPodcast.Episodes
|
|
.Where(e => !string.IsNullOrEmpty(e.EpisodeGuid) && !existingGuids.Contains(e.EpisodeGuid))
|
|
.ToList();
|
|
|
|
if (newEpisodes.Count > 0)
|
|
{
|
|
_logger.LogInformation("Found {Count} new episodes for {Title}", newEpisodes.Count, podcast.Title);
|
|
|
|
// Add new episodes to podcast
|
|
foreach (var episode in newEpisodes)
|
|
{
|
|
episode.PodcastId = podcast.Id;
|
|
podcast.Episodes.Insert(0, episode);
|
|
}
|
|
|
|
podcast.LastUpdated = DateTime.UtcNow;
|
|
|
|
// Auto-download if enabled
|
|
var config = Plugin.Instance?.Configuration;
|
|
if (config?.GlobalAutoDownloadEnabled == true && podcast.AutoDownloadEnabled)
|
|
{
|
|
foreach (var episode in newEpisodes)
|
|
{
|
|
await _downloadService.QueueDownloadAsync(podcast, episode).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
await _storageService.UpdatePodcastAsync(podcast).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("No new episodes for {Title}", podcast.Title);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to update podcast: {Title}", podcast.Title);
|
|
}
|
|
|
|
processedCount++;
|
|
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);
|
|
_logger.LogInformation("Podcast feed update task completed");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
|
{
|
|
var config = Plugin.Instance?.Configuration;
|
|
var intervalHours = config?.UpdateIntervalHours ?? 6;
|
|
|
|
return new[]
|
|
{
|
|
new TaskTriggerInfo
|
|
{
|
|
Type = TaskTriggerInfo.TriggerInterval,
|
|
IntervalTicks = TimeSpan.FromHours(intervalHours).Ticks
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|