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.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace TransmissionRssManager.Core
|
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 class RssFeedItem
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; } = string.Empty;
|
||||||
public string Title { get; set; }
|
public string Title { get; set; } = string.Empty;
|
||||||
public string Link { get; set; }
|
public string Link { get; set; } = string.Empty;
|
||||||
public string Description { get; set; }
|
public string Description { get; set; } = string.Empty;
|
||||||
public DateTime PublishDate { get; set; }
|
public DateTime PublishDate { get; set; }
|
||||||
public string TorrentUrl { get; set; }
|
public string TorrentUrl { get; set; } = string.Empty;
|
||||||
public bool IsDownloaded { get; set; }
|
public bool IsDownloaded { get; set; }
|
||||||
public bool IsMatched { 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 class TorrentInfo
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; } = string.Empty;
|
||||||
public string Status { get; set; }
|
public string Status { get; set; } = string.Empty;
|
||||||
public double PercentDone { get; set; }
|
public double PercentDone { get; set; }
|
||||||
public long TotalSize { 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 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 class RssFeed
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; } = string.Empty;
|
||||||
public string Url { get; set; }
|
public string Url { get; set; } = string.Empty;
|
||||||
public string Name { get; set; }
|
public string Name { get; set; } = string.Empty;
|
||||||
public List<string> Rules { get; set; } = new List<string>();
|
public List<string> Rules { get; set; } = new List<string>();
|
||||||
|
public List<RssFeedRule> AdvancedRules { get; set; } = new List<RssFeedRule>();
|
||||||
public bool AutoDownload { get; set; }
|
public bool AutoDownload { get; set; }
|
||||||
public DateTime LastChecked { 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 class AppConfig
|
||||||
{
|
{
|
||||||
public TransmissionConfig Transmission { get; set; } = new TransmissionConfig();
|
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 List<RssFeed> Feeds { get; set; } = new List<RssFeed>();
|
||||||
public bool AutoDownloadEnabled { get; set; }
|
public bool AutoDownloadEnabled { get; set; }
|
||||||
public int CheckIntervalMinutes { get; set; } = 30;
|
public int CheckIntervalMinutes { get; set; } = 30;
|
||||||
public string DownloadDirectory { get; set; }
|
public string DownloadDirectory { get; set; } = string.Empty;
|
||||||
public string MediaLibraryPath { get; set; }
|
public string MediaLibraryPath { get; set; } = string.Empty;
|
||||||
public PostProcessingConfig PostProcessing { get; set; } = new PostProcessingConfig();
|
public PostProcessingConfig PostProcessing { get; set; } = new PostProcessingConfig();
|
||||||
|
public bool EnableDetailedLogging { get; set; } = false;
|
||||||
|
public UserPreferences UserPreferences { get; set; } = new UserPreferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TransmissionConfig
|
public class TransmissionConfig
|
||||||
{
|
{
|
||||||
public string Host { get; set; } = "localhost";
|
public string Host { get; set; } = "localhost";
|
||||||
public int Port { get; set; } = 9091;
|
public int Port { get; set; } = 9091;
|
||||||
public string Username { get; set; }
|
public string Username { get; set; } = string.Empty;
|
||||||
public string Password { get; set; }
|
public string Password { get; set; } = string.Empty;
|
||||||
public bool UseHttps { get; set; } = false;
|
public bool UseHttps { get; set; } = false;
|
||||||
public string Url => $"{(UseHttps ? "https" : "http")}://{Host}:{Port}/transmission/rpc";
|
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 class PostProcessingConfig
|
||||||
{
|
{
|
||||||
public bool Enabled { get; set; } = false;
|
public bool Enabled { get; set; } = false;
|
||||||
@ -67,6 +130,30 @@ namespace TransmissionRssManager.Core
|
|||||||
public bool OrganizeMedia { get; set; } = true;
|
public bool OrganizeMedia { get; set; } = true;
|
||||||
public int MinimumSeedRatio { get; set; } = 1;
|
public int MinimumSeedRatio { get; set; } = 1;
|
||||||
public List<string> MediaExtensions { get; set; } = new List<string> { ".mp4", ".mkv", ".avi" };
|
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
|
public interface IConfigService
|
||||||
@ -93,6 +180,7 @@ namespace TransmissionRssManager.Core
|
|||||||
Task RemoveFeedAsync(string feedId);
|
Task RemoveFeedAsync(string feedId);
|
||||||
Task UpdateFeedAsync(RssFeed feed);
|
Task UpdateFeedAsync(RssFeed feed);
|
||||||
Task RefreshFeedsAsync(CancellationToken cancellationToken);
|
Task RefreshFeedsAsync(CancellationToken cancellationToken);
|
||||||
|
Task RefreshFeedAsync(string feedId, CancellationToken cancellationToken);
|
||||||
Task MarkItemAsDownloadedAsync(string itemId);
|
Task MarkItemAsDownloadedAsync(string itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.ServiceModel.Syndication;
|
using System.ServiceModel.Syndication;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using TransmissionRssManager.Core;
|
using TransmissionRssManager.Core;
|
||||||
|
|
||||||
namespace TransmissionRssManager.Services
|
namespace TransmissionRssManager.Services
|
||||||
@ -20,9 +20,9 @@ namespace TransmissionRssManager.Services
|
|||||||
private readonly ILogger<RssFeedManager> _logger;
|
private readonly ILogger<RssFeedManager> _logger;
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly ITransmissionClient _transmissionClient;
|
private readonly ITransmissionClient _transmissionClient;
|
||||||
|
private List<RssFeed> _feeds = new List<RssFeed>();
|
||||||
|
private List<RssFeedItem> _feedItems = new List<RssFeedItem>();
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly string _dataPath;
|
|
||||||
private List<RssFeedItem> _items = new List<RssFeedItem>();
|
|
||||||
|
|
||||||
public RssFeedManager(
|
public RssFeedManager(
|
||||||
ILogger<RssFeedManager> logger,
|
ILogger<RssFeedManager> logger,
|
||||||
@ -34,276 +34,294 @@ namespace TransmissionRssManager.Services
|
|||||||
_transmissionClient = transmissionClient;
|
_transmissionClient = transmissionClient;
|
||||||
_httpClient = new HttpClient();
|
_httpClient = new HttpClient();
|
||||||
|
|
||||||
// Create data directory
|
// Load feeds from config
|
||||||
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()
|
|
||||||
{
|
|
||||||
var config = _configService.GetConfiguration();
|
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)
|
public async Task AddFeedAsync(RssFeed feed)
|
||||||
{
|
{
|
||||||
feed.Id = Guid.NewGuid().ToString();
|
feed.Id = Guid.NewGuid().ToString();
|
||||||
feed.LastChecked = DateTime.MinValue;
|
_feeds.Add(feed);
|
||||||
|
await SaveFeedsToConfigAsync();
|
||||||
var config = _configService.GetConfiguration();
|
|
||||||
config.Feeds.Add(feed);
|
|
||||||
await _configService.SaveConfigurationAsync(config);
|
|
||||||
|
|
||||||
// Initial fetch of feed items
|
|
||||||
await FetchFeedAsync(feed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveFeedAsync(string feedId)
|
public async Task RemoveFeedAsync(string feedId)
|
||||||
{
|
{
|
||||||
var config = _configService.GetConfiguration();
|
_feeds.RemoveAll(f => f.Id == feedId);
|
||||||
var feed = config.Feeds.FirstOrDefault(f => f.Id == feedId);
|
_feedItems.RemoveAll(i => i.FeedId == feedId);
|
||||||
|
await SaveFeedsToConfigAsync();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateFeedAsync(RssFeed feed)
|
public async Task UpdateFeedAsync(RssFeed feed)
|
||||||
{
|
{
|
||||||
var config = _configService.GetConfiguration();
|
var existingFeed = _feeds.FirstOrDefault(f => f.Id == feed.Id);
|
||||||
var index = config.Feeds.FindIndex(f => f.Id == feed.Id);
|
if (existingFeed != null)
|
||||||
|
|
||||||
if (index != -1)
|
|
||||||
{
|
{
|
||||||
config.Feeds[index] = feed;
|
int index = _feeds.IndexOf(existingFeed);
|
||||||
await _configService.SaveConfigurationAsync(config);
|
_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)
|
public async Task RefreshFeedsAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting RSS feed refresh");
|
_logger.LogInformation("Refreshing RSS feeds");
|
||||||
var config = _configService.GetConfiguration();
|
|
||||||
|
|
||||||
foreach (var feed in config.Feeds)
|
foreach (var feed in _feeds.Where(f => f.Enabled))
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
break;
|
||||||
_logger.LogInformation("RSS refresh cancelled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await FetchFeedAsync(feed);
|
await RefreshFeedAsync(feed.Id, cancellationToken);
|
||||||
|
|
||||||
// Update last checked time
|
|
||||||
feed.LastChecked = DateTime.Now;
|
|
||||||
await _configService.SaveConfigurationAsync(config);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, $"Error refreshing feed: {feed.Name}");
|
_logger.LogError(ex, $"Error refreshing feed {feed.Name}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for matches and auto-download if enabled
|
public async Task RefreshFeedAsync(string feedId, CancellationToken cancellationToken)
|
||||||
await ProcessMatchesAsync();
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
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)
|
if (item != null)
|
||||||
{
|
{
|
||||||
item.IsDownloaded = true;
|
item.IsDownloaded = true;
|
||||||
await SaveItemsAsync();
|
item.DownloadDate = DateTime.UtcNow;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -311,17 +329,14 @@ namespace TransmissionRssManager.Services
|
|||||||
public class RssFeedBackgroundService : BackgroundService
|
public class RssFeedBackgroundService : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly ILogger<RssFeedBackgroundService> _logger;
|
private readonly ILogger<RssFeedBackgroundService> _logger;
|
||||||
private readonly IRssFeedManager _rssFeedManager;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IConfigService _configService;
|
|
||||||
|
|
||||||
public RssFeedBackgroundService(
|
public RssFeedBackgroundService(
|
||||||
ILogger<RssFeedBackgroundService> logger,
|
ILogger<RssFeedBackgroundService> logger,
|
||||||
IRssFeedManager rssFeedManager,
|
IServiceProvider serviceProvider)
|
||||||
IConfigService configService)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_rssFeedManager = rssFeedManager;
|
_serviceProvider = serviceProvider;
|
||||||
_configService = configService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
@ -332,19 +347,26 @@ namespace TransmissionRssManager.Services
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _rssFeedManager.RefreshFeedsAsync(stoppingToken);
|
using (var scope = _serviceProvider.CreateScope())
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error refreshing RSS feeds");
|
var rssFeedManager = scope.ServiceProvider.GetRequiredService<IRssFeedManager>();
|
||||||
}
|
var configService = scope.ServiceProvider.GetRequiredService<IConfigService>();
|
||||||
|
|
||||||
var config = _configService.GetConfiguration();
|
await rssFeedManager.RefreshFeedsAsync(stoppingToken);
|
||||||
|
|
||||||
|
var config = configService.GetConfiguration();
|
||||||
var interval = TimeSpan.FromMinutes(config.CheckIntervalMinutes);
|
var interval = TimeSpan.FromMinutes(config.CheckIntervalMinutes);
|
||||||
|
|
||||||
_logger.LogInformation($"Next refresh in {interval.TotalMinutes} minutes");
|
_logger.LogInformation($"Next refresh in {interval.TotalMinutes} minutes");
|
||||||
await Task.Delay(interval, stoppingToken);
|
await Task.Delay(interval, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error refreshing RSS feeds");
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user