Add one-click installation script and fix MetricsService
This commit is contained in:
parent
f804ca51d3
commit
96f95f228f
53
README.md
53
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
|
- 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)
|
```bash
|
||||||
2. Clone the repository:
|
sudo bash install.sh
|
||||||
```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`
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
220
install.sh
Executable file
220
install.sh
Executable 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}"
|
746
src/Services/MetricsService.cs
Normal file
746
src/Services/MetricsService.cs
Normal 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
192
test-installation.sh
Executable 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}"
|
Loading…
x
Reference in New Issue
Block a user