Fix RssFeedManager to remove database dependencies and add string initializers to Core models
This commit is contained in:
parent
6dff6103d9
commit
619a861546
@ -2,64 +2,127 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TransmissionRssManager.Core
|
||||
{
|
||||
public class LogEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string Level { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public string Context { get; set; } = string.Empty;
|
||||
public string Properties { get; set; } = string.Empty;
|
||||
}
|
||||
public class RssFeedItem
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Link { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Link { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public DateTime PublishDate { get; set; }
|
||||
public string TorrentUrl { get; set; }
|
||||
public string TorrentUrl { get; set; } = string.Empty;
|
||||
public bool IsDownloaded { get; set; }
|
||||
public bool IsMatched { get; set; }
|
||||
public string MatchedRule { get; set; }
|
||||
public string MatchedRule { get; set; } = string.Empty;
|
||||
public string FeedId { get; set; } = string.Empty;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public long Size { get; set; }
|
||||
public string Author { get; set; } = string.Empty;
|
||||
public List<string> Categories { get; set; } = new List<string>();
|
||||
public Dictionary<string, string> AdditionalMetadata { get; set; } = new Dictionary<string, string>();
|
||||
public DateTime? DownloadDate { get; set; }
|
||||
public int? TorrentId { get; set; }
|
||||
public string RejectionReason { get; set; } = string.Empty;
|
||||
public bool IsRejected => !string.IsNullOrEmpty(RejectionReason);
|
||||
}
|
||||
|
||||
public class TorrentInfo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public double PercentDone { get; set; }
|
||||
public long TotalSize { get; set; }
|
||||
public string DownloadDir { get; set; }
|
||||
public string DownloadDir { get; set; } = string.Empty;
|
||||
public bool IsFinished => PercentDone >= 1.0;
|
||||
public DateTime? AddedDate { get; set; }
|
||||
public DateTime? CompletedDate { get; set; }
|
||||
public long DownloadedEver { get; set; }
|
||||
public long UploadedEver { get; set; }
|
||||
public int UploadRatio { get; set; }
|
||||
public string ErrorString { get; set; } = string.Empty;
|
||||
public bool IsError => !string.IsNullOrEmpty(ErrorString);
|
||||
public int Priority { get; set; }
|
||||
public string HashString { get; set; } = string.Empty;
|
||||
public int PeersConnected { get; set; }
|
||||
public double DownloadSpeed { get; set; }
|
||||
public double UploadSpeed { get; set; }
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public bool HasMetadata { get; set; }
|
||||
public string TransmissionInstance { get; set; } = "default";
|
||||
public string SourceFeedId { get; set; } = string.Empty;
|
||||
public bool IsPostProcessed { get; set; }
|
||||
}
|
||||
|
||||
public class RssFeed
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<string> Rules { get; set; } = new List<string>();
|
||||
public List<RssFeedRule> AdvancedRules { get; set; } = new List<RssFeedRule>();
|
||||
public bool AutoDownload { get; set; }
|
||||
public DateTime LastChecked { get; set; }
|
||||
public string TransmissionInstanceId { get; set; } = "default";
|
||||
public string Schedule { get; set; } = "*/30 * * * *"; // Default is every 30 minutes (cron expression)
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int MaxHistoryItems { get; set; } = 100;
|
||||
public string DefaultCategory { get; set; } = string.Empty;
|
||||
public int ErrorCount { get; set; } = 0;
|
||||
public DateTime? LastError { get; set; }
|
||||
public string LastErrorMessage { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AppConfig
|
||||
{
|
||||
public TransmissionConfig Transmission { get; set; } = new TransmissionConfig();
|
||||
public Dictionary<string, TransmissionConfig> TransmissionInstances { get; set; } = new Dictionary<string, TransmissionConfig>();
|
||||
public List<RssFeed> Feeds { get; set; } = new List<RssFeed>();
|
||||
public bool AutoDownloadEnabled { get; set; }
|
||||
public int CheckIntervalMinutes { get; set; } = 30;
|
||||
public string DownloadDirectory { get; set; }
|
||||
public string MediaLibraryPath { get; set; }
|
||||
public string DownloadDirectory { get; set; } = string.Empty;
|
||||
public string MediaLibraryPath { get; set; } = string.Empty;
|
||||
public PostProcessingConfig PostProcessing { get; set; } = new PostProcessingConfig();
|
||||
public bool EnableDetailedLogging { get; set; } = false;
|
||||
public UserPreferences UserPreferences { get; set; } = new UserPreferences();
|
||||
}
|
||||
|
||||
public class TransmissionConfig
|
||||
{
|
||||
public string Host { get; set; } = "localhost";
|
||||
public int Port { get; set; } = 9091;
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public bool UseHttps { get; set; } = false;
|
||||
public string Url => $"{(UseHttps ? "https" : "http")}://{Host}:{Port}/transmission/rpc";
|
||||
}
|
||||
|
||||
public class RssFeedRule
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Pattern { get; set; } = string.Empty;
|
||||
public bool IsRegex { get; set; } = false;
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public bool IsCaseSensitive { get; set; } = false;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public int Priority { get; set; } = 0;
|
||||
public string Action { get; set; } = "download"; // download, notify, ignore
|
||||
public string DestinationFolder { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PostProcessingConfig
|
||||
{
|
||||
public bool Enabled { get; set; } = false;
|
||||
@ -67,6 +130,30 @@ namespace TransmissionRssManager.Core
|
||||
public bool OrganizeMedia { get; set; } = true;
|
||||
public int MinimumSeedRatio { get; set; } = 1;
|
||||
public List<string> MediaExtensions { get; set; } = new List<string> { ".mp4", ".mkv", ".avi" };
|
||||
public bool AutoOrganizeByMediaType { get; set; } = true;
|
||||
public bool RenameFiles { get; set; } = false;
|
||||
public bool CompressCompletedFiles { get; set; } = false;
|
||||
public int DeleteCompletedAfterDays { get; set; } = 0; // 0 = never delete
|
||||
}
|
||||
|
||||
public class UserPreferences
|
||||
{
|
||||
public bool EnableDarkMode { get; set; } = false;
|
||||
public bool AutoRefreshUIEnabled { get; set; } = true;
|
||||
public int AutoRefreshIntervalSeconds { get; set; } = 30;
|
||||
public bool NotificationsEnabled { get; set; } = true;
|
||||
public List<string> NotificationEvents { get; set; } = new List<string>
|
||||
{
|
||||
"torrent-added",
|
||||
"torrent-completed",
|
||||
"torrent-error"
|
||||
};
|
||||
public string DefaultView { get; set; } = "dashboard";
|
||||
public bool ConfirmBeforeDelete { get; set; } = true;
|
||||
public int MaxItemsPerPage { get; set; } = 25;
|
||||
public string DateTimeFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
|
||||
public bool ShowCompletedTorrents { get; set; } = true;
|
||||
public int KeepHistoryDays { get; set; } = 30;
|
||||
}
|
||||
|
||||
public interface IConfigService
|
||||
@ -93,6 +180,7 @@ namespace TransmissionRssManager.Core
|
||||
Task RemoveFeedAsync(string feedId);
|
||||
Task UpdateFeedAsync(RssFeed feed);
|
||||
Task RefreshFeedsAsync(CancellationToken cancellationToken);
|
||||
Task RefreshFeedAsync(string feedId, CancellationToken cancellationToken);
|
||||
Task MarkItemAsDownloadedAsync(string itemId);
|
||||
}
|
||||
|
||||
|
@ -4,13 +4,13 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.ServiceModel.Syndication;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TransmissionRssManager.Core;
|
||||
|
||||
namespace TransmissionRssManager.Services
|
||||
@ -20,9 +20,9 @@ namespace TransmissionRssManager.Services
|
||||
private readonly ILogger<RssFeedManager> _logger;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ITransmissionClient _transmissionClient;
|
||||
private List<RssFeed> _feeds = new List<RssFeed>();
|
||||
private List<RssFeedItem> _feedItems = new List<RssFeedItem>();
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _dataPath;
|
||||
private List<RssFeedItem> _items = new List<RssFeedItem>();
|
||||
|
||||
public RssFeedManager(
|
||||
ILogger<RssFeedManager> logger,
|
||||
@ -34,276 +34,294 @@ namespace TransmissionRssManager.Services
|
||||
_transmissionClient = transmissionClient;
|
||||
_httpClient = new HttpClient();
|
||||
|
||||
// Create data directory
|
||||
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
string dataDir = Path.Combine(homeDir, ".local", "share", "transmission-rss-manager");
|
||||
|
||||
if (!Directory.Exists(dataDir))
|
||||
{
|
||||
Directory.CreateDirectory(dataDir);
|
||||
}
|
||||
|
||||
_dataPath = Path.Combine(dataDir, "rss-items.json");
|
||||
LoadItems();
|
||||
}
|
||||
|
||||
public Task<List<RssFeedItem>> GetAllItemsAsync()
|
||||
{
|
||||
return Task.FromResult(_items.OrderByDescending(i => i.PublishDate).ToList());
|
||||
}
|
||||
|
||||
public Task<List<RssFeedItem>> GetMatchedItemsAsync()
|
||||
{
|
||||
return Task.FromResult(_items.Where(i => i.IsMatched).OrderByDescending(i => i.PublishDate).ToList());
|
||||
}
|
||||
|
||||
public Task<List<RssFeed>> GetFeedsAsync()
|
||||
{
|
||||
// Load feeds from config
|
||||
var config = _configService.GetConfiguration();
|
||||
return Task.FromResult(config.Feeds);
|
||||
_feeds = config.Feeds;
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<RssFeedItem>> GetAllItemsAsync()
|
||||
{
|
||||
return _feedItems;
|
||||
}
|
||||
|
||||
public async Task<List<RssFeedItem>> GetMatchedItemsAsync()
|
||||
{
|
||||
return _feedItems.Where(item => item.IsMatched).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<RssFeed>> GetFeedsAsync()
|
||||
{
|
||||
return _feeds;
|
||||
}
|
||||
|
||||
public async Task AddFeedAsync(RssFeed feed)
|
||||
{
|
||||
feed.Id = Guid.NewGuid().ToString();
|
||||
feed.LastChecked = DateTime.MinValue;
|
||||
|
||||
var config = _configService.GetConfiguration();
|
||||
config.Feeds.Add(feed);
|
||||
await _configService.SaveConfigurationAsync(config);
|
||||
|
||||
// Initial fetch of feed items
|
||||
await FetchFeedAsync(feed);
|
||||
_feeds.Add(feed);
|
||||
await SaveFeedsToConfigAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task RemoveFeedAsync(string feedId)
|
||||
{
|
||||
var config = _configService.GetConfiguration();
|
||||
var feed = config.Feeds.FirstOrDefault(f => f.Id == feedId);
|
||||
|
||||
if (feed != null)
|
||||
{
|
||||
config.Feeds.Remove(feed);
|
||||
await _configService.SaveConfigurationAsync(config);
|
||||
|
||||
// Remove items from this feed
|
||||
_items.RemoveAll(i => i.Id.StartsWith(feedId));
|
||||
await SaveItemsAsync();
|
||||
}
|
||||
_feeds.RemoveAll(f => f.Id == feedId);
|
||||
_feedItems.RemoveAll(i => i.FeedId == feedId);
|
||||
await SaveFeedsToConfigAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateFeedAsync(RssFeed feed)
|
||||
{
|
||||
var config = _configService.GetConfiguration();
|
||||
var index = config.Feeds.FindIndex(f => f.Id == feed.Id);
|
||||
|
||||
if (index != -1)
|
||||
var existingFeed = _feeds.FirstOrDefault(f => f.Id == feed.Id);
|
||||
if (existingFeed != null)
|
||||
{
|
||||
config.Feeds[index] = feed;
|
||||
await _configService.SaveConfigurationAsync(config);
|
||||
int index = _feeds.IndexOf(existingFeed);
|
||||
_feeds[index] = feed;
|
||||
await SaveFeedsToConfigAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task SaveFeedsToConfigAsync()
|
||||
{
|
||||
var config = _configService.GetConfiguration();
|
||||
config.Feeds = _feeds;
|
||||
await _configService.SaveConfigurationAsync(config);
|
||||
}
|
||||
|
||||
public async Task RefreshFeedsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting RSS feed refresh");
|
||||
var config = _configService.GetConfiguration();
|
||||
_logger.LogInformation("Refreshing RSS feeds");
|
||||
|
||||
foreach (var feed in config.Feeds)
|
||||
foreach (var feed in _feeds.Where(f => f.Enabled))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("RSS refresh cancelled");
|
||||
await RefreshFeedAsync(feed.Id, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error refreshing feed {feed.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshFeedAsync(string feedId, CancellationToken cancellationToken)
|
||||
{
|
||||
var feed = _feeds.FirstOrDefault(f => f.Id == feedId);
|
||||
if (feed == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"Refreshing feed: {feed.Name}");
|
||||
|
||||
var feedItems = await FetchFeedItemsAsync(feed.Url);
|
||||
foreach (var item in feedItems)
|
||||
{
|
||||
// Add only if we don't already have this item
|
||||
if (!_feedItems.Any(i => i.Link == item.Link && i.FeedId == feed.Id))
|
||||
{
|
||||
item.FeedId = feed.Id;
|
||||
_feedItems.Add(item);
|
||||
|
||||
// Apply rules
|
||||
ApplyRulesToItem(feed, item);
|
||||
|
||||
// Download if matched and auto-download is enabled
|
||||
if (item.IsMatched && feed.AutoDownload)
|
||||
{
|
||||
await DownloadMatchedItemAsync(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update last checked time
|
||||
feed.LastChecked = DateTime.UtcNow;
|
||||
feed.ErrorCount = 0;
|
||||
feed.LastErrorMessage = string.Empty;
|
||||
|
||||
// Cleanup old items
|
||||
CleanupOldItems(feed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error refreshing feed {feed.Name}: {ex.Message}");
|
||||
feed.ErrorCount++;
|
||||
feed.LastError = DateTime.UtcNow;
|
||||
feed.LastErrorMessage = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<RssFeedItem>> FetchFeedItemsAsync(string url)
|
||||
{
|
||||
var feedItems = new List<RssFeedItem>();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetStringAsync(url);
|
||||
using (var reader = XmlReader.Create(new StringReader(response)))
|
||||
{
|
||||
var feed = SyndicationFeed.Load(reader);
|
||||
|
||||
foreach (var item in feed.Items)
|
||||
{
|
||||
var feedItem = new RssFeedItem
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Title = item.Title?.Text ?? "",
|
||||
Description = item.Summary?.Text ?? "",
|
||||
Link = item.Links.FirstOrDefault()?.Uri.ToString() ?? "",
|
||||
PublishDate = item.PublishDate.UtcDateTime,
|
||||
Author = item.Authors.FirstOrDefault()?.Name ?? ""
|
||||
};
|
||||
|
||||
// Find torrent link
|
||||
foreach (var link in item.Links)
|
||||
{
|
||||
if (link.MediaType?.Contains("torrent") == true ||
|
||||
link.Uri.ToString().EndsWith(".torrent") ||
|
||||
link.Uri.ToString().StartsWith("magnet:"))
|
||||
{
|
||||
feedItem.TorrentUrl = link.Uri.ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no torrent link found, use the main link
|
||||
if (string.IsNullOrEmpty(feedItem.TorrentUrl))
|
||||
{
|
||||
feedItem.TorrentUrl = feedItem.Link;
|
||||
}
|
||||
|
||||
feedItems.Add(feedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error fetching feed: {url}");
|
||||
throw;
|
||||
}
|
||||
|
||||
return feedItems;
|
||||
}
|
||||
|
||||
private void ApplyRulesToItem(RssFeed feed, RssFeedItem item)
|
||||
{
|
||||
item.IsMatched = false;
|
||||
item.MatchedRule = string.Empty;
|
||||
|
||||
// Apply simple string rules
|
||||
foreach (var rulePattern in feed.Rules)
|
||||
{
|
||||
if (item.Title.Contains(rulePattern, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.IsMatched = true;
|
||||
item.MatchedRule = rulePattern;
|
||||
item.Category = feed.DefaultCategory;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply advanced rules
|
||||
foreach (var rule in feed.AdvancedRules.Where(r => r.IsEnabled).OrderByDescending(r => r.Priority))
|
||||
{
|
||||
bool isMatch = false;
|
||||
|
||||
if (rule.IsRegex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var regex = new Regex(rule.Pattern,
|
||||
rule.IsCaseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase);
|
||||
isMatch = regex.IsMatch(item.Title);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Invalid regex pattern: {rule.Pattern}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var comparison = rule.IsCaseSensitive ?
|
||||
StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
|
||||
isMatch = item.Title.Contains(rule.Pattern, comparison);
|
||||
}
|
||||
|
||||
if (isMatch)
|
||||
{
|
||||
item.IsMatched = true;
|
||||
item.MatchedRule = rule.Name;
|
||||
item.Category = rule.Category;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadMatchedItemAsync(RssFeedItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = _configService.GetConfiguration();
|
||||
var downloadDir = config.DownloadDirectory;
|
||||
|
||||
if (string.IsNullOrEmpty(downloadDir))
|
||||
{
|
||||
_logger.LogWarning("Download directory not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await FetchFeedAsync(feed);
|
||||
|
||||
// Update last checked time
|
||||
feed.LastChecked = DateTime.Now;
|
||||
await _configService.SaveConfigurationAsync(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error refreshing feed: {feed.Name}");
|
||||
}
|
||||
_logger.LogInformation($"Downloading matched item: {item.Title}");
|
||||
|
||||
// Add torrent to Transmission
|
||||
int torrentId = await _transmissionClient.AddTorrentAsync(item.TorrentUrl, downloadDir);
|
||||
|
||||
// Update feed item
|
||||
item.IsDownloaded = true;
|
||||
item.DownloadDate = DateTime.UtcNow;
|
||||
item.TorrentId = torrentId;
|
||||
|
||||
_logger.LogInformation($"Added torrent: {item.Title} (ID: {torrentId})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error downloading item: {item.Title}");
|
||||
item.RejectionReason = ex.Message;
|
||||
}
|
||||
|
||||
// Check for matches and auto-download if enabled
|
||||
await ProcessMatchesAsync();
|
||||
}
|
||||
|
||||
|
||||
private void CleanupOldItems(RssFeed feed)
|
||||
{
|
||||
if (feed.MaxHistoryItems <= 0)
|
||||
return;
|
||||
|
||||
var feedItems = _feedItems.Where(i => i.FeedId == feed.Id).ToList();
|
||||
|
||||
if (feedItems.Count > feed.MaxHistoryItems)
|
||||
{
|
||||
// Keep all downloaded items
|
||||
var downloadedItems = feedItems.Where(i => i.IsDownloaded).ToList();
|
||||
|
||||
// Keep most recent non-downloaded items up to the limit
|
||||
var nonDownloadedItems = feedItems.Where(i => !i.IsDownloaded)
|
||||
.OrderByDescending(i => i.PublishDate)
|
||||
.Take(feed.MaxHistoryItems - downloadedItems.Count)
|
||||
.ToList();
|
||||
|
||||
// Set new list
|
||||
var itemsToKeep = downloadedItems.Union(nonDownloadedItems).ToList();
|
||||
_feedItems.RemoveAll(i => i.FeedId == feed.Id && !itemsToKeep.Contains(i));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MarkItemAsDownloadedAsync(string itemId)
|
||||
{
|
||||
var item = _items.FirstOrDefault(i => i.Id == itemId);
|
||||
|
||||
var item = _feedItems.FirstOrDefault(i => i.Id == itemId);
|
||||
if (item != null)
|
||||
{
|
||||
item.IsDownloaded = true;
|
||||
await SaveItemsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchFeedAsync(RssFeed feed)
|
||||
{
|
||||
_logger.LogInformation($"Fetching feed: {feed.Name}");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetStringAsync(feed.Url);
|
||||
using var stringReader = new StringReader(response);
|
||||
using var xmlReader = XmlReader.Create(stringReader);
|
||||
|
||||
var syndicationFeed = SyndicationFeed.Load(xmlReader);
|
||||
|
||||
foreach (var item in syndicationFeed.Items)
|
||||
{
|
||||
var link = item.Links.FirstOrDefault()?.Uri.ToString() ?? "";
|
||||
var torrentUrl = ExtractTorrentUrl(link, item.Title.Text);
|
||||
|
||||
// Create a unique ID for this item
|
||||
var itemId = $"{feed.Id}:{item.Id ?? Guid.NewGuid().ToString()}";
|
||||
|
||||
// Check if we already have this item
|
||||
if (_items.Any(i => i.Id == itemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var feedItem = new RssFeedItem
|
||||
{
|
||||
Id = itemId,
|
||||
Title = item.Title.Text,
|
||||
Link = link,
|
||||
Description = item.Summary?.Text ?? "",
|
||||
PublishDate = item.PublishDate.DateTime,
|
||||
TorrentUrl = torrentUrl,
|
||||
IsDownloaded = false
|
||||
};
|
||||
|
||||
// Check if this item matches any rules
|
||||
CheckForMatches(feedItem, feed.Rules);
|
||||
|
||||
_items.Add(feedItem);
|
||||
}
|
||||
|
||||
await SaveItemsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error fetching feed: {feed.Name}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private string ExtractTorrentUrl(string link, string title)
|
||||
{
|
||||
// Try to find a .torrent link
|
||||
if (link.EndsWith(".torrent", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return link;
|
||||
}
|
||||
|
||||
// If it's a magnet link, return it
|
||||
if (link.StartsWith("magnet:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return link;
|
||||
}
|
||||
|
||||
// Return the link as is, we'll try to find the torrent on the page
|
||||
return link;
|
||||
}
|
||||
|
||||
private void CheckForMatches(RssFeedItem item, List<string> rules)
|
||||
{
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Regex.IsMatch(item.Title, rule, RegexOptions.IgnoreCase))
|
||||
{
|
||||
item.IsMatched = true;
|
||||
item.MatchedRule = rule;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Invalid regex rule: {rule}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessMatchesAsync()
|
||||
{
|
||||
var config = _configService.GetConfiguration();
|
||||
|
||||
if (!config.AutoDownloadEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var matchedItems = _items.Where(i => i.IsMatched && !i.IsDownloaded).ToList();
|
||||
|
||||
foreach (var item in matchedItems)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"Auto-downloading: {item.Title}");
|
||||
|
||||
var torrentId = await _transmissionClient.AddTorrentAsync(
|
||||
item.TorrentUrl,
|
||||
config.DownloadDirectory);
|
||||
|
||||
item.IsDownloaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error downloading torrent: {item.Title}");
|
||||
}
|
||||
}
|
||||
|
||||
await SaveItemsAsync();
|
||||
}
|
||||
|
||||
private void LoadItems()
|
||||
{
|
||||
if (!File.Exists(_dataPath))
|
||||
{
|
||||
_items = new List<RssFeedItem>();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_dataPath);
|
||||
var items = JsonSerializer.Deserialize<List<RssFeedItem>>(json);
|
||||
_items = items ?? new List<RssFeedItem>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading RSS items");
|
||||
_items = new List<RssFeedItem>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveItemsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(_items, options);
|
||||
await File.WriteAllTextAsync(_dataPath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving RSS items");
|
||||
item.DownloadDate = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -311,17 +329,14 @@ namespace TransmissionRssManager.Services
|
||||
public class RssFeedBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<RssFeedBackgroundService> _logger;
|
||||
private readonly IRssFeedManager _rssFeedManager;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public RssFeedBackgroundService(
|
||||
ILogger<RssFeedBackgroundService> logger,
|
||||
IRssFeedManager rssFeedManager,
|
||||
IConfigService configService)
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_rssFeedManager = rssFeedManager;
|
||||
_configService = configService;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
@ -332,18 +347,25 @@ namespace TransmissionRssManager.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
await _rssFeedManager.RefreshFeedsAsync(stoppingToken);
|
||||
using (var scope = _serviceProvider.CreateScope())
|
||||
{
|
||||
var rssFeedManager = scope.ServiceProvider.GetRequiredService<IRssFeedManager>();
|
||||
var configService = scope.ServiceProvider.GetRequiredService<IConfigService>();
|
||||
|
||||
await rssFeedManager.RefreshFeedsAsync(stoppingToken);
|
||||
|
||||
var config = configService.GetConfiguration();
|
||||
var interval = TimeSpan.FromMinutes(config.CheckIntervalMinutes);
|
||||
|
||||
_logger.LogInformation($"Next refresh in {interval.TotalMinutes} minutes");
|
||||
await Task.Delay(interval, stoppingToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing RSS feeds");
|
||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||
}
|
||||
|
||||
var config = _configService.GetConfiguration();
|
||||
var interval = TimeSpan.FromMinutes(config.CheckIntervalMinutes);
|
||||
|
||||
_logger.LogInformation($"Next refresh in {interval.TotalMinutes} minutes");
|
||||
await Task.Delay(interval, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user