feat: async session cleanup and fix live room scroll
This commit is contained in:
@@ -124,14 +124,109 @@ public sealed class RecordSessionService
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var sessionIds = request.SessionIds
|
||||
var result = await DeleteSessionsByIdsAsync(request.SessionIds, request.DeleteFiles, cancellationToken);
|
||||
|
||||
if (result.DeletedSessionIds.Count > 0)
|
||||
{
|
||||
await _systemLogService.WriteAsync(
|
||||
SystemLogLevel.Warning,
|
||||
"RecordSession",
|
||||
$"Deleted {result.DeletedSessionIds.Count} recording session(s).",
|
||||
detail: request.DeleteFiles
|
||||
? $"video-files={result.DeletedFilePaths.Count}; danmaku-files={result.DeletedDanmakuPaths.Count}; tasks={result.DeletedTaskIds.Count}"
|
||||
: $"video-files=0; danmaku-files=0; tasks={result.DeletedTaskIds.Count}",
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
return new DeleteCompletedRecordTasksResultDto
|
||||
{
|
||||
DeletedTaskIds = result.DeletedTaskIds,
|
||||
DeletedFilePaths = result.DeletedFilePaths,
|
||||
DeletedDanmakuPaths = result.DeletedDanmakuPaths,
|
||||
DeletedSessionIds = result.DeletedSessionIds,
|
||||
Warnings = result.Warnings
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DeleteCompletedRecordTasksResultDto> DeleteMissingFilesAsync(
|
||||
DeleteMissingFileRecordSessionsRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
await _stoppedOrphanRecordSessionCleanupService.CleanupAsync(cancellationToken: cancellationToken);
|
||||
await ReconcileActiveSessionsAsync(null, cancellationToken);
|
||||
|
||||
var sessions = await _recordSessionRepository.ListAsync(null, cancellationToken);
|
||||
var missingFileSessionIds = sessions
|
||||
.Where(CanDeleteMissingFileSession)
|
||||
.Select(static item => item.Id)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (missingFileSessionIds.Length == 0)
|
||||
{
|
||||
return RecordService.CreateEmptyDeleteResult();
|
||||
}
|
||||
|
||||
var result = await DeleteSessionsByIdsAsync(missingFileSessionIds, request.DeleteFiles, cancellationToken);
|
||||
return new DeleteCompletedRecordTasksResultDto
|
||||
{
|
||||
DeletedTaskIds = result.DeletedTaskIds,
|
||||
DeletedFilePaths = result.DeletedFilePaths,
|
||||
DeletedDanmakuPaths = result.DeletedDanmakuPaths,
|
||||
DeletedSessionIds = result.DeletedSessionIds,
|
||||
Warnings = result.Warnings
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DeleteCompletedRecordTasksResultDto> DeleteEmptyAsync(
|
||||
bool deleteFiles,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _stoppedOrphanRecordSessionCleanupService.CleanupAsync(cancellationToken: cancellationToken);
|
||||
await ReconcileActiveSessionsAsync(null, cancellationToken);
|
||||
|
||||
var emptySessionIds = await _systemLogRepository.ListSessionIdsWithoutTasksAsync(cancellationToken);
|
||||
|
||||
if (emptySessionIds.Count == 0)
|
||||
{
|
||||
return RecordService.CreateEmptyDeleteResult();
|
||||
}
|
||||
|
||||
var result = await DeleteSessionsByIdsAsync(emptySessionIds, deleteFiles, cancellationToken);
|
||||
return new DeleteCompletedRecordTasksResultDto
|
||||
{
|
||||
DeletedTaskIds = result.DeletedTaskIds,
|
||||
DeletedFilePaths = result.DeletedFilePaths,
|
||||
DeletedDanmakuPaths = result.DeletedDanmakuPaths,
|
||||
DeletedSessionIds = result.DeletedSessionIds,
|
||||
Warnings = result.Warnings
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<RecordSessionDeletionBatchResult> DeleteSessionsByIdsAsync(
|
||||
IReadOnlyCollection<Guid> requestedSessionIds,
|
||||
bool deleteFiles,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sessionIds = requestedSessionIds
|
||||
.Where(static item => item != Guid.Empty)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (sessionIds.Length == 0)
|
||||
{
|
||||
return RecordService.CreateEmptyDeleteResult();
|
||||
return new RecordSessionDeletionBatchResult
|
||||
{
|
||||
DeletedSessionIds = [],
|
||||
DeletedTaskIds = [],
|
||||
DeletedResultCount = 0,
|
||||
DeletedLogCount = 0,
|
||||
DeletedFilePaths = [],
|
||||
DeletedDanmakuPaths = [],
|
||||
Warnings = []
|
||||
};
|
||||
}
|
||||
|
||||
var warnings = new List<string>();
|
||||
@@ -139,6 +234,8 @@ public sealed class RecordSessionService
|
||||
var deletedTaskIds = new List<Guid>();
|
||||
var deletedFilePaths = new List<string>();
|
||||
var deletedDanmakuPaths = new List<string>();
|
||||
var deletedResultCount = 0;
|
||||
var deletedLogCount = 0;
|
||||
|
||||
foreach (var sessionId in sessionIds)
|
||||
{
|
||||
@@ -197,7 +294,7 @@ public sealed class RecordSessionService
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (request.DeleteFiles)
|
||||
if (deleteFiles)
|
||||
{
|
||||
foreach (var recordTask in session.RecordTasks)
|
||||
{
|
||||
@@ -208,6 +305,7 @@ public sealed class RecordSessionService
|
||||
var relatedLogs = await ListRelatedLogsAsync(session.Id, taskIds, cancellationToken);
|
||||
if (relatedLogs.Count > 0)
|
||||
{
|
||||
deletedLogCount += relatedLogs.Count;
|
||||
_systemLogRepository.RemoveRange(relatedLogs);
|
||||
}
|
||||
|
||||
@@ -217,6 +315,7 @@ public sealed class RecordSessionService
|
||||
.ToArray();
|
||||
if (results.Length > 0)
|
||||
{
|
||||
deletedResultCount += results.Length;
|
||||
_recordResultRepository.RemoveRange(results);
|
||||
}
|
||||
|
||||
@@ -231,81 +330,18 @@ public sealed class RecordSessionService
|
||||
deletedSessionIds.Add(session.Id);
|
||||
}
|
||||
|
||||
if (deletedSessionIds.Count > 0)
|
||||
{
|
||||
await _systemLogService.WriteAsync(
|
||||
SystemLogLevel.Warning,
|
||||
"RecordSession",
|
||||
$"Deleted {deletedSessionIds.Count} recording session(s).",
|
||||
detail: request.DeleteFiles
|
||||
? $"video-files={deletedFilePaths.Count}; danmaku-files={deletedDanmakuPaths.Count}; tasks={deletedTaskIds.Count}"
|
||||
: $"video-files=0; danmaku-files=0; tasks={deletedTaskIds.Count}",
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
return new DeleteCompletedRecordTasksResultDto
|
||||
return new RecordSessionDeletionBatchResult
|
||||
{
|
||||
DeletedSessionIds = deletedSessionIds,
|
||||
DeletedTaskIds = deletedTaskIds,
|
||||
DeletedResultCount = deletedResultCount,
|
||||
DeletedLogCount = deletedLogCount,
|
||||
DeletedFilePaths = deletedFilePaths,
|
||||
DeletedDanmakuPaths = deletedDanmakuPaths,
|
||||
DeletedSessionIds = deletedSessionIds,
|
||||
Warnings = warnings
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DeleteCompletedRecordTasksResultDto> DeleteMissingFilesAsync(
|
||||
DeleteMissingFileRecordSessionsRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
await _stoppedOrphanRecordSessionCleanupService.CleanupAsync(cancellationToken: cancellationToken);
|
||||
await ReconcileActiveSessionsAsync(null, cancellationToken);
|
||||
|
||||
var sessions = await _recordSessionRepository.ListAsync(null, cancellationToken);
|
||||
var missingFileSessionIds = sessions
|
||||
.Where(CanDeleteMissingFileSession)
|
||||
.Select(static item => item.Id)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (missingFileSessionIds.Length == 0)
|
||||
{
|
||||
return RecordService.CreateEmptyDeleteResult();
|
||||
}
|
||||
|
||||
return await DeleteAsync(
|
||||
new DeleteRecordSessionsRequest
|
||||
{
|
||||
SessionIds = missingFileSessionIds,
|
||||
DeleteFiles = request.DeleteFiles
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<DeleteCompletedRecordTasksResultDto> DeleteEmptyAsync(
|
||||
bool deleteFiles,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _stoppedOrphanRecordSessionCleanupService.CleanupAsync(cancellationToken: cancellationToken);
|
||||
await ReconcileActiveSessionsAsync(null, cancellationToken);
|
||||
|
||||
var emptySessionIds = await _systemLogRepository.ListSessionIdsWithoutTasksAsync(cancellationToken);
|
||||
|
||||
if (emptySessionIds.Count == 0)
|
||||
{
|
||||
return RecordService.CreateEmptyDeleteResult();
|
||||
}
|
||||
|
||||
return await DeleteAsync(
|
||||
new DeleteRecordSessionsRequest
|
||||
{
|
||||
SessionIds = emptySessionIds.ToArray(),
|
||||
DeleteFiles = deleteFiles
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task ReconcileActiveSessionsAsync(Guid? liveRoomId, CancellationToken cancellationToken)
|
||||
{
|
||||
var sessions = await _recordSessionRepository.ListAsync(liveRoomId, cancellationToken);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using LiveRecorder.Application.Abstractions.Persistence;
|
||||
using LiveRecorder.Application.Abstractions.Settings;
|
||||
using LiveRecorder.Application.Models.Cleanup;
|
||||
using LiveRecorder.Application.Models.Settings;
|
||||
using LiveRecorder.Domain.Entities;
|
||||
using LiveRecorder.Domain.Enums;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LiveRecorder.Application.Services;
|
||||
|
||||
@@ -71,6 +73,8 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
private const string EnableRetentionCleanupKey = "retention.cleanup.enabled";
|
||||
private const string RetentionDaysKey = "retention.cleanup.days";
|
||||
private const string RetentionDeleteFilesKey = "retention.cleanup.delete_files";
|
||||
private const string RetentionVideoFileConditionKey = "retention.cleanup.video_file_condition";
|
||||
private const string RetentionTaskStatusesKey = "retention.cleanup.task_statuses";
|
||||
private const string EnableEmailNotificationKey = "notification.email.enabled";
|
||||
private const string EmailSmtpHostKey = "notification.email.smtp_host";
|
||||
private const string EmailSmtpPortKey = "notification.email.smtp_port";
|
||||
@@ -214,6 +218,8 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
EnableRetentionCleanup = bool.TryParse(GetValue(lookup, EnableRetentionCleanupKey, "false"), out var enableRetentionCleanup) && enableRetentionCleanup,
|
||||
RetentionDays = GetIntValue(lookup, RetentionDaysKey, 30, 1, 3650),
|
||||
RetentionDeleteFiles = bool.TryParse(GetValue(lookup, RetentionDeleteFilesKey, "false"), out var retentionDeleteFiles) && retentionDeleteFiles,
|
||||
RetentionVideoFileCondition = NormalizeCleanupVideoFileCondition(GetValue(lookup, RetentionVideoFileConditionKey, CleanupVideoFileConditions.Any)),
|
||||
RetentionTaskStatuses = GetIntListValue(lookup, RetentionTaskStatusesKey),
|
||||
EnableEmailNotification = bool.TryParse(GetValue(lookup, EnableEmailNotificationKey, "false"), out var enableEmailNotification) && enableEmailNotification,
|
||||
EmailSmtpHost = GetValue(lookup, EmailSmtpHostKey, string.Empty),
|
||||
EmailSmtpPort = GetIntValue(lookup, EmailSmtpPortKey, 587, 1, 65535),
|
||||
@@ -368,6 +374,8 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
await UpsertAsync(EnableRetentionCleanupKey, request.EnableRetentionCleanup.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(RetentionDaysKey, Math.Clamp(request.RetentionDays, 1, 3650).ToString(), now, cancellationToken);
|
||||
await UpsertAsync(RetentionDeleteFilesKey, request.RetentionDeleteFiles.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(RetentionVideoFileConditionKey, NormalizeCleanupVideoFileCondition(request.RetentionVideoFileCondition), now, cancellationToken);
|
||||
await UpsertAsync(RetentionTaskStatusesKey, SerializeIntList(request.RetentionTaskStatuses), now, cancellationToken);
|
||||
await UpsertAsync(EnableEmailNotificationKey, request.EnableEmailNotification.ToString(), now, cancellationToken);
|
||||
await UpsertAsync(EmailSmtpHostKey, request.EmailSmtpHost.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(EmailSmtpPortKey, request.EmailSmtpPort.ToString(), now, cancellationToken);
|
||||
@@ -435,6 +443,36 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
return Math.Clamp(parsedValue, minimum, maximum);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<int> GetIntListValue(IReadOnlyDictionary<string, string> lookup, string key)
|
||||
{
|
||||
if (!lookup.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<List<int>>(raw);
|
||||
return parsed is null
|
||||
? []
|
||||
: parsed
|
||||
.Distinct()
|
||||
.OrderBy(static item => item)
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return raw
|
||||
.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(static item => int.TryParse(item, out var value) ? value : (int?)null)
|
||||
.Where(static item => item.HasValue)
|
||||
.Select(static item => item!.Value)
|
||||
.Distinct()
|
||||
.OrderBy(static item => item)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeEventScriptMode(string? value) =>
|
||||
string.Equals(value?.Trim(), EventScriptSourceModes.Inline, StringComparison.OrdinalIgnoreCase)
|
||||
? EventScriptSourceModes.Inline
|
||||
@@ -445,6 +483,21 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
.Replace("Detected At (UTC)", "Detected At (Beijing Time)", StringComparison.Ordinal)
|
||||
.Replace("Occurred At (UTC)", "Occurred At (Beijing Time)", StringComparison.Ordinal);
|
||||
|
||||
private static string NormalizeCleanupVideoFileCondition(string? value) =>
|
||||
value?.Trim() switch
|
||||
{
|
||||
var item when string.Equals(item, CleanupVideoFileConditions.AllMissing, StringComparison.OrdinalIgnoreCase) => CleanupVideoFileConditions.AllMissing,
|
||||
var item when string.Equals(item, CleanupVideoFileConditions.AllPresent, StringComparison.OrdinalIgnoreCase) => CleanupVideoFileConditions.AllPresent,
|
||||
_ => CleanupVideoFileConditions.Any
|
||||
};
|
||||
|
||||
private static string SerializeIntList(IReadOnlyList<int>? values) =>
|
||||
JsonSerializer.Serialize(
|
||||
(values ?? [])
|
||||
.Distinct()
|
||||
.OrderBy(static item => item)
|
||||
.ToArray());
|
||||
|
||||
private async Task UpsertAsync(string key, string value, DateTimeOffset updatedAt, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await _appSettingRepository.GetByKeyAsync(key, cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user