Initial commit of Transmission RSS Manager with fixed remote connection and post-processing features

This commit is contained in:
2025-03-12 19:13:59 +00:00
commit f804ca51d3
21 changed files with 5780 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using TransmissionRssManager.Core;
namespace TransmissionRssManager.Services
{
public class ConfigService : IConfigService
{
private readonly ILogger<ConfigService> _logger;
private readonly string _configPath;
private AppConfig _cachedConfig;
public ConfigService(ILogger<ConfigService> logger)
{
_logger = logger;
// Get config directory
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string configDir = Path.Combine(homeDir, ".config", "transmission-rss-manager");
// Ensure directory exists
if (!Directory.Exists(configDir))
{
Directory.CreateDirectory(configDir);
}
_configPath = Path.Combine(configDir, "config.json");
_cachedConfig = LoadConfiguration();
}
public AppConfig GetConfiguration()
{
return _cachedConfig;
}
public async Task SaveConfigurationAsync(AppConfig config)
{
_cachedConfig = config;
var options = new JsonSerializerOptions
{
WriteIndented = true
};
string json = JsonSerializer.Serialize(config, options);
await File.WriteAllTextAsync(_configPath, json);
_logger.LogInformation("Configuration saved successfully");
}
private AppConfig LoadConfiguration()
{
if (!File.Exists(_configPath))
{
_logger.LogInformation("No configuration file found, creating default");
var defaultConfig = CreateDefaultConfig();
SaveConfigurationAsync(defaultConfig).Wait();
return defaultConfig;
}
try
{
string json = File.ReadAllText(_configPath);
var config = JsonSerializer.Deserialize<AppConfig>(json);
if (config == null)
{
_logger.LogWarning("Failed to deserialize config, creating default");
return CreateDefaultConfig();
}
return config;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading configuration");
return CreateDefaultConfig();
}
}
private AppConfig CreateDefaultConfig()
{
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return new AppConfig
{
Transmission = new TransmissionConfig
{
Host = "localhost",
Port = 9091,
Username = "",
Password = ""
},
AutoDownloadEnabled = false,
CheckIntervalMinutes = 30,
DownloadDirectory = Path.Combine(homeDir, "Downloads"),
MediaLibraryPath = Path.Combine(homeDir, "Media"),
PostProcessing = new PostProcessingConfig
{
Enabled = false,
ExtractArchives = true,
OrganizeMedia = true,
MinimumSeedRatio = 1,
MediaExtensions = new System.Collections.Generic.List<string> { ".mp4", ".mkv", ".avi" }
}
};
}
}
}
+272
View File
@@ -0,0 +1,272 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using TransmissionRssManager.Core;
namespace TransmissionRssManager.Services
{
public class PostProcessor : IPostProcessor
{
private readonly ILogger<PostProcessor> _logger;
private readonly IConfigService _configService;
private readonly ITransmissionClient _transmissionClient;
public PostProcessor(
ILogger<PostProcessor> logger,
IConfigService configService,
ITransmissionClient transmissionClient)
{
_logger = logger;
_configService = configService;
_transmissionClient = transmissionClient;
}
public async Task ProcessCompletedDownloadsAsync(CancellationToken cancellationToken)
{
var config = _configService.GetConfiguration();
if (!config.PostProcessing.Enabled)
{
return;
}
_logger.LogInformation("Processing completed downloads");
var torrents = await _transmissionClient.GetTorrentsAsync();
var completedTorrents = torrents.Where(t => t.IsFinished).ToList();
foreach (var torrent in completedTorrents)
{
if (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Post-processing cancelled");
return;
}
try
{
await ProcessTorrentAsync(torrent);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error processing torrent: {torrent.Name}");
}
}
}
public async Task ProcessTorrentAsync(TorrentInfo torrent)
{
_logger.LogInformation($"Processing completed torrent: {torrent.Name}");
var config = _configService.GetConfiguration();
var downloadDir = torrent.DownloadDir;
var torrentPath = Path.Combine(downloadDir, torrent.Name);
// Check if the file/directory exists
if (!Directory.Exists(torrentPath) && !File.Exists(torrentPath))
{
_logger.LogWarning($"Downloaded path not found: {torrentPath}");
return;
}
// Handle archives if enabled
if (config.PostProcessing.ExtractArchives && IsArchive(torrentPath))
{
await ExtractArchiveAsync(torrentPath, downloadDir);
}
// Organize media files if enabled
if (config.PostProcessing.OrganizeMedia)
{
await OrganizeMediaAsync(torrentPath, config.MediaLibraryPath);
}
}
private bool IsArchive(string path)
{
if (!File.Exists(path))
{
return false;
}
var extension = Path.GetExtension(path).ToLowerInvariant();
return extension == ".rar" || extension == ".zip" || extension == ".7z";
}
private async Task ExtractArchiveAsync(string archivePath, string outputDir)
{
_logger.LogInformation($"Extracting archive: {archivePath}");
try
{
var extension = Path.GetExtension(archivePath).ToLowerInvariant();
var extractDir = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(archivePath));
// Create extraction directory if it doesn't exist
if (!Directory.Exists(extractDir))
{
Directory.CreateDirectory(extractDir);
}
var processStartInfo = new ProcessStartInfo
{
FileName = extension switch
{
".rar" => "unrar",
".zip" => "unzip",
".7z" => "7z",
_ => throw new Exception($"Unsupported archive format: {extension}")
},
Arguments = extension switch
{
".rar" => $"x -o+ \"{archivePath}\" \"{extractDir}\"",
".zip" => $"-o \"{archivePath}\" -d \"{extractDir}\"",
".7z" => $"x \"{archivePath}\" -o\"{extractDir}\"",
_ => throw new Exception($"Unsupported archive format: {extension}")
},
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = new Process
{
StartInfo = processStartInfo
};
process.Start();
await process.WaitForExitAsync();
if (process.ExitCode != 0)
{
var error = await process.StandardError.ReadToEndAsync();
_logger.LogError($"Error extracting archive: {error}");
return;
}
_logger.LogInformation($"Archive extracted to: {extractDir}");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error extracting archive: {archivePath}");
}
}
private async Task OrganizeMediaAsync(string path, string mediaLibraryPath)
{
_logger.LogInformation($"Organizing media: {path}");
var config = _configService.GetConfiguration();
var mediaExtensions = config.PostProcessing.MediaExtensions;
// Ensure media library path exists
if (!Directory.Exists(mediaLibraryPath))
{
Directory.CreateDirectory(mediaLibraryPath);
}
try
{
if (File.Exists(path))
{
// Single file
var extension = Path.GetExtension(path).ToLowerInvariant();
if (mediaExtensions.Contains(extension))
{
await CopyFileToMediaLibraryAsync(path, mediaLibraryPath);
}
}
else if (Directory.Exists(path))
{
// Directory - find all media files recursively
var mediaFiles = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)
.Where(f => mediaExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.ToList();
foreach (var mediaFile in mediaFiles)
{
await CopyFileToMediaLibraryAsync(mediaFile, mediaLibraryPath);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error organizing media: {path}");
}
}
private async Task CopyFileToMediaLibraryAsync(string filePath, string mediaLibraryPath)
{
var fileName = Path.GetFileName(filePath);
var destinationPath = Path.Combine(mediaLibraryPath, fileName);
// If destination file already exists, add a unique identifier
if (File.Exists(destinationPath))
{
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
var extension = Path.GetExtension(fileName);
var uniqueId = Guid.NewGuid().ToString().Substring(0, 8);
destinationPath = Path.Combine(mediaLibraryPath, $"{fileNameWithoutExt}_{uniqueId}{extension}");
}
_logger.LogInformation($"Copying media file to library: {destinationPath}");
try
{
using var sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
using var destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
await sourceStream.CopyToAsync(destinationStream);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error copying file to media library: {filePath}");
}
}
}
public class PostProcessingBackgroundService : BackgroundService
{
private readonly ILogger<PostProcessingBackgroundService> _logger;
private readonly IPostProcessor _postProcessor;
private readonly IConfigService _configService;
public PostProcessingBackgroundService(
ILogger<PostProcessingBackgroundService> logger,
IPostProcessor postProcessor,
IConfigService configService)
{
_logger = logger;
_postProcessor = postProcessor;
_configService = configService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Post-processing background service started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await _postProcessor.ProcessCompletedDownloadsAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing completed downloads");
}
// Check every 5 minutes
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
}
+350
View File
@@ -0,0 +1,350 @@
using System;
using System.Collections.Generic;
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.Hosting;
using TransmissionRssManager.Core;
namespace TransmissionRssManager.Services
{
public class RssFeedManager : IRssFeedManager
{
private readonly ILogger<RssFeedManager> _logger;
private readonly IConfigService _configService;
private readonly ITransmissionClient _transmissionClient;
private readonly HttpClient _httpClient;
private readonly string _dataPath;
private List<RssFeedItem> _items = new List<RssFeedItem>();
public RssFeedManager(
ILogger<RssFeedManager> logger,
IConfigService configService,
ITransmissionClient transmissionClient)
{
_logger = logger;
_configService = configService;
_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()
{
var config = _configService.GetConfiguration();
return Task.FromResult(config.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);
}
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();
}
}
public async Task UpdateFeedAsync(RssFeed feed)
{
var config = _configService.GetConfiguration();
var index = config.Feeds.FindIndex(f => f.Id == feed.Id);
if (index != -1)
{
config.Feeds[index] = feed;
await _configService.SaveConfigurationAsync(config);
}
}
public async Task RefreshFeedsAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting RSS feed refresh");
var config = _configService.GetConfiguration();
foreach (var feed in config.Feeds)
{
if (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("RSS refresh cancelled");
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}");
}
}
// Check for matches and auto-download if enabled
await ProcessMatchesAsync();
}
public async Task MarkItemAsDownloadedAsync(string itemId)
{
var item = _items.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");
}
}
}
public class RssFeedBackgroundService : BackgroundService
{
private readonly ILogger<RssFeedBackgroundService> _logger;
private readonly IRssFeedManager _rssFeedManager;
private readonly IConfigService _configService;
public RssFeedBackgroundService(
ILogger<RssFeedBackgroundService> logger,
IRssFeedManager rssFeedManager,
IConfigService configService)
{
_logger = logger;
_rssFeedManager = rssFeedManager;
_configService = configService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("RSS feed background service started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await _rssFeedManager.RefreshFeedsAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing RSS feeds");
}
var config = _configService.GetConfiguration();
var interval = TimeSpan.FromMinutes(config.CheckIntervalMinutes);
_logger.LogInformation($"Next refresh in {interval.TotalMinutes} minutes");
await Task.Delay(interval, stoppingToken);
}
}
}
}
+309
View File
@@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Extensions.Logging;
using TransmissionRssManager.Core;
namespace TransmissionRssManager.Services
{
public class TransmissionClient : ITransmissionClient
{
private readonly ILogger<TransmissionClient> _logger;
private readonly IConfigService _configService;
private readonly HttpClient _httpClient;
private string _sessionId = string.Empty;
public TransmissionClient(ILogger<TransmissionClient> logger, IConfigService configService)
{
_logger = logger;
_configService = configService;
// Configure the main HttpClient with handler that ignores certificate errors
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
};
_httpClient = new HttpClient(handler);
_httpClient.Timeout = TimeSpan.FromSeconds(10);
_logger.LogInformation("TransmissionClient initialized with certificate validation disabled");
}
public async Task<List<TorrentInfo>> GetTorrentsAsync()
{
var config = _configService.GetConfiguration();
var request = new
{
method = "torrent-get",
arguments = new
{
fields = new[] { "id", "name", "status", "percentDone", "totalSize", "downloadDir" }
}
};
var response = await SendRequestAsync<TorrentGetResponse>(config.Transmission.Url, request);
_logger.LogInformation($"Transmission torrent response: {response != null}, Arguments: {response?.Arguments != null}, Result: {response?.Result}");
if (response?.Arguments?.Torrents == null)
{
_logger.LogWarning("No torrents found in response");
return new List<TorrentInfo>();
}
_logger.LogInformation($"Found {response.Arguments.Torrents.Count} torrents in response");
var torrents = new List<TorrentInfo>();
foreach (var torrent in response.Arguments.Torrents)
{
_logger.LogInformation($"Processing torrent: {torrent.Id} - {torrent.Name}");
torrents.Add(new TorrentInfo
{
Id = torrent.Id,
Name = torrent.Name,
Status = GetStatusText(torrent.Status),
PercentDone = torrent.PercentDone,
TotalSize = torrent.TotalSize,
DownloadDir = torrent.DownloadDir
});
}
return torrents;
}
public async Task<int> AddTorrentAsync(string torrentUrl, string downloadDir)
{
var config = _configService.GetConfiguration();
var request = new
{
method = "torrent-add",
arguments = new
{
filename = torrentUrl,
downloadDir = downloadDir
}
};
var response = await SendRequestAsync<TorrentAddResponse>(config.Transmission.Url, request);
if (response?.Arguments?.TorrentAdded != null)
{
return response.Arguments.TorrentAdded.Id;
}
else if (response?.Arguments?.TorrentDuplicate != null)
{
return response.Arguments.TorrentDuplicate.Id;
}
throw new Exception("Failed to add torrent");
}
public async Task RemoveTorrentAsync(int id, bool deleteLocalData)
{
var config = _configService.GetConfiguration();
var request = new
{
method = "torrent-remove",
arguments = new
{
ids = new[] { id },
deleteLocalData = deleteLocalData
}
};
await SendRequestAsync<object>(config.Transmission.Url, request);
}
public async Task StartTorrentAsync(int id)
{
var config = _configService.GetConfiguration();
var request = new
{
method = "torrent-start",
arguments = new
{
ids = new[] { id }
}
};
await SendRequestAsync<object>(config.Transmission.Url, request);
}
public async Task StopTorrentAsync(int id)
{
var config = _configService.GetConfiguration();
var request = new
{
method = "torrent-stop",
arguments = new
{
ids = new[] { id }
}
};
await SendRequestAsync<object>(config.Transmission.Url, request);
}
private async Task<T> SendRequestAsync<T>(string url, object requestData)
{
var config = _configService.GetConfiguration();
var jsonContent = JsonSerializer.Serialize(requestData);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
// Always create a fresh HttpClient to avoid connection issues
using var httpClient = new HttpClient(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
});
// Ensure we have a valid URL by reconstructing it explicitly
var protocol = config.Transmission.UseHttps ? "https" : "http";
var serverUrl = $"{protocol}://{config.Transmission.Host}:{config.Transmission.Port}/transmission/rpc";
var request = new HttpRequestMessage(HttpMethod.Post, serverUrl)
{
Content = content
};
// Add session ID if we have one
if (!string.IsNullOrEmpty(_sessionId))
{
request.Headers.Add("X-Transmission-Session-Id", _sessionId);
}
// Add authentication if provided
if (!string.IsNullOrEmpty(config.Transmission.Username) && !string.IsNullOrEmpty(config.Transmission.Password))
{
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{config.Transmission.Username}:{config.Transmission.Password}"));
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
}
try
{
// Set timeout to avoid hanging indefinitely on connection issues
httpClient.Timeout = TimeSpan.FromSeconds(10);
_logger.LogInformation($"Connecting to Transmission at {serverUrl} with auth: {!string.IsNullOrEmpty(config.Transmission.Username)}");
var response = await httpClient.SendAsync(request);
// Check if we need a new session ID
if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
{
if (response.Headers.TryGetValues("X-Transmission-Session-Id", out var sessionIds))
{
_sessionId = sessionIds.FirstOrDefault() ?? string.Empty;
_logger.LogInformation($"Got new Transmission session ID: {_sessionId}");
// Retry request with new session ID
return await SendRequestAsync<T>(url, requestData);
}
}
response.EnsureSuccessStatusCode();
var resultContent = await response.Content.ReadAsStringAsync();
_logger.LogInformation($"Received successful response from Transmission: {resultContent.Substring(0, Math.Min(resultContent.Length, 500))}");
// Configure JSON deserializer to be case insensitive
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<T>(resultContent, options);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error communicating with Transmission at {serverUrl}: {ex.Message}");
throw new Exception($"Failed to connect to Transmission at {config.Transmission.Host}:{config.Transmission.Port}. Error: {ex.Message}", ex);
}
}
private string GetStatusText(int status)
{
return status switch
{
0 => "Stopped",
1 => "Queued",
2 => "Verifying",
3 => "Downloading",
4 => "Seeding",
5 => "Queued",
6 => "Checking",
_ => "Unknown"
};
}
// Transmission response classes with proper JSON attribute names
private class TorrentGetResponse
{
[System.Text.Json.Serialization.JsonPropertyName("arguments")]
public TorrentGetArguments Arguments { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("result")]
public string Result { get; set; }
}
private class TorrentGetArguments
{
[System.Text.Json.Serialization.JsonPropertyName("torrents")]
public List<TransmissionTorrent> Torrents { get; set; }
}
private class TransmissionTorrent
{
[System.Text.Json.Serialization.JsonPropertyName("id")]
public int Id { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("name")]
public string Name { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("status")]
public int Status { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("percentDone")]
public double PercentDone { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("totalSize")]
public long TotalSize { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("downloadDir")]
public string DownloadDir { get; set; }
}
private class TorrentAddResponse
{
[System.Text.Json.Serialization.JsonPropertyName("arguments")]
public TorrentAddArguments Arguments { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("result")]
public string Result { get; set; }
}
private class TorrentAddArguments
{
[System.Text.Json.Serialization.JsonPropertyName("torrent-added")]
public TorrentAddInfo TorrentAdded { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("torrent-duplicate")]
public TorrentAddInfo TorrentDuplicate { get; set; }
}
private class TorrentAddInfo
{
[System.Text.Json.Serialization.JsonPropertyName("id")]
public int Id { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("name")]
public string Name { get; set; }
[System.Text.Json.Serialization.JsonPropertyName("hashString")]
public string HashString { get; set; }
}
}
}