161 lines
8.6 KiB
C#
161 lines
8.6 KiB
C#
using LiveRecorder.Application.Abstractions.Persistence;
|
|
using LiveRecorder.Application.Abstractions.Settings;
|
|
using LiveRecorder.Application.Abstractions.Storage;
|
|
using LiveRecorder.Application.Models.Reports;
|
|
using LiveRecorder.Application.Common;
|
|
using LiveRecorder.Domain.Enums;
|
|
|
|
namespace LiveRecorder.Application.Services;
|
|
|
|
public sealed class DashboardService
|
|
{
|
|
private readonly ILiveRoomRepository _liveRoomRepository;
|
|
private readonly IRecordSessionRepository _recordSessionRepository;
|
|
private readonly IRecordTaskRepository _recordTaskRepository;
|
|
private readonly IRecordResultRepository _recordResultRepository;
|
|
private readonly ISystemLogRepository _systemLogRepository;
|
|
private readonly ISystemSettingsService _systemSettingsService;
|
|
private readonly IStorageGuardService _storageGuardService;
|
|
private readonly IOperationsMetricsRepository _operationsMetricsRepository;
|
|
|
|
public DashboardService(
|
|
ILiveRoomRepository liveRoomRepository,
|
|
IRecordSessionRepository recordSessionRepository,
|
|
IRecordTaskRepository recordTaskRepository,
|
|
IRecordResultRepository recordResultRepository,
|
|
ISystemLogRepository systemLogRepository,
|
|
ISystemSettingsService systemSettingsService,
|
|
IStorageGuardService storageGuardService,
|
|
IOperationsMetricsRepository operationsMetricsRepository)
|
|
{
|
|
_liveRoomRepository = liveRoomRepository;
|
|
_recordSessionRepository = recordSessionRepository;
|
|
_recordTaskRepository = recordTaskRepository;
|
|
_recordResultRepository = recordResultRepository;
|
|
_systemLogRepository = systemLogRepository;
|
|
_systemSettingsService = systemSettingsService;
|
|
_storageGuardService = storageGuardService;
|
|
_operationsMetricsRepository = operationsMetricsRepository;
|
|
}
|
|
|
|
public async Task<DashboardDto> GetDashboardAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
var beijingNow = ChinaTime.ToBeijingTime(now);
|
|
var todayBeijingDate = DateOnly.FromDateTime(beijingNow.DateTime);
|
|
// Bound "today in Beijing" but normalize to UTC — Npgsql only accepts offset 0
|
|
// when writing to a `timestamp with time zone` column.
|
|
var todayUtcStart = new DateTimeOffset(todayBeijingDate.ToDateTime(TimeOnly.MinValue), ChinaTime.Zone.GetUtcOffset(beijingNow.DateTime)).ToUniversalTime();
|
|
var todayUtcEnd = new DateTimeOffset(todayBeijingDate.ToDateTime(TimeOnly.MaxValue), ChinaTime.Zone.GetUtcOffset(beijingNow.DateTime)).ToUniversalTime();
|
|
var recentErrorSince = now.AddHours(-24);
|
|
var currentErrorSince = now.AddMinutes(-30);
|
|
|
|
// Run queries sequentially — DbContext is not thread-safe
|
|
var activeRecordingCount = await _recordSessionRepository.CountByStatusAsync(RecordSessionStatus.Running, cancellationToken);
|
|
var liveRoomCount = await _liveRoomRepository.CountByAvailabilityAsync(LiveRoomAvailabilityStatus.Live, cancellationToken);
|
|
var offlineRoomCount = await _liveRoomRepository.CountByAvailabilityAsync(LiveRoomAvailabilityStatus.Offline, cancellationToken);
|
|
var totalRoomCount = await _liveRoomRepository.CountAsync(cancellationToken);
|
|
var activeSessionCount = await _recordSessionRepository.CountActiveAsync(cancellationToken);
|
|
var recentErrorCount = await _systemLogRepository.CountRecentErrorsAsync(recentErrorSince, cancellationToken);
|
|
var currentErrorCount = await _systemLogRepository.CountRecentErrorsAsync(currentErrorSince, cancellationToken);
|
|
var todayRecordingSeconds = await _recordTaskRepository.SumDurationSecondsAsync(todayUtcStart, todayUtcEnd, cancellationToken);
|
|
var (todayTotalBytes, todayTotalDanmaku) = await _recordResultRepository.GetTodayAggregateAsync(todayUtcStart, todayUtcEnd, cancellationToken);
|
|
var recentSessions = await _recordSessionRepository.ListRecentAsync(5, cancellationToken);
|
|
var todaySessions = await _recordSessionRepository.ListInDateRangeAsync(todayUtcStart, todayUtcEnd, cancellationToken);
|
|
var settings = await _systemSettingsService.GetAsync(cancellationToken);
|
|
var storageCheck = _storageGuardService.CheckCanStartOrResume(settings);
|
|
var pendingTranscodeCount = await _recordTaskRepository.CountByStatusAsync(RecordTaskStatus.Processing, cancellationToken);
|
|
var pendingUploadCount = await _recordResultRepository.CountPendingUploadAsync(cancellationToken);
|
|
var queuedDataBytes = await _recordResultRepository.SumPendingUploadBytesAsync(cancellationToken);
|
|
var operationsMetrics = await _operationsMetricsRepository.GetAsync(cancellationToken);
|
|
|
|
return new DashboardDto
|
|
{
|
|
ActiveRecordingCount = activeRecordingCount,
|
|
LiveRoomCount = liveRoomCount,
|
|
OfflineRoomCount = offlineRoomCount,
|
|
TotalRoomCount = totalRoomCount,
|
|
TodayRecordingSeconds = todayRecordingSeconds,
|
|
TodayDataBytes = todayTotalBytes,
|
|
TodayDanmakuCount = todayTotalDanmaku,
|
|
ActiveSessionCount = activeSessionCount,
|
|
RecentErrorCount = recentErrorCount,
|
|
CurrentErrorCount = currentErrorCount,
|
|
StorageStatus = new StorageStatusDto
|
|
{
|
|
IsEnabled = storageCheck.IsEnabled,
|
|
IsAvailable = storageCheck.IsAvailable,
|
|
HasEnoughSpace = storageCheck.HasEnoughSpace,
|
|
Message = storageCheck.Message ?? string.Empty,
|
|
CheckedPath = storageCheck.CheckedPath,
|
|
TotalBytes = storageCheck.TotalBytes,
|
|
UsedBytes = storageCheck.UsedBytes,
|
|
AvailableBytes = storageCheck.AvailableBytes,
|
|
RequiredBytes = storageCheck.RequiredBytes,
|
|
Tier = storageCheck.Tier.ToString(),
|
|
UsagePercent = storageCheck.UsagePercent,
|
|
FreePercent = storageCheck.FreePercent,
|
|
GreenThresholdPercent = storageCheck.GreenThresholdPercent,
|
|
RedThresholdPercent = storageCheck.RedThresholdPercent
|
|
},
|
|
PendingTranscodeCount = pendingTranscodeCount,
|
|
PendingUploadCount = pendingUploadCount,
|
|
QueuedDataBytes = queuedDataBytes,
|
|
OldestTranscodeUpdatedAt = operationsMetrics.OldestTranscodeUpdatedAt,
|
|
OldestUploadProgressAt = operationsMetrics.OldestUploadProgressAt,
|
|
StalledUploadCount = operationsMetrics.StalledUploadCount,
|
|
UploadCleanupFailureCount = operationsMetrics.CleanupFailureCount,
|
|
RecentSessions = recentSessions
|
|
.Select(MapRecentSession)
|
|
.ToList(),
|
|
TopRooms = ComputeTopRooms(todaySessions)
|
|
};
|
|
}
|
|
|
|
private static RecentSessionItemDto MapRecentSession(Domain.Entities.RecordSession session)
|
|
{
|
|
var duration = session.RecordTasks
|
|
.Where(item => item.DurationSeconds.HasValue)
|
|
.Sum(item => item.DurationSeconds ?? 0);
|
|
|
|
return new RecentSessionItemDto
|
|
{
|
|
Id = session.Id,
|
|
LiveRoomId = session.LiveRoomId,
|
|
LiveRoomTitle = session.LiveRoom?.Title ?? session.LiveRoom?.Alias ?? session.LiveRoom?.AnchorName ?? "-",
|
|
PlatformName = session.LiveRoom?.Platform.ToString() ?? "-",
|
|
SegmentCount = session.SegmentCount,
|
|
Status = (int)session.Status,
|
|
StartedAt = session.StartedAt ?? session.CreatedAt,
|
|
DurationSeconds = duration > 0 ? duration : null
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyList<TopRoomItemDto> ComputeTopRooms(IReadOnlyCollection<Domain.Entities.RecordSession> sessions)
|
|
{
|
|
return sessions
|
|
.GroupBy(item => item.LiveRoomId)
|
|
.Select(group =>
|
|
{
|
|
var first = group.First();
|
|
var totalDuration = group
|
|
.SelectMany(item => item.RecordTasks)
|
|
.Sum(item => item.DurationSeconds ?? 0);
|
|
|
|
return new TopRoomItemDto
|
|
{
|
|
LiveRoomId = group.Key,
|
|
Title = first.LiveRoom?.Title ?? first.LiveRoom?.Alias ?? first.LiveRoom?.AnchorName,
|
|
AnchorName = first.LiveRoom?.AnchorName,
|
|
PlatformName = first.LiveRoom?.Platform.ToString() ?? "-",
|
|
RoomId = first.LiveRoom?.RoomId ?? "-",
|
|
SessionCount = group.Count(),
|
|
TotalDurationSeconds = totalDuration
|
|
};
|
|
})
|
|
.OrderByDescending(item => item.TotalDurationSeconds)
|
|
.Take(5)
|
|
.ToList();
|
|
}
|
|
}
|