Simplify MetricsService implementation to remove database dependencies
This commit is contained in:
parent
71fc571f38
commit
681e1aa3e9
@ -1,528 +1,91 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using TransmissionRssManager.Core;
|
using TransmissionRssManager.Core;
|
||||||
using TransmissionRssManager.Data.Repositories;
|
|
||||||
|
|
||||||
namespace TransmissionRssManager.Services
|
namespace TransmissionRssManager.Services
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for the metrics service that provides dashboard statistics and performance data
|
||||||
|
/// </summary>
|
||||||
public interface IMetricsService
|
public interface IMetricsService
|
||||||
{
|
{
|
||||||
Task<DashboardStats> GetDashboardStatsAsync();
|
Task<Dictionary<string, object>> GetDashboardStatsAsync();
|
||||||
Task<List<HistoricalDataPoint>> GetDownloadHistoryAsync(int days = 30);
|
|
||||||
Task<List<CategoryStats>> GetCategoryStatsAsync();
|
|
||||||
Task<SystemStatus> GetSystemStatusAsync();
|
|
||||||
Task<Dictionary<string, long>> EstimateDiskUsageAsync();
|
Task<Dictionary<string, long>> EstimateDiskUsageAsync();
|
||||||
Task<Dictionary<string, double>> GetPerformanceMetricsAsync();
|
Task<Dictionary<string, object>> GetSystemStatusAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service that provides metrics and statistics about downloads, system status, and performance
|
||||||
|
/// </summary>
|
||||||
public class MetricsService : IMetricsService
|
public class MetricsService : IMetricsService
|
||||||
{
|
{
|
||||||
private readonly ILogger<MetricsService> _logger;
|
private readonly ILogger<MetricsService> _logger;
|
||||||
private readonly ITransmissionClient _transmissionClient;
|
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;
|
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(
|
public MetricsService(
|
||||||
ILogger<MetricsService> logger,
|
ILogger<MetricsService> logger,
|
||||||
ITransmissionClient transmissionClient,
|
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)
|
IConfigService configService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_transmissionClient = transmissionClient;
|
_transmissionClient = transmissionClient;
|
||||||
_feedRepository = feedRepository;
|
|
||||||
_feedItemRepository = feedItemRepository;
|
|
||||||
_torrentRepository = torrentRepository;
|
|
||||||
_logRepository = logRepository;
|
|
||||||
_loggingService = loggingService;
|
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DashboardStats> GetDashboardStatsAsync()
|
/// <summary>
|
||||||
|
/// Gets dashboard statistics including active downloads, upload/download speeds, etc.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Dictionary<string, object>> GetDashboardStatsAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var stats = new Dictionary<string, object>();
|
||||||
var today = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc);
|
var torrents = await _transmissionClient.GetTorrentsAsync();
|
||||||
|
|
||||||
// Combine data from both Transmission and database
|
// Calculate basic stats
|
||||||
// Get active torrents from Transmission for real-time data
|
stats["TotalTorrents"] = torrents.Count;
|
||||||
var transmissionTorrents = await _transmissionClient.GetTorrentsAsync();
|
stats["ActiveDownloads"] = torrents.Count(t => t.Status == "Downloading");
|
||||||
|
stats["SeedingTorrents"] = torrents.Count(t => t.Status == "Seeding");
|
||||||
|
stats["CompletedTorrents"] = torrents.Count(t => t.IsFinished);
|
||||||
|
stats["TotalDownloaded"] = torrents.Sum(t => t.DownloadedEver);
|
||||||
|
stats["TotalUploaded"] = torrents.Sum(t => t.UploadedEver);
|
||||||
|
stats["DownloadSpeed"] = torrents.Sum(t => t.DownloadSpeed);
|
||||||
|
stats["UploadSpeed"] = torrents.Sum(t => t.UploadSpeed);
|
||||||
|
|
||||||
// Get database torrent data for historical information
|
// Calculate total size
|
||||||
var dbTorrents = await _torrentRepository.Query().ToListAsync();
|
long totalSize = torrents.Sum(t => t.TotalSize);
|
||||||
|
stats["TotalSize"] = totalSize;
|
||||||
|
|
||||||
// Count active downloads (status is downloading)
|
return stats;
|
||||||
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error getting dashboard stats");
|
_logger.LogError(ex, "Error getting dashboard stats");
|
||||||
_loggingService.Log(
|
return new Dictionary<string, object>
|
||||||
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);
|
["Error"] = ex.Message,
|
||||||
|
["TotalTorrents"] = 0,
|
||||||
// Get data for this day
|
["ActiveDownloads"] = 0,
|
||||||
var addedData = groupedByAdded.FirstOrDefault(g => g.Date == day);
|
["SeedingTorrents"] = 0,
|
||||||
var completedData = groupedByCompleted.FirstOrDefault(g => g.Date == day);
|
["CompletedTorrents"] = 0
|
||||||
|
};
|
||||||
// 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()
|
/// <summary>
|
||||||
{
|
/// Estimates disk usage for torrents and available space
|
||||||
try
|
/// </summary>
|
||||||
{
|
|
||||||
// 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()
|
public async Task<Dictionary<string, long>> EstimateDiskUsageAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get disk usage from both Transmission and database
|
// Get disk usage from torrents
|
||||||
var transmissionTorrents = await _transmissionClient.GetTorrentsAsync();
|
var torrents = await _transmissionClient.GetTorrentsAsync();
|
||||||
var dbTorrents = await _torrentRepository.Query().ToListAsync();
|
long totalSize = torrents.Sum(t => t.TotalSize);
|
||||||
|
|
||||||
// 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
|
// Calculate available space in download directory
|
||||||
string downloadDir = _configService.GetConfiguration().DownloadDirectory;
|
string downloadDir = _configService.GetConfiguration().DownloadDirectory;
|
||||||
@ -543,204 +106,56 @@ namespace TransmissionRssManager.Services
|
|||||||
|
|
||||||
return new Dictionary<string, long>
|
return new Dictionary<string, long>
|
||||||
{
|
{
|
||||||
["activeTorrentsSize"] = transmissionSize,
|
["activeTorrentsSize"] = totalSize,
|
||||||
["historicalTorrentsSize"] = historicalSize,
|
|
||||||
["totalTorrentsSize"] = transmissionSize + historicalSize,
|
|
||||||
["databaseSize"] = databaseSize,
|
|
||||||
["availableSpace"] = availableSpace
|
["availableSpace"] = availableSpace
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error estimating disk usage");
|
_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>
|
return new Dictionary<string, long>
|
||||||
{
|
{
|
||||||
["activeTorrentsSize"] = 0,
|
["activeTorrentsSize"] = 0,
|
||||||
["historicalTorrentsSize"] = 0,
|
|
||||||
["totalTorrentsSize"] = 0,
|
|
||||||
["databaseSize"] = 0,
|
|
||||||
["availableSpace"] = 0
|
["availableSpace"] = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<string, double>> GetPerformanceMetricsAsync()
|
/// <summary>
|
||||||
|
/// Gets system status including Transmission connection state
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Dictionary<string, object>> GetSystemStatusAsync()
|
||||||
{
|
{
|
||||||
var metrics = new Dictionary<string, double>();
|
var config = _configService.GetConfiguration();
|
||||||
|
|
||||||
|
var status = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["TransmissionConnected"] = false,
|
||||||
|
["AutoDownloadEnabled"] = config.AutoDownloadEnabled,
|
||||||
|
["PostProcessingEnabled"] = config.PostProcessing.Enabled,
|
||||||
|
["CheckIntervalMinutes"] = config.CheckIntervalMinutes
|
||||||
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Calculate average time to complete downloads
|
// Try to connect to Transmission to check if it's available
|
||||||
var completedTorrents = await _torrentRepository.Query()
|
var torrents = await _transmissionClient.GetTorrentsAsync();
|
||||||
.Where(t => t.CompletedOn.HasValue)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (completedTorrents.Any())
|
status["TransmissionConnected"] = true;
|
||||||
{
|
status["TransmissionTorrentCount"] = torrents.Count;
|
||||||
var avgCompletionTimeMinutes = completedTorrents
|
|
||||||
.Where(t => t.AddedOn < t.CompletedOn)
|
|
||||||
.Average(t => (t.CompletedOn.Value - t.AddedOn).TotalMinutes);
|
|
||||||
|
|
||||||
metrics["AvgCompletionTimeMinutes"] = Math.Round(avgCompletionTimeMinutes, 2);
|
// Count torrents by status
|
||||||
}
|
status["ActiveTorrentCount"] = torrents.Count(t => t.Status == "Downloading");
|
||||||
else
|
status["CompletedTorrentCount"] = torrents.Count(t => t.IsFinished);
|
||||||
{
|
|
||||||
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error getting performance metrics");
|
_logger.LogError(ex, "Error getting system status");
|
||||||
return new Dictionary<string, double>
|
status["TransmissionConnected"] = false;
|
||||||
{
|
status["LastErrorMessage"] = ex.Message;
|
||||||
["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");
|
return status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user