feat: improve polling recovery and session cleanup

This commit is contained in:
2026-04-27 20:33:12 +08:00
parent ca2c559555
commit 89ba4163fc
22 changed files with 442 additions and 75 deletions
@@ -774,6 +774,7 @@ public sealed class RecordService
RecordSaveMode saveMode,
DateTimeOffset now)
{
var localNow = ChinaTime.ToBeijingTime(now);
var safeRoomId = SanitizeFileName(roomId, "room");
var effectiveFileNameTemplate = EnsureSegmentSuffixTemplate(outputFileNameTemplate, saveMode);
var baseFileStem = BuildFileNameStem(
@@ -782,7 +783,7 @@ public sealed class RecordService
safeRoomId,
anchorName,
title,
now,
localNow,
segmentSuffix: string.Empty);
var directoryPath = BuildDirectoryPath(
outputDirectoryTemplate,
@@ -790,7 +791,7 @@ public sealed class RecordService
safeRoomId,
anchorName,
title,
now,
localNow,
baseFileStem);
var fileNameStem = BuildFileNameStem(
effectiveFileNameTemplate,
@@ -798,7 +799,7 @@ public sealed class RecordService
safeRoomId,
anchorName,
title,
now,
localNow,
saveMode == RecordSaveMode.Segmented ? "_%05d" : string.Empty);
var folder = Path.Combine(outputRoot, directoryPath);
var extension = outputFormat == RecordOutputFormat.Ts ? "ts" : "mp4";
@@ -253,6 +253,36 @@ public sealed class RecordSessionService
};
}
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);
}
private async Task ReconcileActiveSessionsAsync(Guid? liveRoomId, CancellationToken cancellationToken)
{
var sessions = await _recordSessionRepository.ListAsync(liveRoomId, cancellationToken);
@@ -271,6 +301,39 @@ public sealed class RecordSessionService
private static bool IsActiveStatus(RecordSessionStatus status) =>
status is RecordSessionStatus.Starting or RecordSessionStatus.Running or RecordSessionStatus.Stopping;
private static bool CanDeleteMissingFileSession(RecordSession session)
{
if (IsActiveStatus(session.Status))
{
return false;
}
if (session.RecordTasks.Count == 0)
{
return true;
}
return session.RecordTasks.All(static task => !HasExistingVideoFile(task));
}
private static bool HasExistingVideoFile(RecordTask task)
{
var candidatePath = !string.IsNullOrWhiteSpace(task.Result?.FilePath)
? task.Result!.FilePath
: task.OutputFilePath;
if (string.IsNullOrWhiteSpace(candidatePath))
{
return false;
}
var resolvedPath = Path.IsPathRooted(candidatePath)
? candidatePath
: Path.GetFullPath(candidatePath, AppContext.BaseDirectory);
return File.Exists(resolvedPath);
}
private async Task<IReadOnlyList<SystemLogEntry>> ListRelatedLogsAsync(
Guid recordSessionId,
IReadOnlyCollection<Guid> taskIds,
@@ -225,7 +225,7 @@ public sealed class SystemSettingsService : ISystemSettingsService
NotifyOnLiveStarted = bool.TryParse(GetValue(lookup, NotifyOnLiveStartedKey, "true"), out var notifyOnLiveStarted) && notifyOnLiveStarted,
NotifyOnException = bool.TryParse(GetValue(lookup, NotifyOnExceptionKey, "true"), out var notifyOnException) && notifyOnException,
EmailLiveStartedSubjectTemplate = GetValue(lookup, EmailLiveStartedSubjectTemplateKey, "[{{appName}}] Live started: {{anchor}} {{title}} ({{roomId}})"),
EmailLiveStartedBodyTemplateHtml = GetValue(
EmailLiveStartedBodyTemplateHtml = NormalizeBeijingTimeTemplate(GetValue(
lookup,
EmailLiveStartedBodyTemplateHtmlKey,
"""
@@ -237,13 +237,13 @@ public sealed class SystemSettingsService : ISystemSettingsService
<li><strong>Room ID:</strong> {{roomId}}</li>
<li><strong>Title:</strong> {{title}}</li>
<li><strong>Anchor:</strong> {{anchor}}</li>
<li><strong>Detected At (UTC):</strong> {{detectedAtUtc}}</li>
<li><strong>Detected At (Beijing Time):</strong> {{detectedAtUtc}}</li>
</ul>
<p><strong>Source URL:</strong> <a href="{{sourceUrl}}">{{sourceUrl}}</a></p>
</div>
"""),
""")),
EmailExceptionSubjectTemplate = GetValue(lookup, EmailExceptionSubjectTemplateKey, "[{{appName}}] Exception: {{source}}"),
EmailExceptionBodyTemplateHtml = GetValue(
EmailExceptionBodyTemplateHtml = NormalizeBeijingTimeTemplate(GetValue(
lookup,
EmailExceptionBodyTemplateHtmlKey,
"""
@@ -256,11 +256,11 @@ public sealed class SystemSettingsService : ISystemSettingsService
<li><strong>Room ID:</strong> {{roomId}}</li>
<li><strong>Record Task ID:</strong> {{recordTaskId}}</li>
<li><strong>Task Status:</strong> {{taskStatus}}</li>
<li><strong>Occurred At (UTC):</strong> {{occurredAtUtc}}</li>
<li><strong>Occurred At (Beijing Time):</strong> {{occurredAtUtc}}</li>
</ul>
<div style="margin-top: 16px; padding: 12px 14px; border-radius: 8px; background: #f5f5f5; white-space: pre-wrap;">{{detail}}</div>
</div>
"""),
""")),
EnableWebhookNotification = bool.TryParse(GetValue(lookup, EnableWebhookNotificationKey, "false"), out var enableWebhookNotification) && enableWebhookNotification,
WebhookUrl = GetValue(lookup, WebhookUrlKey, string.Empty),
WebhookHeaders = GetValue(lookup, WebhookHeadersKey, string.Empty),
@@ -437,6 +437,11 @@ public sealed class SystemSettingsService : ISystemSettingsService
? EventScriptSourceModes.Inline
: EventScriptSourceModes.Path;
private static string NormalizeBeijingTimeTemplate(string value) =>
value
.Replace("Detected At (UTC)", "Detected At (Beijing Time)", StringComparison.Ordinal)
.Replace("Occurred At (UTC)", "Occurred At (Beijing Time)", StringComparison.Ordinal);
private async Task UpsertAsync(string key, string value, DateTimeOffset updatedAt, CancellationToken cancellationToken)
{
var existing = await _appSettingRepository.GetByKeyAsync(key, cancellationToken);