feat: add dashboard, file preview metadata, and bandwidth statistics
Dashboard:
- Add DashboardDto, DashboardService with SQL-level aggregate queries
- Add GET /api/dashboard endpoint with real-time system status
- Add repository aggregate methods (CountByAvailability, SumDuration, etc.)
- Add DashboardView.vue as new landing page with KPI cards, storage status, recent sessions, top rooms
- Update router to make dashboard the new / route, add nav item in sidebar
File Preview:
- Add IVideoMetadataService + FfmpegVideoMetadataService for video metadata extraction
- Extend MediaBrowserItemDto with Metadata and ThumbnailUrl fields
- Add includeMetadata param to media browser API, add thumbnail endpoint
- Add SessionPlaylistDto and GET /api/record-sessions/{id}/playlist for continuous playback
Bandwidth Statistics:
- Add -progress pipe:1 to live recording ffmpeg args for bitrate output
- Parse total_size/bitrate/speed from ffmpeg progress lines during recording
- Write bandwidth samples as SystemLogEntry (Category=Bandwidth) every 30s
- Add BandwidthStatisticsService, BandwidthController with session timeline + daily summary
- Add bandwidth TypeScript types
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
This commit is contained in:
co-authored by
Claude Opus 4.8 noreply@anthropic.com
parent
a5c2cc3202
commit
cbee29bef9
@@ -0,0 +1,147 @@
|
||||
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;
|
||||
|
||||
public DashboardService(
|
||||
ILiveRoomRepository liveRoomRepository,
|
||||
IRecordSessionRepository recordSessionRepository,
|
||||
IRecordTaskRepository recordTaskRepository,
|
||||
IRecordResultRepository recordResultRepository,
|
||||
ISystemLogRepository systemLogRepository,
|
||||
ISystemSettingsService systemSettingsService,
|
||||
IStorageGuardService storageGuardService)
|
||||
{
|
||||
_liveRoomRepository = liveRoomRepository;
|
||||
_recordSessionRepository = recordSessionRepository;
|
||||
_recordTaskRepository = recordTaskRepository;
|
||||
_recordResultRepository = recordResultRepository;
|
||||
_systemLogRepository = systemLogRepository;
|
||||
_systemSettingsService = systemSettingsService;
|
||||
_storageGuardService = storageGuardService;
|
||||
}
|
||||
|
||||
public async Task<DashboardDto> GetDashboardAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var beijingNow = ChinaTime.ToBeijingTime(now);
|
||||
var todayBeijingDate = DateOnly.FromDateTime(beijingNow.DateTime);
|
||||
var todayUtcStart = new DateTimeOffset(todayBeijingDate.ToDateTime(TimeOnly.MinValue), ChinaTime.Zone.GetUtcOffset(beijingNow.DateTime));
|
||||
var todayUtcEnd = new DateTimeOffset(todayBeijingDate.ToDateTime(TimeOnly.MaxValue), ChinaTime.Zone.GetUtcOffset(beijingNow.DateTime));
|
||||
var recentErrorSince = now.AddHours(-24);
|
||||
|
||||
// Run independent queries in parallel for efficiency
|
||||
var activeRecordingTask = _recordSessionRepository.CountByStatusAsync(RecordSessionStatus.Running, cancellationToken);
|
||||
var liveRoomCountTask = _liveRoomRepository.CountByAvailabilityAsync(LiveRoomAvailabilityStatus.Live, cancellationToken);
|
||||
var offlineRoomCountTask = _liveRoomRepository.CountByAvailabilityAsync(LiveRoomAvailabilityStatus.Offline, cancellationToken);
|
||||
var totalRoomCountTask = _liveRoomRepository.CountAsync(cancellationToken);
|
||||
var activeSessionCountTask = _recordSessionRepository.CountActiveAsync(cancellationToken);
|
||||
var recentErrorCountTask = _systemLogRepository.CountRecentErrorsAsync(recentErrorSince, cancellationToken);
|
||||
var todayDurationTask = _recordTaskRepository.SumDurationSecondsAsync(todayUtcStart, todayUtcEnd, cancellationToken);
|
||||
var todayAggregateTask = _recordResultRepository.GetTodayAggregateAsync(todayUtcStart, todayUtcEnd, cancellationToken);
|
||||
var recentSessionsTask = _recordSessionRepository.ListRecentAsync(5, cancellationToken);
|
||||
var todaySessionsTask = _recordSessionRepository.ListInDateRangeAsync(todayUtcStart, todayUtcEnd, cancellationToken);
|
||||
var settingsTask = _systemSettingsService.GetAsync(cancellationToken);
|
||||
|
||||
await Task.WhenAll(
|
||||
activeRecordingTask,
|
||||
liveRoomCountTask,
|
||||
offlineRoomCountTask,
|
||||
totalRoomCountTask,
|
||||
activeSessionCountTask,
|
||||
recentErrorCountTask,
|
||||
todayDurationTask,
|
||||
todayAggregateTask,
|
||||
recentSessionsTask,
|
||||
todaySessionsTask,
|
||||
(Task)settingsTask
|
||||
);
|
||||
|
||||
var (todayTotalBytes, todayTotalDanmaku) = todayAggregateTask.Result;
|
||||
var settings = settingsTask.Result;
|
||||
var storageCheck = _storageGuardService.CheckCanStartOrResume(settings);
|
||||
|
||||
return new DashboardDto
|
||||
{
|
||||
ActiveRecordingCount = activeRecordingTask.Result,
|
||||
LiveRoomCount = liveRoomCountTask.Result,
|
||||
OfflineRoomCount = offlineRoomCountTask.Result,
|
||||
TotalRoomCount = totalRoomCountTask.Result,
|
||||
TodayRecordingSeconds = todayDurationTask.Result,
|
||||
TodayDataBytes = todayTotalBytes,
|
||||
TodayDanmakuCount = todayTotalDanmaku,
|
||||
ActiveSessionCount = activeSessionCountTask.Result,
|
||||
RecentErrorCount = recentErrorCountTask.Result,
|
||||
StorageStatus = new StorageStatusDto
|
||||
{
|
||||
HasEnoughSpace = storageCheck.HasEnoughSpace,
|
||||
Message = storageCheck.Message ?? string.Empty,
|
||||
AvailableBytes = storageCheck.AvailableBytes
|
||||
},
|
||||
RecentSessions = recentSessionsTask.Result
|
||||
.Select(MapRecentSession)
|
||||
.ToList(),
|
||||
TopRooms = ComputeTopRooms(todaySessionsTask.Result)
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user