Torrent-Manager/reset-and-run.sh

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