Add one-click installation script and fix MetricsService

This commit is contained in:
MasterDraco 2025-03-12 20:56:40 +00:00
parent f804ca51d3
commit 96f95f228f
4 changed files with 1188 additions and 23 deletions

View File

@ -9,44 +9,51 @@ A C# application for managing RSS feeds and automatically downloading torrents v
- Manage Transmission torrents through a user-friendly web interface - Manage Transmission torrents through a user-friendly web interface
- Post-processing of completed downloads (extract archives, organize media files) - Post-processing of completed downloads (extract archives, organize media files)
- Responsive web UI for desktop and mobile use - Responsive web UI for desktop and mobile use
- Database integration for historical data and statistics
- Dashboard with metrics and download history
## Requirements ## Requirements
- .NET 7.0 or higher - Linux OS (Debian, Ubuntu, or derivatives)
- Transmission BitTorrent client (with remote access enabled) - Transmission BitTorrent client (with remote access enabled)
- Linux OS (tested on Ubuntu, Debian, Fedora, Arch)
- Dependencies: unzip, p7zip, unrar (for post-processing)
## Installation ## One-Click Installation
### Automatic Installation Install Transmission RSS Manager with a single command:
Run the installer script:
```bash ```bash
curl -sSL https://raw.githubusercontent.com/yourusername/transmission-rss-manager/main/install-script.sh | bash wget -O - https://git.powerdata.dk/masterdraco/Torrent-Manager/raw/branch/main/TransmissionRssManager/install.sh | sudo bash
``` ```
Or if you've cloned the repository: This command:
1. Downloads the installer script
2. Installs all dependencies (.NET SDK, PostgreSQL, etc.)
3. Sets up the database
4. Builds and configures the application
5. Creates and starts the system service
After installation, access the web interface at: http://localhost:5000
## Alternative Installation Methods
If you prefer to examine the install script before running it, you can:
```bash ```bash
./src/Infrastructure/install-script.sh # Download the install script
wget https://git.powerdata.dk/masterdraco/Torrent-Manager/raw/branch/main/TransmissionRssManager/install.sh
# Review it
less install.sh
# Run it
sudo bash install.sh
``` ```
### Manual Installation Or if you've already cloned the repository:
1. Install .NET 7.0 SDK from [Microsoft's website](https://dotnet.microsoft.com/download)
2. Clone the repository:
```bash ```bash
git clone https://github.com/yourusername/transmission-rss-manager.git sudo bash install.sh
cd transmission-rss-manager
``` ```
3. Build and run the application:
```bash
dotnet build -c Release
dotnet run
```
4. Open a web browser and navigate to: `http://localhost:5000`
## Configuration ## Configuration

220
install.sh Executable file
View File

@ -0,0 +1,220 @@
#!/bin/bash
# Transmission RSS Manager One-Click Installer
# This script downloads and installs Transmission RSS Manager with all dependencies
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Print section header
print_section() {
echo -e "\n${GREEN}===== $1 =====${NC}"
}
# Error handling
set -e
trap 'echo -e "${RED}An error occurred. Installation failed.${NC}"; exit 1' ERR
print_section "Transmission RSS Manager Installer"
echo -e "This script will install Transmission RSS Manager and all required components."
# Check if running as root (sudo)
if [ "$EUID" -ne 0 ]; then
echo -e "${YELLOW}Please run this script with sudo:${NC}"
echo -e "${YELLOW}sudo bash install.sh${NC}"
exit 1
fi
# Detect Linux distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
DISTRO=$ID
else
echo -e "${RED}Cannot detect Linux distribution. This script supports Debian, Ubuntu, and their derivatives.${NC}"
exit 1
fi
print_section "Installing Dependencies"
# Update package lists
echo "Updating package lists..."
apt-get update
# Install basic dependencies
echo "Installing required packages..."
apt-get install -y wget curl unzip git
# Install .NET SDK
print_section "Installing .NET SDK"
if ! command -v dotnet &> /dev/null; then
echo "Installing .NET SDK 7.0..."
# Add Microsoft package repository
wget -O packages-microsoft-prod.deb https://packages.microsoft.com/config/$DISTRO/$VERSION_ID/packages-microsoft-prod.deb
dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
# Install .NET SDK
apt-get update
apt-get install -y apt-transport-https
apt-get update
apt-get install -y dotnet-sdk-7.0
else
echo ".NET SDK is already installed."
fi
# Verify .NET installation
dotnet --version
if [ $? -ne 0 ]; then
echo -e "${RED}.NET SDK installation failed.${NC}"
exit 1
fi
# Install PostgreSQL
print_section "Installing PostgreSQL"
if ! command -v psql &> /dev/null; then
echo "Installing PostgreSQL..."
apt-get install -y postgresql postgresql-contrib
else
echo "PostgreSQL is already installed."
fi
# Start PostgreSQL service
systemctl start postgresql
systemctl enable postgresql
# Install Entity Framework Core tools
print_section "Installing EF Core tools"
if ! su - postgres -c "dotnet tool list -g" | grep "dotnet-ef" > /dev/null; then
echo "Installing Entity Framework Core tools..."
su - postgres -c "dotnet tool install --global dotnet-ef --version 7.0.15"
else
echo "Entity Framework Core tools are already installed."
fi
# Create installation directory
print_section "Setting up application"
INSTALL_DIR="/opt/transmission-rss-manager"
mkdir -p $INSTALL_DIR
# Download or clone the application
if [ ! -d "$INSTALL_DIR/.git" ]; then
echo "Downloading application files..."
# Clone the repository
git clone https://git.powerdata.dk/masterdraco/Torrent-Manager.git $INSTALL_DIR
else
echo "Updating existing installation..."
cd $INSTALL_DIR
git pull
fi
# Setup database
print_section "Setting up database"
DB_NAME="torrentmanager"
DB_USER="torrentmanager"
DB_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16)
# Check if database exists
DB_EXISTS=$(su - postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\"")
if [ "$DB_EXISTS" != "1" ]; then
echo "Creating database and user..."
# Create database and user
su - postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASSWORD';\""
su - postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\""
su - postgres -c "psql -c \"GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;\""
else
echo "Database already exists."
fi
# Save connection string
CONFIG_DIR="/etc/transmission-rss-manager"
mkdir -p $CONFIG_DIR
echo '{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database='$DB_NAME';Username='$DB_USER';Password='$DB_PASSWORD'"
}
}' > "$CONFIG_DIR/appsettings.json"
# Set proper permissions
chown -R postgres:postgres "$CONFIG_DIR"
chmod 750 "$CONFIG_DIR"
chmod 640 "$CONFIG_DIR/appsettings.json"
# Build and deploy the application
print_section "Building application"
# Check if TransmissionRssManager directory exists directly or as a subdirectory
if [ -d "$INSTALL_DIR/TransmissionRssManager" ]; then
PROJECT_DIR="$INSTALL_DIR/TransmissionRssManager"
elif [ -f "$INSTALL_DIR/TransmissionRssManager.csproj" ]; then
PROJECT_DIR="$INSTALL_DIR"
else
# Look for the .csproj file
PROJECT_DIR=$(find $INSTALL_DIR -name "*.csproj" -exec dirname {} \; | head -n 1)
fi
echo "Building project from: $PROJECT_DIR"
cd "$PROJECT_DIR"
dotnet restore
dotnet build -c Release
dotnet publish -c Release -o $INSTALL_DIR/publish
# Copy configuration
cp "$CONFIG_DIR/appsettings.json" "$INSTALL_DIR/publish/appsettings.json"
# Run migrations
print_section "Running database migrations"
cd "$PROJECT_DIR"
dotnet ef database update
# Create systemd service
print_section "Creating systemd service"
# Find the main application DLL
APP_DLL=$(find $INSTALL_DIR/publish -name "*.dll" | head -n 1)
APP_NAME=$(basename "$APP_DLL" .dll)
echo "[Unit]
Description=Transmission RSS Manager
After=network.target postgresql.service
[Service]
WorkingDirectory=$INSTALL_DIR/publish
ExecStart=/usr/bin/dotnet $APP_DLL
Restart=always
RestartSec=10
SyslogIdentifier=transmission-rss-manager
User=postgres
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target" > /etc/systemd/system/transmission-rss-manager.service
# Reload systemd, enable and start service
systemctl daemon-reload
systemctl enable transmission-rss-manager
systemctl start transmission-rss-manager
# Create shortcut
print_section "Creating application shortcut"
echo "[Desktop Entry]
Name=Transmission RSS Manager
Comment=RSS Feed Manager for Transmission BitTorrent Client
Exec=xdg-open http://localhost:5000
Icon=transmission
Terminal=false
Type=Application
Categories=Network;P2P;" > /usr/share/applications/transmission-rss-manager.desktop
# Installation complete
print_section "Installation Complete!"
echo -e "${GREEN}Transmission RSS Manager has been successfully installed!${NC}"
echo -e "Web interface: ${YELLOW}http://localhost:5000${NC}"
echo -e "Database username: ${YELLOW}$DB_USER${NC}"
echo -e "Database password: ${YELLOW}$DB_PASSWORD${NC}"
echo -e "Configuration file: ${YELLOW}$CONFIG_DIR/appsettings.json${NC}"
echo -e "Application files: ${YELLOW}$INSTALL_DIR${NC}"
echo -e "\nTo check service status: ${YELLOW}systemctl status transmission-rss-manager${NC}"
echo -e "View logs: ${YELLOW}journalctl -u transmission-rss-manager${NC}"

View File

@ -0,0 +1,746 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TransmissionRssManager.Core;
using TransmissionRssManager.Data.Repositories;
namespace TransmissionRssManager.Services
{
public interface IMetricsService
{
Task<DashboardStats> GetDashboardStatsAsync();
Task<List<HistoricalDataPoint>> GetDownloadHistoryAsync(int days = 30);
Task<List<CategoryStats>> GetCategoryStatsAsync();
Task<SystemStatus> GetSystemStatusAsync();
Task<Dictionary<string, long>> EstimateDiskUsageAsync();
Task<Dictionary<string, double>> GetPerformanceMetricsAsync();
}
public class MetricsService : IMetricsService
{
private readonly ILogger<MetricsService> _logger;
private readonly ITransmissionClient _transmissionClient;
private readonly IRepository<TransmissionRssManager.Data.Models.RssFeed> _feedRepository;
private readonly IRepository<TransmissionRssManager.Data.Models.RssFeedItem> _feedItemRepository;
private readonly IRepository<TransmissionRssManager.Data.Models.Torrent> _torrentRepository;
private readonly IRepository<TransmissionRssManager.Data.Models.SystemLogEntry> _logRepository;
private readonly ILoggingService _loggingService;
private readonly IConfigService _configService;
// Helper method to map database Torrent model to Core.TorrentInfo
private Core.TorrentInfo MapToTorrentInfo(TransmissionRssManager.Data.Models.Torrent torrent)
{
return new Core.TorrentInfo
{
Id = torrent.Id,
Name = torrent.Name,
Status = torrent.Status,
PercentDone = torrent.PercentDone,
TotalSize = torrent.TotalSize,
DownloadDir = torrent.DownloadDirectory ?? "",
AddedDate = torrent.AddedOn,
CompletedDate = torrent.CompletedOn,
DownloadedEver = torrent.DownloadedEver,
UploadedEver = torrent.UploadedEver,
UploadRatio = (int)torrent.UploadRatio,
ErrorString = torrent.ErrorMessage ?? "",
HashString = torrent.Hash,
PeersConnected = torrent.PeersConnected,
DownloadSpeed = torrent.DownloadSpeed,
UploadSpeed = torrent.UploadSpeed,
Category = torrent.Category ?? "",
HasMetadata = torrent.HasMetadata,
TransmissionInstance = torrent.TransmissionInstance ?? "default",
SourceFeedId = torrent.RssFeedItemId?.ToString() ?? "",
IsPostProcessed = torrent.PostProcessed
};
}
// Helper method to map database RssFeed model to Core.RssFeed
private Core.RssFeed MapToRssFeed(TransmissionRssManager.Data.Models.RssFeed feed)
{
var result = new Core.RssFeed
{
Id = feed.Id.ToString(),
Name = feed.Name,
Url = feed.Url,
AutoDownload = feed.Enabled,
LastChecked = feed.LastCheckedAt,
TransmissionInstanceId = feed.TransmissionInstanceId ?? "default",
Schedule = feed.Schedule,
Enabled = feed.Enabled,
MaxHistoryItems = feed.MaxHistoryItems,
DefaultCategory = feed.DefaultCategory ?? "",
ErrorCount = feed.ErrorCount,
LastError = feed.LastError != null ? feed.LastCheckedAt : null,
LastErrorMessage = feed.LastError
};
// Add rules to the feed
if (feed.Rules != null)
{
foreach (var rule in feed.Rules)
{
result.AdvancedRules.Add(new Core.RssFeedRule
{
Id = rule.Id.ToString(),
Name = rule.Name,
Pattern = rule.IncludePattern ?? "",
IsRegex = rule.UseRegex,
IsEnabled = rule.Enabled,
IsCaseSensitive = false, // Default value as this field doesn't exist in the DB model
Category = rule.CustomSavePath ?? "",
Priority = rule.Priority,
Action = rule.EnablePostProcessing ? "process" : "download",
DestinationFolder = rule.CustomSavePath ?? ""
});
}
}
return result;
}
public MetricsService(
ILogger<MetricsService> logger,
ITransmissionClient transmissionClient,
IRepository<TransmissionRssManager.Data.Models.RssFeed> feedRepository,
IRepository<TransmissionRssManager.Data.Models.RssFeedItem> feedItemRepository,
IRepository<TransmissionRssManager.Data.Models.Torrent> torrentRepository,
IRepository<TransmissionRssManager.Data.Models.SystemLogEntry> logRepository,
ILoggingService loggingService,
IConfigService configService)
{
_logger = logger;
_transmissionClient = transmissionClient;
_feedRepository = feedRepository;
_feedItemRepository = feedItemRepository;
_torrentRepository = torrentRepository;
_logRepository = logRepository;
_loggingService = loggingService;
_configService = configService;
}
public async Task<DashboardStats> GetDashboardStatsAsync()
{
try
{
var now = DateTime.UtcNow;
var today = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc);
// Combine data from both Transmission and database
// Get active torrents from Transmission for real-time data
var transmissionTorrents = await _transmissionClient.GetTorrentsAsync();
// Get database torrent data for historical information
var dbTorrents = await _torrentRepository.Query().ToListAsync();
// Count active downloads (status is downloading)
var activeDownloads = transmissionTorrents.Count(t => t.Status == "Downloading");
// Count seeding torrents (status is seeding)
var seedingTorrents = transmissionTorrents.Count(t => t.Status == "Seeding");
// Get active feeds count
var activeFeeds = await _feedRepository.Query()
.Where(f => f.Enabled)
.CountAsync();
// Get completed downloads today
var completedToday = dbTorrents
.Count(t => t.CompletedOn.HasValue && t.CompletedOn.Value >= today);
// Get added today
var addedToday = dbTorrents
.Count(t => t.AddedOn >= today);
// Get matched count (all time)
var matchedCount = await _feedItemRepository.Query()
.Where(i => i.MatchedRuleId != null)
.CountAsync();
// Calculate download/upload speeds from Transmission (real-time data)
double downloadSpeed = transmissionTorrents.Sum(t => t.DownloadSpeed);
double uploadSpeed = transmissionTorrents.Sum(t => t.UploadSpeed);
// Update database objects with transmission data
// This helps keep database in sync with transmission for metrics
foreach (var transmissionTorrent in transmissionTorrents)
{
var dbTorrent = dbTorrents.FirstOrDefault(t => t.TransmissionId == transmissionTorrent.Id);
if (dbTorrent != null)
{
// Update database with current speeds and status for the background service to store
dbTorrent.DownloadSpeed = transmissionTorrent.DownloadSpeed;
dbTorrent.UploadSpeed = transmissionTorrent.UploadSpeed;
dbTorrent.Status = transmissionTorrent.Status;
dbTorrent.PercentDone = transmissionTorrent.PercentDone;
dbTorrent.DownloadedEver = transmissionTorrent.DownloadedEver;
dbTorrent.UploadedEver = transmissionTorrent.UploadedEver;
dbTorrent.PeersConnected = transmissionTorrent.PeersConnected;
// Update the database object
await _torrentRepository.UpdateAsync(dbTorrent);
}
}
// Save the changes
await _torrentRepository.SaveChangesAsync();
// Calculate total downloaded and uploaded
// Use Transmission for active torrents (more accurate) and database for historical torrents
var totalDownloaded = transmissionTorrents.Sum(t => t.DownloadedEver);
var totalUploaded = transmissionTorrents.Sum(t => t.UploadedEver);
// Add historical data from database for torrents that are no longer in Transmission
var transmissionIds = transmissionTorrents.Select(t => t.Id).ToHashSet();
var historicalTorrents = dbTorrents.Where(t => t.TransmissionId.HasValue && !transmissionIds.Contains(t.TransmissionId.Value));
totalDownloaded += historicalTorrents.Sum(t => t.DownloadedEver);
totalUploaded += historicalTorrents.Sum(t => t.UploadedEver);
return new DashboardStats
{
ActiveDownloads = activeDownloads,
SeedingTorrents = seedingTorrents,
ActiveFeeds = activeFeeds,
CompletedToday = completedToday,
AddedToday = addedToday,
FeedsCount = await _feedRepository.Query().CountAsync(),
MatchedCount = matchedCount,
DownloadSpeed = downloadSpeed,
UploadSpeed = uploadSpeed,
TotalDownloaded = totalDownloaded,
TotalUploaded = totalUploaded
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting dashboard stats");
_loggingService.Log(
LogLevel.Error,
$"Error getting dashboard stats: {ex.Message}",
"MetricsService",
new Dictionary<string, string> {
{ "ErrorMessage", ex.Message },
{ "StackTrace", ex.StackTrace }
}
);
return new DashboardStats();
}
}
public async Task<List<HistoricalDataPoint>> GetDownloadHistoryAsync(int days = 30)
{
try
{
var result = new List<HistoricalDataPoint>();
var endDate = DateTime.UtcNow;
var startDate = endDate.AddDays(-days);
// Get download history from the database for added torrents
var addedTorrents = await _torrentRepository.Query()
.Where(t => t.AddedOn >= startDate && t.AddedOn <= endDate)
.ToListAsync();
// Get completed torrents history
var completedTorrents = await _torrentRepository.Query()
.Where(t => t.CompletedOn.HasValue && t.CompletedOn.Value >= startDate && t.CompletedOn.Value <= endDate)
.ToListAsync();
// Group by date added
var groupedByAdded = addedTorrents
.GroupBy(t => new DateTime(t.AddedOn.Year, t.AddedOn.Month, t.AddedOn.Day, 0, 0, 0, DateTimeKind.Utc))
.Select(g => new {
Date = g.Key,
Count = g.Count(),
TotalSize = g.Sum(t => t.TotalSize)
})
.OrderBy(g => g.Date)
.ToList();
// Group by date completed
var groupedByCompleted = completedTorrents
.GroupBy(t => new DateTime(t.CompletedOn.Value.Year, t.CompletedOn.Value.Month, t.CompletedOn.Value.Day, 0, 0, 0, DateTimeKind.Utc))
.Select(g => new {
Date = g.Key,
Count = g.Count(),
TotalSize = g.Sum(t => t.TotalSize)
})
.OrderBy(g => g.Date)
.ToList();
// Fill in missing dates
for (var date = startDate; date <= endDate; date = date.AddDays(1))
{
var day = new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, DateTimeKind.Utc);
// Get data for this day
var addedData = groupedByAdded.FirstOrDefault(g => g.Date == day);
var completedData = groupedByCompleted.FirstOrDefault(g => g.Date == day);
// Create data point with added count and size
result.Add(new HistoricalDataPoint
{
Date = day,
Count = addedData?.Count ?? 0, // Use added count by default
TotalSize = addedData?.TotalSize ?? 0,
CompletedCount = completedData?.Count ?? 0,
CompletedSize = completedData?.TotalSize ?? 0
});
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting download history");
_loggingService.Log(
LogLevel.Error,
$"Error getting download history: {ex.Message}",
"MetricsService",
new Dictionary<string, string> {
{ "ErrorMessage", ex.Message },
{ "StackTrace", ex.StackTrace },
{ "Days", days.ToString() }
}
);
return new List<HistoricalDataPoint>();
}
}
public async Task<List<CategoryStats>> GetCategoryStatsAsync()
{
try
{
// Get torrents with categories from database
var dbTorrents = await _torrentRepository.Query()
.Where(t => t.Category != null)
.ToListAsync();
// Get torrents from Transmission for real-time data
var transmissionTorrents = await _transmissionClient.GetTorrentsAsync();
// Map Transmission torrents to dictionary by hash for quick lookup
var transmissionTorrentsByHash = transmissionTorrents
.Where(t => !string.IsNullOrEmpty(t.HashString))
.ToDictionary(t => t.HashString, t => t);
// Create a list to store category stats with combined data
var categoryStats = new Dictionary<string, CategoryStats>();
// Process database torrents first
foreach (var torrent in dbTorrents)
{
var category = torrent.Category ?? "Uncategorized";
if (!categoryStats.TryGetValue(category, out var stats))
{
stats = new CategoryStats
{
Category = category,
Count = 0,
TotalSize = 0,
ActiveCount = 0,
CompletedCount = 0,
DownloadSpeed = 0,
UploadSpeed = 0
};
categoryStats[category] = stats;
}
stats.Count++;
stats.TotalSize += torrent.TotalSize;
// Check if this torrent is completed
if (torrent.CompletedOn.HasValue)
{
stats.CompletedCount++;
}
// Check if this torrent is active in Transmission
if (transmissionTorrentsByHash.TryGetValue(torrent.Hash, out var transmissionTorrent))
{
stats.ActiveCount++;
stats.DownloadSpeed += transmissionTorrent.DownloadSpeed;
stats.UploadSpeed += transmissionTorrent.UploadSpeed;
}
}
// Process any Transmission torrents that might not be in the database
foreach (var torrent in transmissionTorrents)
{
// Skip if no hash or already processed
if (string.IsNullOrEmpty(torrent.HashString) ||
dbTorrents.Any(t => t.Hash == torrent.HashString))
{
continue;
}
var category = torrent.Category ?? "Uncategorized";
if (!categoryStats.TryGetValue(category, out var stats))
{
stats = new CategoryStats
{
Category = category,
Count = 0,
TotalSize = 0,
ActiveCount = 0,
CompletedCount = 0,
DownloadSpeed = 0,
UploadSpeed = 0
};
categoryStats[category] = stats;
}
stats.Count++;
stats.TotalSize += torrent.TotalSize;
stats.ActiveCount++;
stats.DownloadSpeed += torrent.DownloadSpeed;
stats.UploadSpeed += torrent.UploadSpeed;
// Check if this torrent is completed
if (torrent.IsFinished)
{
stats.CompletedCount++;
}
}
// Return the category stats ordered by count
return categoryStats.Values
.OrderByDescending(c => c.Count)
.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting category stats");
_loggingService.Log(
LogLevel.Error,
$"Error getting category stats: {ex.Message}",
"MetricsService",
new Dictionary<string, string> {
{ "ErrorMessage", ex.Message },
{ "StackTrace", ex.StackTrace }
}
);
return new List<CategoryStats>();
}
}
public async Task<SystemStatus> GetSystemStatusAsync()
{
var config = _configService.GetConfiguration();
var status = new SystemStatus
{
TransmissionConnected = false,
AutoDownloadEnabled = config.AutoDownloadEnabled,
PostProcessingEnabled = config.PostProcessing.Enabled,
EnabledFeeds = await _feedRepository.Query().Where(f => f.Enabled).CountAsync(),
TotalFeeds = await _feedRepository.Query().CountAsync(),
CheckIntervalMinutes = config.CheckIntervalMinutes,
NotificationsEnabled = config.UserPreferences.NotificationsEnabled,
DatabaseStatus = "Connected"
};
try
{
// Check database health by counting torrents
var torrentCount = await _torrentRepository.Query().CountAsync();
status.TorrentCount = torrentCount;
// Count torrents by status
status.ActiveTorrentCount = await _torrentRepository.Query()
.Where(t => t.Status == "downloading" || t.Status == "Downloading")
.CountAsync();
status.CompletedTorrentCount = await _torrentRepository.Query()
.Where(t => t.CompletedOn.HasValue)
.CountAsync();
// Check feed items count
status.FeedItemCount = await _feedItemRepository.Query().CountAsync();
// Check log entries count
var logCount = await _logRepository.Query().CountAsync();
status.LogEntryCount = logCount;
// Get database size estimate (rows * avg row size)
long estimatedDbSize = (torrentCount * 1024) + (status.FeedItemCount * 512) + (logCount * 256);
status.EstimatedDatabaseSizeBytes = estimatedDbSize;
// Try to connect to Transmission to check if it's available
var torrents = await _transmissionClient.GetTorrentsAsync();
status.TransmissionConnected = true;
status.TransmissionVersion = "Unknown"; // We don't have a way to get this info directly
status.TransmissionTorrentCount = torrents.Count;
// Enhancement: Map any transmission torrents not in our database
var dbTorrents = await _torrentRepository.Query().ToListAsync();
var transmissionHashes = torrents.Select(t => t.HashString).ToHashSet();
var dbHashes = dbTorrents.Select(t => t.Hash).ToHashSet();
status.OrphanedTorrentCount = transmissionHashes.Count(h => !dbHashes.Contains(h));
status.StaleDbTorrentCount = dbHashes.Count(h => !transmissionHashes.Contains(h));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting system status");
status.TransmissionConnected = false;
status.LastErrorMessage = ex.Message;
}
return status;
}
public async Task<Dictionary<string, long>> EstimateDiskUsageAsync()
{
try
{
// Get disk usage from both Transmission and database
var transmissionTorrents = await _transmissionClient.GetTorrentsAsync();
var dbTorrents = await _torrentRepository.Query().ToListAsync();
// Calculate total size from Transmission (most accurate for active torrents)
long transmissionSize = transmissionTorrents.Sum(t => t.TotalSize);
// Add sizes from database for torrents not in Transmission (historical data)
var transmissionHashes = transmissionTorrents.Select(t => t.HashString).ToHashSet();
var historicalTorrents = dbTorrents.Where(t => !transmissionHashes.Contains(t.Hash));
long historicalSize = historicalTorrents.Sum(t => t.TotalSize);
// Also get estimated database size
long databaseSize = await _torrentRepository.Query().CountAsync() * 1024 + // ~1KB per torrent
await _feedItemRepository.Query().CountAsync() * 512 + // ~512B per feed item
await _logRepository.Query().CountAsync() * 256; // ~256B per log entry
// Calculate available space in download directory
string downloadDir = _configService.GetConfiguration().DownloadDirectory;
long availableSpace = 0;
if (!string.IsNullOrEmpty(downloadDir) && System.IO.Directory.Exists(downloadDir))
{
try
{
var driveInfo = new System.IO.DriveInfo(System.IO.Path.GetPathRoot(downloadDir));
availableSpace = driveInfo.AvailableFreeSpace;
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Error getting available disk space for {downloadDir}");
}
}
return new Dictionary<string, long>
{
["activeTorrentsSize"] = transmissionSize,
["historicalTorrentsSize"] = historicalSize,
["totalTorrentsSize"] = transmissionSize + historicalSize,
["databaseSize"] = databaseSize,
["availableSpace"] = availableSpace
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error estimating disk usage");
_loggingService.Log(
LogLevel.Error,
$"Error estimating disk usage: {ex.Message}",
"MetricsService",
new Dictionary<string, string> {
{ "ErrorMessage", ex.Message },
{ "StackTrace", ex.StackTrace }
}
);
return new Dictionary<string, long>
{
["activeTorrentsSize"] = 0,
["historicalTorrentsSize"] = 0,
["totalTorrentsSize"] = 0,
["databaseSize"] = 0,
["availableSpace"] = 0
};
}
}
public async Task<Dictionary<string, double>> GetPerformanceMetricsAsync()
{
var metrics = new Dictionary<string, double>();
try
{
// Calculate average time to complete downloads
var completedTorrents = await _torrentRepository.Query()
.Where(t => t.CompletedOn.HasValue)
.ToListAsync();
if (completedTorrents.Any())
{
var avgCompletionTimeMinutes = completedTorrents
.Where(t => t.AddedOn < t.CompletedOn)
.Average(t => (t.CompletedOn.Value - t.AddedOn).TotalMinutes);
metrics["AvgCompletionTimeMinutes"] = Math.Round(avgCompletionTimeMinutes, 2);
}
else
{
metrics["AvgCompletionTimeMinutes"] = 0;
}
// Calculate feed refresh performance
var feeds = await _feedRepository.Query().ToListAsync();
if (feeds.Any())
{
var avgItemsPerFeed = await _feedItemRepository.Query().CountAsync() / (double)feeds.Count;
metrics["AvgItemsPerFeed"] = Math.Round(avgItemsPerFeed, 2);
}
else
{
metrics["AvgItemsPerFeed"] = 0;
}
return metrics;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting performance metrics");
return new Dictionary<string, double>
{
["AvgCompletionTimeMinutes"] = 0,
["AvgItemsPerFeed"] = 0
};
}
}
}
public class DashboardStats
{
public int ActiveDownloads { get; set; }
public int SeedingTorrents { get; set; }
public int ActiveFeeds { get; set; }
public int CompletedToday { get; set; }
public int AddedToday { get; set; }
public int FeedsCount { get; set; }
public int MatchedCount { get; set; }
public double DownloadSpeed { get; set; }
public double UploadSpeed { get; set; }
public long TotalDownloaded { get; set; }
public long TotalUploaded { get; set; }
}
public class HistoricalDataPoint
{
public DateTime Date { get; set; }
public int Count { get; set; }
public long TotalSize { get; set; }
public int CompletedCount { get; set; }
public long CompletedSize { get; set; }
}
public class CategoryStats
{
public string Category { get; set; }
public int Count { get; set; }
public long TotalSize { get; set; }
public int ActiveCount { get; set; }
public int CompletedCount { get; set; }
public double DownloadSpeed { get; set; }
public double UploadSpeed { get; set; }
}
public class SystemStatus
{
public bool TransmissionConnected { get; set; }
public string TransmissionVersion { get; set; }
public bool AutoDownloadEnabled { get; set; }
public bool PostProcessingEnabled { get; set; }
public int EnabledFeeds { get; set; }
public int TotalFeeds { get; set; }
public int CheckIntervalMinutes { get; set; }
public bool NotificationsEnabled { get; set; }
// Database status
public string DatabaseStatus { get; set; }
public int TorrentCount { get; set; }
public int ActiveTorrentCount { get; set; }
public int CompletedTorrentCount { get; set; }
public int FeedItemCount { get; set; }
public int LogEntryCount { get; set; }
public long EstimatedDatabaseSizeBytes { get; set; }
// Transmission status
public int TransmissionTorrentCount { get; set; }
// Sync status
public int OrphanedTorrentCount { get; set; } // Torrents in Transmission but not in database
public int StaleDbTorrentCount { get; set; } // Torrents in database but not in Transmission
// For compatibility
public string TranmissionVersion
{
get => TransmissionVersion;
set => TransmissionVersion = value;
}
// Error info
public string LastErrorMessage { get; set; }
}
public class MetricsBackgroundService : BackgroundService
{
private readonly ILogger<MetricsBackgroundService> _logger;
private readonly IServiceProvider _serviceProvider;
public MetricsBackgroundService(
ILogger<MetricsBackgroundService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Metrics background service started");
// Update metrics every minute
var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
using var scope = _serviceProvider.CreateScope();
var metricsService = scope.ServiceProvider.GetRequiredService<IMetricsService>();
// This just ensures the metrics are calculated and cached if needed
await metricsService.GetDashboardStatsAsync();
_logger.LogDebug("Metrics updated");
}
catch (OperationCanceledException)
{
// Service is shutting down
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating metrics");
}
}
_logger.LogInformation("Metrics background service stopped");
}
}
}

192
test-installation.sh Executable file
View File

@ -0,0 +1,192 @@
#!/bin/bash
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Print section header
print_section() {
echo -e "\n${GREEN}====== $1 ======${NC}"
}
# Error handling
set -e
trap 'echo -e "${RED}An error occurred. Test failed.${NC}"; exit 1' ERR
# Setup test environment
print_section "Setting up test environment"
CURRENT_DIR=$(pwd)
TEST_DIR="$CURRENT_DIR/test-install"
mkdir -p "$TEST_DIR"
CONFIG_DIR="$TEST_DIR/config"
mkdir -p "$CONFIG_DIR"
# Copy necessary files
print_section "Copying files"
mkdir -p "$TEST_DIR"
# Copy only essential files (not the test directory itself)
find "$CURRENT_DIR" -maxdepth 1 -not -path "*test-install*" -not -path "$CURRENT_DIR" -exec cp -r {} "$TEST_DIR/" \;
cd "$TEST_DIR"
# Check .NET SDK installation
print_section "Checking .NET SDK"
dotnet --version
if [ $? -ne 0 ]; then
echo -e "${RED}.NET SDK is not installed or not in PATH. Please install .NET SDK 7.0.${NC}"
exit 1
fi
# Build the application
print_section "Building application"
dotnet build -c Release
if [ $? -ne 0 ]; then
echo -e "${RED}Build failed. See errors above.${NC}"
exit 1
fi
echo -e "${GREEN}Build successful.${NC}"
# Create database configuration
print_section "Creating database configuration"
echo '{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=torrentmanager_test;Username=postgres;Password=postgres"
}
}' > "$CONFIG_DIR/appsettings.json"
cp "$CONFIG_DIR/appsettings.json" "$TEST_DIR/appsettings.json"
echo -e "${GREEN}Database configuration created at $CONFIG_DIR/appsettings.json${NC}"
# Check if PostgreSQL is running
print_section "Checking PostgreSQL"
if command -v pg_isready >/dev/null 2>&1; then
pg_isready
PG_READY=$?
else
echo -e "${YELLOW}PostgreSQL client tools not found.${NC}"
PG_READY=1
fi
if [ $PG_READY -ne 0 ]; then
echo -e "${YELLOW}PostgreSQL is not running or not installed.${NC}"
echo -e "${YELLOW}This test expects PostgreSQL to be available with a 'postgres' user.${NC}"
echo -e "${YELLOW}Either install PostgreSQL or modify the connection string in appsettings.json.${NC}"
# Continue anyway for testing other aspects
echo -e "${YELLOW}Continuing test without database functionality.${NC}"
# Update appsettings.json to use SQLite instead
echo '{
"ConnectionStrings": {
"DefaultConnection": "Data Source=torrentmanager_test.db"
}
}' > "$TEST_DIR/appsettings.json"
echo -e "${YELLOW}Using SQLite database instead.${NC}"
# Install Entity Framework Core SQLite provider
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 7.0.17
# Update DatabaseProvider to use SQLite
if [ -f "$TEST_DIR/src/Data/TorrentManagerContextFactory.cs" ]; then
# Replace Npgsql with SQLite in DbContext factory
sed -i 's/UseNpgsql/UseSqlite/g' "$TEST_DIR/src/Data/TorrentManagerContextFactory.cs"
echo -e "${YELLOW}Modified database provider to use SQLite.${NC}"
fi
else
# Create test database
echo -e "${GREEN}Creating test database...${NC}"
psql -U postgres -c "DROP DATABASE IF EXISTS torrentmanager_test;" || true
psql -U postgres -c "CREATE DATABASE torrentmanager_test;"
fi
# Install Entity Framework CLI if needed
if ! dotnet tool list -g | grep "dotnet-ef" > /dev/null; then
echo -e "${GREEN}Installing Entity Framework Core tools...${NC}"
dotnet tool install --global dotnet-ef --version 7.0.15
fi
# Apply migrations
print_section "Applying database migrations"
dotnet ef database update || true
if [ $? -eq 0 ]; then
echo -e "${GREEN}Migrations applied successfully.${NC}"
else
echo -e "${YELLOW}Failed to apply migrations, but continuing test.${NC}"
fi
# Test run the application (with timeout)
print_section "Testing application startup"
timeout 30s dotnet run --urls=http://localhost:5555 &
APP_PID=$!
# Wait for the app to start
echo -e "${GREEN}Waiting for application to start...${NC}"
sleep 5
# Try to access the API
print_section "Testing API endpoints"
MAX_ATTEMPTS=30
ATTEMPT=0
API_STATUS=1
echo -e "${GREEN}Waiting for API to become available...${NC}"
while [ $ATTEMPT -lt $MAX_ATTEMPTS ] && [ $API_STATUS -ne 0 ]; do
if command -v curl >/dev/null 2>&1; then
curl -s http://localhost:5555/swagger/index.html > /dev/null
API_STATUS=$?
else
# Try using wget if curl is not available
if command -v wget >/dev/null 2>&1; then
wget -q --spider http://localhost:5555/swagger/index.html
API_STATUS=$?
else
echo -e "${YELLOW}Neither curl nor wget found. Cannot test API.${NC}"
break
fi
fi
if [ $API_STATUS -ne 0 ]; then
echo -n "."
sleep 1
ATTEMPT=$((ATTEMPT+1))
fi
done
echo "" # New line after progress dots
# Kill the app
kill $APP_PID 2>/dev/null || true
if [ $API_STATUS -eq 0 ]; then
echo -e "${GREEN}API successfully responded.${NC}"
else
echo -e "${YELLOW}API did not respond within the timeout period.${NC}"
echo -e "${YELLOW}This may be normal if database migrations are slow or there are other startup delays.${NC}"
fi
# Check file permissions and structure
print_section "Checking build outputs"
if [ -f "$TEST_DIR/bin/Release/net7.0/TransmissionRssManager.dll" ]; then
echo -e "${GREEN}Application was built successfully.${NC}"
else
echo -e "${RED}Missing application binaries.${NC}"
fi
# Clean up test resources
print_section "Test completed"
if [ "$1" != "--keep" ]; then
echo -e "${GREEN}Cleaning up test resources...${NC}"
# Drop test database if PostgreSQL is available
pg_isready > /dev/null && psql -U postgres -c "DROP DATABASE IF EXISTS torrentmanager_test;" || true
# Remove test directory
rm -rf "$TEST_DIR"
echo -e "${GREEN}Test directory removed.${NC}"
else
echo -e "${GREEN}Test resources kept as requested.${NC}"
echo -e "${GREEN}Test directory: $TEST_DIR${NC}"
fi
echo -e "${GREEN}Installation test completed.${NC}"