feat: improve recording automation and task workflows

This commit is contained in:
2026-04-23 23:18:11 +08:00
parent 1c892259a9
commit 23ead56781
88 changed files with 9579 additions and 1581 deletions
@@ -0,0 +1,278 @@
using System.Diagnostics;
using LiveRecorder.Application.Abstractions.Logging;
using LiveRecorder.Application.Abstractions.Scripting;
using LiveRecorder.Application.Abstractions.Settings;
using LiveRecorder.Domain.Entities;
using LiveRecorder.Domain.Enums;
using Microsoft.Extensions.Logging;
namespace LiveRecorder.Infrastructure.Services;
public sealed class EventScriptService : IEventScriptService
{
private readonly ISystemSettingsService _settingsService;
private readonly ISystemLogService _systemLogService;
private readonly ILogger<EventScriptService> _logger;
public EventScriptService(
ISystemSettingsService settingsService,
ISystemLogService systemLogService,
ILogger<EventScriptService> logger)
{
_settingsService = settingsService;
_systemLogService = systemLogService;
_logger = logger;
}
public async Task RunLiveStartedAsync(LiveRoom liveRoom, DateTimeOffset occurredAt, CancellationToken cancellationToken = default)
{
var settings = await _settingsService.GetAsync(cancellationToken);
var environment = BuildLiveRoomEnvironment(liveRoom, occurredAt);
environment["LIVE_RECORDER_EVENT"] = "live_started";
await RunAsync(
settings.EnableEventScripts,
settings.LiveStartedScriptPath,
settings.EventScriptTimeoutSeconds,
"live_started",
environment,
liveRoom.Id,
recordSessionId: null,
recordTaskId: null,
cancellationToken);
}
public async Task RunLiveEndedAsync(LiveRoom liveRoom, DateTimeOffset occurredAt, CancellationToken cancellationToken = default)
{
var settings = await _settingsService.GetAsync(cancellationToken);
var environment = BuildLiveRoomEnvironment(liveRoom, occurredAt);
environment["LIVE_RECORDER_EVENT"] = "live_ended";
await RunAsync(
settings.EnableEventScripts,
settings.LiveEndedScriptPath,
settings.EventScriptTimeoutSeconds,
"live_ended",
environment,
liveRoom.Id,
recordSessionId: null,
recordTaskId: null,
cancellationToken);
}
public async Task RunSegmentCompletedAsync(
LiveRoom? liveRoom,
RecordSession recordSession,
RecordTask recordTask,
RecordResult? recordResult,
string segmentFilePath,
DateTimeOffset occurredAt,
CancellationToken cancellationToken = default)
{
var settings = await _settingsService.GetAsync(cancellationToken);
var environment = BuildLiveRoomEnvironment(liveRoom, occurredAt);
environment["LIVE_RECORDER_EVENT"] = "segment_completed";
environment["LIVE_RECORDER_RECORD_SESSION_ID"] = recordSession.Id.ToString();
environment["LIVE_RECORDER_RECORD_TASK_ID"] = recordTask.Id.ToString();
environment["LIVE_RECORDER_SEGMENT_INDEX"] = recordTask.SegmentIndex.ToString();
environment["LIVE_RECORDER_SEGMENT_FILE_PATH"] = NormalizePath(segmentFilePath);
environment["LIVE_RECORDER_DANMAKU_FILE_PATH"] = NormalizePath(recordResult?.DanmakuFilePath);
environment["LIVE_RECORDER_DURATION_SECONDS"] = recordResult?.DurationSeconds?.ToString("0.###") ?? recordTask.DurationSeconds?.ToString("0.###") ?? string.Empty;
environment["LIVE_RECORDER_FILE_SIZE_BYTES"] = recordResult?.FileSizeBytes?.ToString() ?? TryGetFileSize(segmentFilePath);
environment["LIVE_RECORDER_TASK_STATUS"] = recordTask.Status.ToString();
environment["LIVE_RECORDER_SESSION_STATUS"] = recordSession.Status.ToString();
await RunAsync(
settings.EnableEventScripts,
settings.SegmentCompletedScriptPath,
settings.EventScriptTimeoutSeconds,
"segment_completed",
environment,
liveRoom?.Id ?? recordSession.LiveRoomId,
recordSession.Id,
recordTask.Id,
cancellationToken);
}
private async Task RunAsync(
bool enabled,
string scriptPath,
int timeoutSeconds,
string eventName,
IReadOnlyDictionary<string, string> environment,
Guid? liveRoomId,
Guid? recordSessionId,
Guid? recordTaskId,
CancellationToken cancellationToken)
{
if (!enabled || string.IsNullOrWhiteSpace(scriptPath))
{
return;
}
var normalizedScriptPath = NormalizePath(scriptPath);
if (!File.Exists(normalizedScriptPath))
{
await _systemLogService.WriteAsync(
SystemLogLevel.Warning,
"Script",
$"Event script was not found for {eventName}.",
normalizedScriptPath,
liveRoomId,
recordSessionId,
recordTaskId,
cancellationToken);
return;
}
var startInfo = CreateStartInfo(normalizedScriptPath);
foreach (var pair in environment)
{
startInfo.Environment[pair.Key] = pair.Value;
}
using var process = new Process
{
StartInfo = startInfo,
EnableRaisingEvents = true
};
try
{
process.Start();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(timeoutSeconds, 1, 3600)));
await process.WaitForExitAsync(timeoutCts.Token);
await _systemLogService.WriteAsync(
process.ExitCode == 0 ? SystemLogLevel.Info : SystemLogLevel.Warning,
"Script",
process.ExitCode == 0
? $"Event script completed for {eventName}."
: $"Event script exited with code {process.ExitCode} for {eventName}.",
normalizedScriptPath,
liveRoomId,
recordSessionId,
recordTaskId,
cancellationToken);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
TryKill(process);
await _systemLogService.WriteAsync(
SystemLogLevel.Warning,
"Script",
$"Event script timed out for {eventName}.",
normalizedScriptPath,
liveRoomId,
recordSessionId,
recordTaskId,
CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Event script failed for {EventName}", eventName);
await _systemLogService.WriteAsync(
SystemLogLevel.Warning,
"Script",
$"Event script failed for {eventName}.",
ex.ToString(),
liveRoomId,
recordSessionId,
recordTaskId,
cancellationToken);
}
}
private static Dictionary<string, string> BuildLiveRoomEnvironment(LiveRoom? liveRoom, DateTimeOffset occurredAt)
{
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["LIVE_RECORDER_EVENT"] = string.Empty,
["LIVE_RECORDER_PLATFORM"] = liveRoom?.Platform.ToString() ?? string.Empty,
["LIVE_RECORDER_LIVE_ROOM_ID"] = liveRoom?.Id.ToString() ?? string.Empty,
["LIVE_RECORDER_ROOM_ID"] = liveRoom?.RoomId ?? string.Empty,
["LIVE_RECORDER_TITLE"] = liveRoom?.Title ?? string.Empty,
["LIVE_RECORDER_ANCHOR"] = liveRoom?.AnchorName ?? string.Empty,
["LIVE_RECORDER_SOURCE_URL"] = liveRoom?.SourceUrl ?? liveRoom?.NormalizedUrl ?? string.Empty,
["LIVE_RECORDER_OCCURRED_AT_UTC"] = occurredAt.ToString("O")
};
}
private static ProcessStartInfo CreateStartInfo(string scriptPath)
{
var extension = Path.GetExtension(scriptPath);
var startInfo = new ProcessStartInfo
{
UseShellExecute = false,
RedirectStandardError = false,
RedirectStandardOutput = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(scriptPath) ?? AppContext.BaseDirectory
};
if (OperatingSystem.IsWindows() && extension.Equals(".ps1", StringComparison.OrdinalIgnoreCase))
{
startInfo.FileName = "powershell";
startInfo.ArgumentList.Add("-NoProfile");
startInfo.ArgumentList.Add("-ExecutionPolicy");
startInfo.ArgumentList.Add("Bypass");
startInfo.ArgumentList.Add("-File");
startInfo.ArgumentList.Add(scriptPath);
return startInfo;
}
if (OperatingSystem.IsWindows() && (extension.Equals(".bat", StringComparison.OrdinalIgnoreCase) || extension.Equals(".cmd", StringComparison.OrdinalIgnoreCase)))
{
startInfo.FileName = "cmd";
startInfo.ArgumentList.Add("/c");
startInfo.ArgumentList.Add(scriptPath);
return startInfo;
}
if (!OperatingSystem.IsWindows() && extension.Equals(".sh", StringComparison.OrdinalIgnoreCase))
{
startInfo.FileName = "/bin/sh";
startInfo.ArgumentList.Add(scriptPath);
return startInfo;
}
startInfo.FileName = scriptPath;
return startInfo;
}
private static string NormalizePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
return Path.IsPathRooted(path)
? path
: Path.GetFullPath(path, AppContext.BaseDirectory);
}
private static string TryGetFileSize(string? path)
{
var normalizedPath = NormalizePath(path);
if (string.IsNullOrWhiteSpace(normalizedPath) || !File.Exists(normalizedPath))
{
return string.Empty;
}
return new FileInfo(normalizedPath).Length.ToString();
}
private static void TryKill(Process process)
{
try
{
if (!process.HasExited)
{
process.Kill(true);
}
}
catch
{
}
}
}