feat: add OpenList upload support and upload tasks monitoring page
Backend: - Add OpenListRecordArtifactUploader implementing AList-compatible API upload - Add Uploading status to RecordArtifactUploadStatus enum - Add MarkUploadStarted() to RecordResult entity for upload progress tracking - Add ListUploadStatus API endpoint with pagination and status filtering - Add UploadTaskItemDto and UploadTaskListResponse models - Add upload segment count stats (uploaded/failed/uploading) to session DTO - Add OpenListUploadSettingsDto and upload target type OpenList Frontend: - Add UploadTasksView page with route /upload-tasks - Add upload status labels and UploadTaskItem types - Refactor MainLayout navigation and clean up main.css - Polish DashboardView, MetricCard, StatusBadge, RightDrawer components - Update SettingsView to support OpenList upload configuration Build: - Add frontend/Dockerfile.arm64 for ARM64 frontend image - Update build-arm64-image.sh script Other: - Add segment_completed_openlist.sh trigger script - Add prototype/ directory with UI mockups - Add frontend .dockerignore refinements Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,16 @@ public interface IRecordResultRepository
|
||||
|
||||
Task<long> SumPendingUploadBytesAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<(RecordResult Result, RecordTask Task)>> ListUploadStatusAsync(
|
||||
RecordArtifactUploadStatus? uploadStatusFilter,
|
||||
int skip,
|
||||
int take,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<int> CountUploadStatusAsync(
|
||||
RecordArtifactUploadStatus? uploadStatusFilter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(RecordResult recordResult, CancellationToken cancellationToken = default);
|
||||
|
||||
void Update(RecordResult recordResult);
|
||||
|
||||
@@ -42,6 +42,12 @@ public sealed class RecordSessionDto
|
||||
|
||||
public int TotalDanmakuMessageCount { get; init; }
|
||||
|
||||
public int UploadedSegmentCount { get; init; }
|
||||
|
||||
public int FailedUploadSegmentCount { get; init; }
|
||||
|
||||
public int UploadingSegmentCount { get; init; }
|
||||
|
||||
public required IReadOnlyList<RecordTaskDto> Tasks { get; init; }
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,8 @@ public sealed class RecordTaskDto
|
||||
|
||||
public double? DurationSeconds { get; init; }
|
||||
|
||||
public RecordArtifactUploadStatus? UploadStatus { get; init; }
|
||||
|
||||
public string? PostProcessStage { get; init; }
|
||||
|
||||
public double? PostProcessProgressPercent { get; init; }
|
||||
@@ -169,3 +171,57 @@ public sealed class ManualSegmentCompletedTriggerResultDto
|
||||
|
||||
public required string Message { get; init; }
|
||||
}
|
||||
|
||||
public sealed class UploadTaskItemDto
|
||||
{
|
||||
public Guid RecordTaskId { get; init; }
|
||||
|
||||
public Guid RecordSessionId { get; init; }
|
||||
|
||||
public Guid LiveRoomId { get; init; }
|
||||
|
||||
public required string LiveRoomTitle { get; init; }
|
||||
|
||||
public int Platform { get; init; }
|
||||
|
||||
public required string RoomId { get; init; }
|
||||
|
||||
public int SegmentIndex { get; init; }
|
||||
|
||||
public string OutputFormat { get; init; } = string.Empty;
|
||||
|
||||
public string? FilePath { get; init; }
|
||||
|
||||
public long? FileSizeBytes { get; init; }
|
||||
|
||||
public string? DanmakuFilePath { get; init; }
|
||||
|
||||
public RecordArtifactUploadStatus UploadStatus { get; init; }
|
||||
|
||||
public string? LastUploadProvider { get; init; }
|
||||
|
||||
public string? RemoteVideoPath { get; init; }
|
||||
|
||||
public string? RemoteDanmakuPath { get; init; }
|
||||
|
||||
public DateTimeOffset? LastUploadedAt { get; init; }
|
||||
|
||||
public string? UploadErrorMessage { get; init; }
|
||||
|
||||
public bool DeletedLocalFilesAfterUpload { get; init; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
public sealed class UploadTaskListResponse
|
||||
{
|
||||
public IReadOnlyList<UploadTaskItemDto> Items { get; init; } = [];
|
||||
|
||||
public int TotalCount { get; init; }
|
||||
|
||||
public int NotUploadedCount { get; init; }
|
||||
|
||||
public int SucceededCount { get; init; }
|
||||
|
||||
public int FailedCount { get; init; }
|
||||
}
|
||||
|
||||
@@ -82,6 +82,17 @@ public sealed class S3UploadSettingsDto
|
||||
public bool ForcePathStyle { get; set; }
|
||||
}
|
||||
|
||||
public sealed class OpenListUploadSettingsDto
|
||||
{
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
public string BasePath { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class SystemSettingsDto
|
||||
{
|
||||
public string FfmpegPath { get; set; } = "ffmpeg";
|
||||
@@ -155,6 +166,8 @@ public sealed class SystemSettingsDto
|
||||
|
||||
public S3UploadSettingsDto S3Upload { get; set; } = new();
|
||||
|
||||
public OpenListUploadSettingsDto OpenListUpload { get; set; } = new();
|
||||
|
||||
public bool EnableEventScripts { get; set; } = false;
|
||||
|
||||
public bool EnableLiveStartedScript { get; set; } = false;
|
||||
@@ -434,6 +447,8 @@ public sealed class UpdateSystemSettingsRequest
|
||||
|
||||
public S3UploadSettingsDto S3Upload { get; set; } = new();
|
||||
|
||||
public OpenListUploadSettingsDto OpenListUpload { get; set; } = new();
|
||||
|
||||
public bool EnableEventScripts { get; set; } = false;
|
||||
|
||||
public bool EnableLiveStartedScript { get; set; } = false;
|
||||
|
||||
@@ -27,6 +27,7 @@ internal static class RecordModelMapper
|
||||
StartedAt = recordTask.StartedAt,
|
||||
EndedAt = recordTask.EndedAt,
|
||||
DurationSeconds = recordTask.DurationSeconds,
|
||||
UploadStatus = recordTask.Result?.UploadStatus,
|
||||
PostProcessStage = runtimeState?.Stage,
|
||||
PostProcessProgressPercent = runtimeState?.ProgressPercent,
|
||||
PostProcessDetail = runtimeState?.Detail
|
||||
@@ -87,6 +88,9 @@ internal static class RecordModelMapper
|
||||
EndedAt = recordSession.EndedAt,
|
||||
TotalFileSizeBytes = totalFileSizeBytes,
|
||||
TotalDanmakuMessageCount = totalDanmakuMessageCount,
|
||||
UploadedSegmentCount = orderedTasks.Count(item => item.Result?.UploadStatus == RecordArtifactUploadStatus.Succeeded),
|
||||
FailedUploadSegmentCount = orderedTasks.Count(item => item.Result?.UploadStatus == RecordArtifactUploadStatus.Failed),
|
||||
UploadingSegmentCount = orderedTasks.Count(item => item.Result?.UploadStatus == RecordArtifactUploadStatus.Uploading),
|
||||
Tasks = orderedTasks.Select(item => MapTaskWithFallback(
|
||||
item,
|
||||
recordSession,
|
||||
@@ -122,6 +126,7 @@ internal static class RecordModelMapper
|
||||
StartedAt = recordTask.StartedAt,
|
||||
EndedAt = recordTask.EndedAt,
|
||||
DurationSeconds = recordTask.DurationSeconds,
|
||||
UploadStatus = recordTask.Result?.UploadStatus,
|
||||
PostProcessStage = runtimeState?.Stage,
|
||||
PostProcessProgressPercent = runtimeState?.ProgressPercent,
|
||||
PostProcessDetail = runtimeState?.Detail
|
||||
|
||||
@@ -54,6 +54,10 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
private const string S3SecretKeyKey = "upload.s3.secret_key";
|
||||
private const string S3PrefixKey = "upload.s3.prefix";
|
||||
private const string S3ForcePathStyleKey = "upload.s3.force_path_style";
|
||||
private const string OpenListBaseUrlKey = "upload.openlist.base_url";
|
||||
private const string OpenListUsernameKey = "upload.openlist.username";
|
||||
private const string OpenListPasswordKey = "upload.openlist.password";
|
||||
private const string OpenListBasePathKey = "upload.openlist.base_path";
|
||||
private const string DouyinProxyEnabledKey = "platform_proxy.douyin.enabled";
|
||||
private const string DouyinProxyUrlKey = "platform_proxy.douyin.url";
|
||||
private const string BilibiliProxyEnabledKey = "platform_proxy.bilibili.enabled";
|
||||
@@ -180,6 +184,13 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
Prefix = GetValue(lookup, S3PrefixKey, string.Empty),
|
||||
ForcePathStyle = bool.TryParse(GetValue(lookup, S3ForcePathStyleKey, "false"), out var s3ForcePathStyle) && s3ForcePathStyle
|
||||
},
|
||||
OpenListUpload = new OpenListUploadSettingsDto
|
||||
{
|
||||
BaseUrl = GetValue(lookup, OpenListBaseUrlKey, string.Empty),
|
||||
Username = GetValue(lookup, OpenListUsernameKey, string.Empty),
|
||||
Password = GetValue(lookup, OpenListPasswordKey, string.Empty),
|
||||
BasePath = GetValue(lookup, OpenListBasePathKey, string.Empty)
|
||||
},
|
||||
EnableEventScripts = bool.TryParse(GetValue(lookup, EnableEventScriptsKey, "false"), out var enableEventScripts) && enableEventScripts,
|
||||
EnableLiveStartedScript = GetEventScriptEnabled(
|
||||
lookup,
|
||||
@@ -348,6 +359,11 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
||||
await UpsertAsync(S3SecretKeyKey, s3Upload.SecretKey, now, cancellationToken);
|
||||
await UpsertAsync(S3PrefixKey, s3Upload.Prefix.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(S3ForcePathStyleKey, s3Upload.ForcePathStyle.ToString(), now, cancellationToken);
|
||||
var openListUpload = request.OpenListUpload ?? new OpenListUploadSettingsDto();
|
||||
await UpsertAsync(OpenListBaseUrlKey, openListUpload.BaseUrl.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(OpenListUsernameKey, openListUpload.Username.Trim(), now, cancellationToken);
|
||||
await UpsertAsync(OpenListPasswordKey, openListUpload.Password, now, cancellationToken);
|
||||
await UpsertAsync(OpenListBasePathKey, openListUpload.BasePath.Trim(), now, cancellationToken);
|
||||
foreach (var platformDefinition in LivePlatformCatalog.All)
|
||||
{
|
||||
var platformRequestSettings = request.GetPlatformRequestSettings(platformDefinition.Type);
|
||||
|
||||
Reference in New Issue
Block a user