Implement media storage and processing features: update configuration in .env.example and appsettings files, enhance Docker setup for media storage, and add media-related services and database entities. Include ffmpeg for media handling and adjust Kestrel settings for large file uploads.

This commit is contained in:
Leonid Pershin
2026-07-24 08:33:11 +03:00
parent 1dd6991174
commit e15ecbdb29
47 changed files with 2599 additions and 1 deletions
@@ -0,0 +1,105 @@
namespace TeleWave.Domain.Media;
/// <summary>
/// Один видеофайл в библиотеке. После загрузки нормализуется ffmpeg'ом в HLS-сегменты по
/// <see cref="SegmentSeconds"/> секунд; длительность добивается до кратности длине сегмента, чтобы
/// эфирная математика (следующий этап) оставалась целочисленной. Rich model: приватные сеттеры,
/// фабрика <see cref="Register"/> и поведенческие переходы статуса.
/// </summary>
public class MediaAsset
{
public Guid Id { get; private set; }
/// <summary>Исходное имя файла (для отображения в админке).</summary>
public string OriginalFileName { get; private set; } = string.Empty;
/// <summary>Расширение исходника с точкой, в нижнем регистре (например «.mkv»).</summary>
public string OriginalExtension { get; private set; } = string.Empty;
public MediaSource Source { get; private set; }
public MediaAssetStatus Status { get; private set; }
/// <summary>Точная длительность нарезанного ассета (кратна <see cref="SegmentSeconds"/>).</summary>
public TimeSpan? Duration { get; private set; }
public int? SegmentSeconds { get; private set; }
public int? SegmentCount { get; private set; }
public int? Width { get; private set; }
public int? Height { get; private set; }
public string? VideoCodec { get; private set; }
public string? AudioCodec { get; private set; }
/// <summary>Относительный путь каталога сегментов от корня хранилища («assets/{id}»).</summary>
public string? RelativePath { get; private set; }
/// <summary>Текст ошибки, если <see cref="Status"/> == <see cref="MediaAssetStatus.Failed"/>.</summary>
public string? ErrorMessage { get; private set; }
public DateTimeOffset CreatedAt { get; private set; }
public DateTimeOffset UpdatedAt { get; private set; }
private MediaAsset() { }
public static MediaAsset Register(string originalFileName, string extension, MediaSource source)
{
var now = DateTimeOffset.UtcNow;
return new MediaAsset
{
Id = Guid.NewGuid(),
OriginalFileName = originalFileName,
OriginalExtension = extension.ToLowerInvariant(),
Source = source,
Status = MediaAssetStatus.Pending,
CreatedAt = now,
UpdatedAt = now,
};
}
public void MarkProcessing()
{
Status = MediaAssetStatus.Processing;
ErrorMessage = null;
Touch();
}
public void MarkReady(
TimeSpan duration,
int segmentSeconds,
int segmentCount,
int width,
int height,
string videoCodec,
string audioCodec,
string relativePath
)
{
Status = MediaAssetStatus.Ready;
Duration = duration;
SegmentSeconds = segmentSeconds;
SegmentCount = segmentCount;
Width = width;
Height = height;
VideoCodec = videoCodec;
AudioCodec = audioCodec;
RelativePath = relativePath;
ErrorMessage = null;
Touch();
}
public void MarkFailed(string error)
{
Status = MediaAssetStatus.Failed;
ErrorMessage = error;
Touch();
}
/// <summary>Сбросить прерванную обработку (Processing после рестарта) обратно в очередь.</summary>
public void ResetToPending()
{
Status = MediaAssetStatus.Pending;
ErrorMessage = null;
Touch();
}
private void Touch() => UpdatedAt = DateTimeOffset.UtcNow;
}
@@ -0,0 +1,17 @@
namespace TeleWave.Domain.Media;
/// <summary>Жизненный цикл медиа-ассета от загрузки до готовности к эфиру.</summary>
public enum MediaAssetStatus
{
/// <summary>Файл загружен, ждёт обработки в очереди.</summary>
Pending,
/// <summary>ffmpeg нарезает файл на HLS-сегменты.</summary>
Processing,
/// <summary>Нарезка завершена, ассет готов к постановке в эфир.</summary>
Ready,
/// <summary>Обработка провалилась (см. <see cref="MediaAsset.ErrorMessage"/>).</summary>
Failed,
}
@@ -0,0 +1,11 @@
namespace TeleWave.Domain.Media;
/// <summary>Откуда файл попал в хранилище.</summary>
public enum MediaSource
{
/// <summary>Загружен через админку (chunked/stream upload в uploads/).</summary>
Upload,
/// <summary>Положен вручную в inbox/ и подобран сканером.</summary>
Inbox,
}