1048 lines
35 KiB
Bash
Executable File
1048 lines
35 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Reset and run the Transmission RSS Manager application
|
|
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Clean up existing test directory
|
|
echo -e "${YELLOW}Removing existing test directory...${NC}"
|
|
rm -rf "$HOME/transmission-rss-test"
|
|
|
|
# Create and prepare test directory
|
|
echo -e "${GREEN}Creating fresh test directory...${NC}"
|
|
TEST_DIR="$HOME/transmission-rss-test"
|
|
mkdir -p "$TEST_DIR"
|
|
|
|
# Copy files with fixed namespaces
|
|
echo -e "${GREEN}Copying application files with fixed namespaces...${NC}"
|
|
mkdir -p "$TEST_DIR/src/Api"
|
|
mkdir -p "$TEST_DIR/src/Core"
|
|
mkdir -p "$TEST_DIR/src/Services"
|
|
mkdir -p "$TEST_DIR/src/Web/wwwroot/css"
|
|
mkdir -p "$TEST_DIR/src/Web/wwwroot/js"
|
|
|
|
# Copy Program.cs with fixed namespaces
|
|
cat > "$TEST_DIR/src/Api/Program.cs" << 'EOL'
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.FileProviders;
|
|
using Microsoft.Extensions.Logging;
|
|
using System;
|
|
using System.IO;
|
|
using TransmissionRssManager.Core;
|
|
using TransmissionRssManager.Services;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Add services to the container
|
|
builder.Services.AddControllers();
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
|
|
// Add custom services
|
|
builder.Services.AddSingleton<IConfigService, ConfigService>();
|
|
builder.Services.AddSingleton<ITransmissionClient, TransmissionClient>();
|
|
builder.Services.AddSingleton<IRssFeedManager, RssFeedManager>();
|
|
builder.Services.AddSingleton<IPostProcessor, PostProcessor>();
|
|
|
|
// Add background services
|
|
builder.Services.AddHostedService<RssFeedBackgroundService>();
|
|
builder.Services.AddHostedService<PostProcessingBackgroundService>();
|
|
|
|
var app = builder.Build();
|
|
|
|
// Configure middleware
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
|
|
// Configure static files
|
|
var webRootPath = Path.Combine(AppContext.BaseDirectory, "wwwroot");
|
|
app.UseStaticFiles(new StaticFileOptions
|
|
{
|
|
FileProvider = new PhysicalFileProvider(webRootPath),
|
|
RequestPath = ""
|
|
});
|
|
|
|
// Create default route to serve index.html
|
|
app.MapGet("/", context =>
|
|
{
|
|
context.Response.ContentType = "text/html";
|
|
context.Response.Redirect("/index.html");
|
|
return System.Threading.Tasks.Task.CompletedTask;
|
|
});
|
|
|
|
app.UseRouting();
|
|
app.UseAuthorization();
|
|
app.MapControllers();
|
|
|
|
// Log where static files are being served from
|
|
app.Logger.LogInformation($"Static files are served from: {webRootPath}");
|
|
|
|
app.Run();
|
|
EOL
|
|
|
|
# Copy project file
|
|
cat > "$TEST_DIR/TransmissionRssManager.csproj" << 'EOL'
|
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
|
|
<PropertyGroup>
|
|
<TargetFramework>net7.0</TargetFramework>
|
|
<RootNamespace>TransmissionRssManager</RootNamespace>
|
|
<ImplicitUsings>disable</ImplicitUsings>
|
|
<Nullable>enable</Nullable>
|
|
<Version>1.0.0</Version>
|
|
<Authors>TransmissionRssManager</Authors>
|
|
<Description>A C# application to manage RSS feeds and automatically download torrents via Transmission</Description>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.13" />
|
|
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="7.0.0" />
|
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
|
<PackageReference Include="System.ServiceModel.Syndication" Version="7.0.0" />
|
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
|
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<None Update="wwwroot\**\*">
|
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
</None>
|
|
</ItemGroup>
|
|
|
|
</Project>
|
|
EOL
|
|
|
|
# Copy Core/Interfaces.cs
|
|
cp -v /opt/develop/transmission-rss-manager/TransmissionRssManager/src/Core/Interfaces.cs "$TEST_DIR/src/Core/"
|
|
|
|
# Copy RssFeedManager with fixed namespaces
|
|
cat > "$TEST_DIR/src/Services/RssFeedManager.cs" << 'EOL'
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EOL
|
|
|
|
# Copy PostProcessor with fixed namespaces
|
|
cat > "$TEST_DIR/src/Services/PostProcessor.cs" << 'EOL'
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EOL
|
|
|
|
# Copy TransmissionClient with fixed namespaces
|
|
cat > "$TEST_DIR/src/Services/TransmissionClient.cs" << 'EOL'
|
|
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;
|
|
_httpClient = new HttpClient();
|
|
}
|
|
|
|
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);
|
|
|
|
if (response?.Arguments?.Torrents == null)
|
|
{
|
|
return new List<TorrentInfo>();
|
|
}
|
|
|
|
var torrents = new List<TorrentInfo>();
|
|
foreach (var torrent in response.Arguments.Torrents)
|
|
{
|
|
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");
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, url)
|
|
{
|
|
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
|
|
{
|
|
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");
|
|
|
|
// Retry request with new session ID
|
|
return await SendRequestAsync<T>(url, requestData);
|
|
}
|
|
}
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var resultContent = await response.Content.ReadAsStringAsync();
|
|
return JsonSerializer.Deserialize<T>(resultContent);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error communicating with Transmission");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
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
|
|
private class TorrentGetResponse
|
|
{
|
|
public TorrentGetArguments Arguments { get; set; }
|
|
public string Result { get; set; }
|
|
}
|
|
|
|
private class TorrentGetArguments
|
|
{
|
|
public List<TransmissionTorrent> Torrents { get; set; }
|
|
}
|
|
|
|
private class TransmissionTorrent
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; }
|
|
public int Status { get; set; }
|
|
public double PercentDone { get; set; }
|
|
public long TotalSize { get; set; }
|
|
public string DownloadDir { get; set; }
|
|
}
|
|
|
|
private class TorrentAddResponse
|
|
{
|
|
public TorrentAddArguments Arguments { get; set; }
|
|
public string Result { get; set; }
|
|
}
|
|
|
|
private class TorrentAddArguments
|
|
{
|
|
public TorrentAddInfo TorrentAdded { get; set; }
|
|
public TorrentAddInfo TorrentDuplicate { get; set; }
|
|
}
|
|
|
|
private class TorrentAddInfo
|
|
{
|
|
public int Id { get; set; }
|
|
public string Name { get; set; }
|
|
public string HashString { get; set; }
|
|
}
|
|
}
|
|
}
|
|
EOL
|
|
|
|
# Copy ConfigService.cs
|
|
cp -v /opt/develop/transmission-rss-manager/TransmissionRssManager/src/Services/ConfigService.cs "$TEST_DIR/src/Services/"
|
|
|
|
# Copy API Controllers
|
|
cp -vr /opt/develop/transmission-rss-manager/TransmissionRssManager/src/Api/Controllers "$TEST_DIR/src/Api/"
|
|
|
|
# Copy web content
|
|
cp -vr /opt/develop/transmission-rss-manager/TransmissionRssManager/src/Web/wwwroot/* "$TEST_DIR/src/Web/wwwroot/"
|
|
|
|
# Build the application
|
|
cd "$TEST_DIR"
|
|
echo -e "${GREEN}Setting up NuGet packages...${NC}"
|
|
dotnet restore
|
|
if [ $? -ne 0 ]; then
|
|
echo -e "${RED}Failed to restore NuGet packages.${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
echo -e "${GREEN}Building application...${NC}"
|
|
dotnet build
|
|
if [ $? -ne 0 ]; then
|
|
echo -e "${RED}Build failed.${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# Find server's IP address
|
|
SERVER_IP=$(hostname -I | awk '{print $1}')
|
|
|
|
# Skip running if parameter is passed
|
|
if [[ "$1" == "no_run" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Run the application
|
|
echo -e "${GREEN}Starting application...${NC}"
|
|
echo -e "${GREEN}The web interface will be available at:${NC}"
|
|
echo -e "${GREEN}- Local: http://localhost:5000${NC}"
|
|
if [[ -f "./Properties/launchSettings.json" && -n "$SERVER_IP" ]]; then
|
|
echo -e "${GREEN}- Network: http://${SERVER_IP}:5000${NC}"
|
|
fi
|
|
echo -e "${YELLOW}Press Ctrl+C to stop the application${NC}"
|
|
|
|
dotnet run |