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);
|
||||
|
||||
@@ -85,6 +85,15 @@ public class RecordResult
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public void MarkUploadStarted(string provider, DateTimeOffset startedAt)
|
||||
{
|
||||
UploadStatus = RecordArtifactUploadStatus.Uploading;
|
||||
LastUploadProvider = NormalizeNullable(provider);
|
||||
LastUploadedAt = startedAt;
|
||||
UploadErrorMessage = null;
|
||||
DeletedLocalFilesAfterUpload = false;
|
||||
}
|
||||
|
||||
public void MarkUploadSucceeded(
|
||||
string provider,
|
||||
string? remoteVideoPath,
|
||||
|
||||
@@ -4,5 +4,6 @@ public enum RecordArtifactUploadStatus
|
||||
{
|
||||
NotUploaded = 0,
|
||||
Succeeded = 1,
|
||||
Failed = 2
|
||||
Failed = 2,
|
||||
Uploading = 3
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ public enum UploadTargetType
|
||||
{
|
||||
None = 0,
|
||||
WebDav = 1,
|
||||
S3 = 2
|
||||
S3 = 2,
|
||||
OpenList = 3
|
||||
}
|
||||
|
||||
@@ -277,6 +277,48 @@ public sealed class RecordResultRepository : IRecordResultRepository
|
||||
.Where(item => item.UploadStatus == RecordArtifactUploadStatus.NotUploaded)
|
||||
.SumAsync(item => item.FileSizeBytes ?? 0L, cancellationToken);
|
||||
|
||||
public async Task<List<(RecordResult Result, RecordTask Task)>> ListUploadStatusAsync(
|
||||
RecordArtifactUploadStatus? uploadStatusFilter,
|
||||
int skip,
|
||||
int take,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _dbContext.RecordResults
|
||||
.Include(item => item.RecordTask!)
|
||||
.ThenInclude(task => task.LiveRoom)
|
||||
.AsQueryable();
|
||||
|
||||
if (uploadStatusFilter.HasValue)
|
||||
{
|
||||
query = query.Where(item => item.UploadStatus == uploadStatusFilter.Value);
|
||||
}
|
||||
|
||||
var results = await query
|
||||
.OrderByDescending(item => item.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return results
|
||||
.Where(item => item.RecordTask is not null)
|
||||
.Select(item => (item, item.RecordTask!))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public Task<int> CountUploadStatusAsync(
|
||||
RecordArtifactUploadStatus? uploadStatusFilter,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _dbContext.RecordResults.AsQueryable();
|
||||
|
||||
if (uploadStatusFilter.HasValue)
|
||||
{
|
||||
query = query.Where(item => item.UploadStatus == uploadStatusFilter.Value);
|
||||
}
|
||||
|
||||
return query.CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task AddAsync(RecordResult recordResult, CancellationToken cancellationToken = default) =>
|
||||
_dbContext.RecordResults.AddAsync(recordResult, cancellationToken).AsTask();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LiveRecorder.Application.Abstractions.Logging;
|
||||
using LiveRecorder.Application.Abstractions.Settings;
|
||||
using LiveRecorder.Application.Models.RecordTasks;
|
||||
@@ -147,6 +148,8 @@ public sealed class RecordUploadService
|
||||
}
|
||||
|
||||
var uploader = CreateUploader(settings);
|
||||
recordResult.MarkUploadStarted(uploader.ProviderName, DateTimeOffset.UtcNow);
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var remoteVideoPath = recordResult.RemoteVideoPath;
|
||||
@@ -325,6 +328,7 @@ public sealed class RecordUploadService
|
||||
{
|
||||
UploadTargetType.WebDav => new WebDavRecordArtifactUploader(settings.WebDavUpload),
|
||||
UploadTargetType.S3 => new S3RecordArtifactUploader(settings.S3Upload),
|
||||
UploadTargetType.OpenList => new OpenListRecordArtifactUploader(settings.OpenListUpload),
|
||||
_ => throw new InvalidOperationException("No supported upload target is configured.")
|
||||
};
|
||||
}
|
||||
@@ -620,3 +624,137 @@ internal sealed class S3RecordArtifactUploader : IRecordArtifactUploader
|
||||
private static string ConvertToHex(byte[] bytes) =>
|
||||
Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
internal sealed class OpenListRecordArtifactUploader : IRecordArtifactUploader
|
||||
{
|
||||
private readonly OpenListUploadSettingsDto _settings;
|
||||
|
||||
public OpenListRecordArtifactUploader(OpenListUploadSettingsDto settings)
|
||||
{
|
||||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
public string ProviderName => "openlist";
|
||||
|
||||
public async Task<string> UploadFileAsync(string localPath, string relativeRemotePath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_settings.BaseUrl))
|
||||
{
|
||||
throw new InvalidOperationException("OpenList base URL is not configured.");
|
||||
}
|
||||
|
||||
var baseUrl = _settings.BaseUrl.Trim().TrimEnd('/');
|
||||
var remotePath = BuildRemotePath(_settings.BasePath, relativeRemotePath);
|
||||
var token = await LoginAsync(baseUrl, cancellationToken);
|
||||
await EnsureDirectoriesAsync(baseUrl, token, remotePath, cancellationToken);
|
||||
return await PutFileAsync(baseUrl, token, remotePath, localPath, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<string> LoginAsync(string baseUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
||||
var payload = JsonSerializer.Serialize(new { username = _settings.Username, password = _settings.Password });
|
||||
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
|
||||
using var response = await client.PostAsync($"{baseUrl}/api/auth/login", content, cancellationToken);
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var root = doc.RootElement;
|
||||
var code = root.TryGetProperty("code", out var codeElement) ? codeElement.GetInt32() : -1;
|
||||
if (code != 200)
|
||||
{
|
||||
var message = root.TryGetProperty("message", out var msgElement) ? msgElement.GetString() : body;
|
||||
throw new InvalidOperationException($"OpenList login failed with code {code}: {message}");
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("data", out var dataElement) ||
|
||||
!dataElement.TryGetProperty("token", out var tokenElement) ||
|
||||
string.IsNullOrWhiteSpace(tokenElement.GetString()))
|
||||
{
|
||||
throw new InvalidOperationException("OpenList login response did not contain a token.");
|
||||
}
|
||||
|
||||
return tokenElement.GetString()!;
|
||||
}
|
||||
|
||||
private static async Task EnsureDirectoriesAsync(string baseUrl, string token, string remotePath, CancellationToken cancellationToken)
|
||||
{
|
||||
var segments = remotePath.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (segments.Length <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
||||
var accumulatedPath = string.Empty;
|
||||
for (var i = 0; i < segments.Length - 1; i++)
|
||||
{
|
||||
accumulatedPath += "/" + segments[i];
|
||||
await MkdirAsync(client, baseUrl, token, accumulatedPath, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task MkdirAsync(HttpClient client, string baseUrl, string token, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(new { path });
|
||||
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/api/fs/mkdir")
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
request.Headers.TryAddWithoutValidation("Authorization", token);
|
||||
|
||||
using var response = await client.SendAsync(request, cancellationToken);
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var code = doc.RootElement.TryGetProperty("code", out var codeElement) ? codeElement.GetInt32() : -1;
|
||||
if (code != 200)
|
||||
{
|
||||
var message = doc.RootElement.TryGetProperty("message", out var msgElement) ? msgElement.GetString() : body;
|
||||
// AList returns code 500 with "already exists" or "exist" when directory already exists — that's acceptable
|
||||
if (message != null && message.Contains("exist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"OpenList mkdir failed with code {code} for path '{path}': {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> PutFileAsync(string baseUrl, string token, string remotePath, string localPath, CancellationToken cancellationToken)
|
||||
{
|
||||
using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
||||
using var request = new HttpRequestMessage(HttpMethod.Put, $"{baseUrl}/api/fs/put");
|
||||
request.Headers.TryAddWithoutValidation("Authorization", token);
|
||||
request.Headers.TryAddWithoutValidation("File-Path", Uri.EscapeDataString(remotePath));
|
||||
request.Content = new StreamContent(File.OpenRead(localPath));
|
||||
|
||||
using var response = await client.SendAsync(request, cancellationToken);
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var code = doc.RootElement.TryGetProperty("code", out var codeElement) ? codeElement.GetInt32() : -1;
|
||||
if (code != 200)
|
||||
{
|
||||
var message = doc.RootElement.TryGetProperty("message", out var msgElement) ? msgElement.GetString() : body;
|
||||
throw new InvalidOperationException($"OpenList file upload failed with code {code}: {message}");
|
||||
}
|
||||
|
||||
return $"{baseUrl}{remotePath}";
|
||||
}
|
||||
|
||||
private static string BuildRemotePath(string? basePath, string relativeRemotePath)
|
||||
{
|
||||
var parts = new[]
|
||||
{
|
||||
basePath?.Trim(),
|
||||
relativeRemotePath.Trim()
|
||||
}
|
||||
.Where(static item => !string.IsNullOrWhiteSpace(item))
|
||||
.Select(static item => item!.Trim('/'))
|
||||
.ToArray();
|
||||
|
||||
return "/" + string.Join('/', parts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using LiveRecorder.Application.Abstractions.Persistence;
|
||||
using LiveRecorder.Application.Models.RecordTasks;
|
||||
using LiveRecorder.Application.Services;
|
||||
using LiveRecorder.Application.Abstractions.Recording;
|
||||
using LiveRecorder.Domain.Entities;
|
||||
using LiveRecorder.Domain.Enums;
|
||||
using LiveRecorder.Infrastructure.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -12,6 +15,7 @@ public sealed class RecordTasksController : ControllerBase
|
||||
{
|
||||
private readonly RecordService _recordService;
|
||||
private readonly RecordUploadService _recordUploadService;
|
||||
private readonly IRecordResultRepository _recordResultRepository;
|
||||
private readonly IRecordMediaService _recordMediaService;
|
||||
private readonly IDanmakuService _danmakuService;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
@@ -19,12 +23,14 @@ public sealed class RecordTasksController : ControllerBase
|
||||
public RecordTasksController(
|
||||
RecordService recordService,
|
||||
RecordUploadService recordUploadService,
|
||||
IRecordResultRepository recordResultRepository,
|
||||
IRecordMediaService recordMediaService,
|
||||
IDanmakuService danmakuService,
|
||||
LinkGenerator linkGenerator)
|
||||
{
|
||||
_recordService = recordService;
|
||||
_recordUploadService = recordUploadService;
|
||||
_recordResultRepository = recordResultRepository;
|
||||
_recordMediaService = recordMediaService;
|
||||
_danmakuService = danmakuService;
|
||||
_linkGenerator = linkGenerator;
|
||||
@@ -34,6 +40,62 @@ public sealed class RecordTasksController : ControllerBase
|
||||
public async Task<ActionResult<IReadOnlyList<RecordTaskDto>>> List([FromQuery] Guid? liveRoomId, CancellationToken cancellationToken) =>
|
||||
Ok(await _recordService.ListAsync(liveRoomId, cancellationToken));
|
||||
|
||||
[HttpGet("upload-status")]
|
||||
public async Task<ActionResult<UploadTaskListResponse>> ListUploadStatus(
|
||||
[FromQuery] int? uploadStatus,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 50,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = uploadStatus.HasValue && Enum.IsDefined(typeof(RecordArtifactUploadStatus), uploadStatus.Value)
|
||||
? (RecordArtifactUploadStatus)uploadStatus.Value
|
||||
: (RecordArtifactUploadStatus?)null;
|
||||
|
||||
var items = await _recordResultRepository.ListUploadStatusAsync(filter, skip, take, cancellationToken);
|
||||
var totalCount = await _recordResultRepository.CountUploadStatusAsync(filter, cancellationToken);
|
||||
var notUploadedCount = await _recordResultRepository.CountUploadStatusAsync(RecordArtifactUploadStatus.NotUploaded, cancellationToken);
|
||||
var succeededCount = await _recordResultRepository.CountUploadStatusAsync(RecordArtifactUploadStatus.Succeeded, cancellationToken);
|
||||
var failedCount = await _recordResultRepository.CountUploadStatusAsync(RecordArtifactUploadStatus.Failed, cancellationToken);
|
||||
|
||||
return Ok(new UploadTaskListResponse
|
||||
{
|
||||
Items = items.Select(MapUploadTaskItem).ToList(),
|
||||
TotalCount = totalCount,
|
||||
NotUploadedCount = notUploadedCount,
|
||||
SucceededCount = succeededCount,
|
||||
FailedCount = failedCount
|
||||
});
|
||||
}
|
||||
|
||||
private static UploadTaskItemDto MapUploadTaskItem((RecordResult Result, RecordTask Task) pair)
|
||||
{
|
||||
var (result, task) = pair;
|
||||
var liveRoom = task.LiveRoom;
|
||||
|
||||
return new UploadTaskItemDto
|
||||
{
|
||||
RecordTaskId = task.Id,
|
||||
RecordSessionId = task.RecordSessionId,
|
||||
LiveRoomId = task.LiveRoomId,
|
||||
LiveRoomTitle = liveRoom?.Title ?? liveRoom?.AnchorName ?? liveRoom?.RoomId ?? "Unknown Room",
|
||||
Platform = (int)(liveRoom?.Platform ?? LivePlatformType.Unknown),
|
||||
RoomId = liveRoom?.RoomId ?? string.Empty,
|
||||
SegmentIndex = task.SegmentIndex,
|
||||
OutputFormat = task.OutputFormat.ToString(),
|
||||
FilePath = result.FilePath,
|
||||
FileSizeBytes = result.FileSizeBytes,
|
||||
DanmakuFilePath = result.DanmakuFilePath,
|
||||
UploadStatus = result.UploadStatus,
|
||||
LastUploadProvider = result.LastUploadProvider,
|
||||
RemoteVideoPath = result.RemoteVideoPath,
|
||||
RemoteDanmakuPath = result.RemoteDanmakuPath,
|
||||
LastUploadedAt = result.LastUploadedAt,
|
||||
UploadErrorMessage = result.UploadErrorMessage,
|
||||
DeletedLocalFilesAfterUpload = result.DeletedLocalFilesAfterUpload,
|
||||
CreatedAt = result.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<RecordTaskDetailDto>> Get(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user