From 96f95f228fb97c0eeb0257f01fb573cfd80f286d Mon Sep 17 00:00:00 2001 From: MasterDraco Date: Wed, 12 Mar 2025 20:56:40 +0000 Subject: [PATCH] Add one-click installation script and fix MetricsService --- README.md | 53 ++- install.sh | 220 ++++++++++ src/Services/MetricsService.cs | 746 +++++++++++++++++++++++++++++++++ test-installation.sh | 192 +++++++++ 4 files changed, 1188 insertions(+), 23 deletions(-) create mode 100755 install.sh create mode 100644 src/Services/MetricsService.cs create mode 100755 test-installation.sh diff --git a/README.md b/README.md index c083190..d3a3cbf 100644 --- a/README.md +++ b/README.md @@ -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 - Post-processing of completed downloads (extract archives, organize media files) - Responsive web UI for desktop and mobile use +- Database integration for historical data and statistics +- Dashboard with metrics and download history ## Requirements -- .NET 7.0 or higher +- Linux OS (Debian, Ubuntu, or derivatives) - 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 - -Run the installer script: +Install Transmission RSS Manager with a single command: ```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 -./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 - git clone https://github.com/yourusername/transmission-rss-manager.git - 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` +```bash +sudo bash install.sh +``` ## Configuration diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..263d371 --- /dev/null +++ b/install.sh @@ -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}" \ No newline at end of file diff --git a/src/Services/MetricsService.cs b/src/Services/MetricsService.cs new file mode 100644 index 0000000..657c6ed --- /dev/null +++ b/src/Services/MetricsService.cs @@ -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 GetDashboardStatsAsync(); + Task> GetDownloadHistoryAsync(int days = 30); + Task> GetCategoryStatsAsync(); + Task GetSystemStatusAsync(); + Task> EstimateDiskUsageAsync(); + Task> GetPerformanceMetricsAsync(); + } + + public class MetricsService : IMetricsService + { + private readonly ILogger _logger; + private readonly ITransmissionClient _transmissionClient; + private readonly IRepository _feedRepository; + private readonly IRepository _feedItemRepository; + private readonly IRepository _torrentRepository; + private readonly IRepository _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 logger, + ITransmissionClient transmissionClient, + IRepository feedRepository, + IRepository feedItemRepository, + IRepository torrentRepository, + IRepository logRepository, + ILoggingService loggingService, + IConfigService configService) + { + _logger = logger; + _transmissionClient = transmissionClient; + _feedRepository = feedRepository; + _feedItemRepository = feedItemRepository; + _torrentRepository = torrentRepository; + _logRepository = logRepository; + _loggingService = loggingService; + _configService = configService; + } + + public async Task 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 { + { "ErrorMessage", ex.Message }, + { "StackTrace", ex.StackTrace } + } + ); + + return new DashboardStats(); + } + } + + public async Task> GetDownloadHistoryAsync(int days = 30) + { + try + { + var result = new List(); + 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 { + { "ErrorMessage", ex.Message }, + { "StackTrace", ex.StackTrace }, + { "Days", days.ToString() } + } + ); + + return new List(); + } + } + + public async Task> 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(); + + // 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 { + { "ErrorMessage", ex.Message }, + { "StackTrace", ex.StackTrace } + } + ); + + return new List(); + } + } + + public async Task 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> 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 + { + ["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 { + { "ErrorMessage", ex.Message }, + { "StackTrace", ex.StackTrace } + } + ); + + return new Dictionary + { + ["activeTorrentsSize"] = 0, + ["historicalTorrentsSize"] = 0, + ["totalTorrentsSize"] = 0, + ["databaseSize"] = 0, + ["availableSpace"] = 0 + }; + } + } + + public async Task> GetPerformanceMetricsAsync() + { + var metrics = new Dictionary(); + + 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 + { + ["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 _logger; + private readonly IServiceProvider _serviceProvider; + + public MetricsBackgroundService( + ILogger 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(); + + // 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"); + } + } +} \ No newline at end of file diff --git a/test-installation.sh b/test-installation.sh new file mode 100755 index 0000000..7093c64 --- /dev/null +++ b/test-installation.sh @@ -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}" \ No newline at end of file