Refactor user and media endpoints: remove unused GetUser and GetMedia methods, streamline AdminUserEndpoints and MediaEndpoints, and enhance metadata handling in MetadataEndpoints. Update Program.cs to remove legacy image relocation logic and improve overall code clarity.
This commit is contained in:
@@ -122,7 +122,11 @@ public sealed class BumperRenderBackgroundService(
|
||||
var spec = await BuildSpecAsync(db, assetId, cancellationToken);
|
||||
if (spec is null)
|
||||
{
|
||||
await FailAsync(assetId, "Не удалось восстановить спецификацию заставки", cancellationToken);
|
||||
await FailAsync(
|
||||
assetId,
|
||||
"Не удалось восстановить спецификацию заставки",
|
||||
cancellationToken
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,7 +199,11 @@ public sealed class BumperRenderBackgroundService(
|
||||
if (variant.Kind == Domain.Broadcast.BumperTextKind.NowNext)
|
||||
posterPath = await ResolveShowPosterAsync(db, cache.ToShowId, cancellationToken);
|
||||
|
||||
var bgPath = await ResolveTemplateBackgroundAsync(db, template.BackgroundImageId, cancellationToken);
|
||||
var bgPath = await ResolveTemplateBackgroundAsync(
|
||||
db,
|
||||
template.BackgroundImageId,
|
||||
cancellationToken
|
||||
);
|
||||
var aligned = BumperDuration.Aligned(
|
||||
BumperDuration.TemplateSeconds(template),
|
||||
_segmentSeconds
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
|
||||
/// <summary>Сигнальная очередь-будильник поверх Channel (id ассета — лишь сигнал; работу берём из БД).</summary>
|
||||
public sealed class BumperRenderQueue : IBumperRenderQueue
|
||||
{
|
||||
private readonly Channel<Guid> _channel = Channel.CreateUnbounded<Guid>(
|
||||
new UnboundedChannelOptions { SingleReader = true }
|
||||
);
|
||||
|
||||
public void Enqueue(Guid assetId) => _channel.Writer.TryWrite(assetId);
|
||||
|
||||
public async ValueTask WaitAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _channel.Reader.ReadAsync(cancellationToken);
|
||||
while (_channel.Reader.TryRead(out _)) { }
|
||||
}
|
||||
}
|
||||
/// <summary>Сигнальная очередь-будильник рендерера заставок (см. <see cref="SignalQueue"/>).</summary>
|
||||
public sealed class BumperRenderQueue : SignalQueue, IBumperRenderQueue { }
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using static TeleWave.Infrastructure.Media.FfmpegText;
|
||||
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
|
||||
@@ -221,9 +222,7 @@ public sealed class FfmpegBumperRenderer(
|
||||
var nextSize = FitSize(spec.NextTitle, titleSize, textWidth);
|
||||
vchain
|
||||
.Append(',')
|
||||
.Append(
|
||||
DrawLabel(font, nowLabelFile, spec.AccentColor, labelSize, nowLabelY, 0.2)
|
||||
);
|
||||
.Append(DrawLabel(font, nowLabelFile, spec.AccentColor, labelSize, nowLabelY, 0.2));
|
||||
vchain
|
||||
.Append(',')
|
||||
.Append(DrawTitle(font, nowFile, spec.TextColor, nowSize, nowTitleY, 0.3));
|
||||
@@ -340,9 +339,6 @@ public sealed class FfmpegBumperRenderer(
|
||||
/// двоеточие экранируется). На Linux (контейнере) — фактически no-op.</summary>
|
||||
private static string EscapePath(string path) => path.Replace('\\', '/').Replace(":", "\\:");
|
||||
|
||||
private static string Fmt(double value) =>
|
||||
value.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
@@ -355,11 +351,4 @@ public sealed class FfmpegBumperRenderer(
|
||||
// Файл-подсказка для drawtext; не критично, если не удалился.
|
||||
}
|
||||
}
|
||||
|
||||
private static string Tail(string text)
|
||||
{
|
||||
text = text.Trim();
|
||||
const int max = 500;
|
||||
return text.Length <= max ? text : text[^max..];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Media;
|
||||
using static TeleWave.Infrastructure.Media.FfmpegText;
|
||||
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
|
||||
@@ -98,14 +99,7 @@ public sealed class FfmpegMediaProcessor(
|
||||
if (padding)
|
||||
vfilter += $",tpad=stop_duration={Fmt(pad)}:stop_mode=add:color=black";
|
||||
|
||||
var args = new List<string>
|
||||
{
|
||||
"-hide_banner",
|
||||
"-nostdin",
|
||||
"-y",
|
||||
"-i",
|
||||
input,
|
||||
};
|
||||
var args = new List<string> { "-hide_banner", "-nostdin", "-y", "-i", input };
|
||||
|
||||
// Явный выбор дорожки по предпочитаемому языку: маппим видео + конкретную аудиодорожку.
|
||||
// Без выбора (audioTrack == null) — не маппим, оставляя дефолтную эвристику ffmpeg (как раньше).
|
||||
@@ -262,13 +256,17 @@ public sealed class FfmpegMediaProcessor(
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var settings = scope.ServiceProvider.GetRequiredService<ISiteSettings>();
|
||||
var configured = await settings.GetPreferredAudioLanguagesAsync(cancellationToken);
|
||||
var raw = string.IsNullOrWhiteSpace(configured) ? _media.PreferredAudioLanguages : configured;
|
||||
return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
var raw = string.IsNullOrWhiteSpace(configured)
|
||||
? _media.PreferredAudioLanguages
|
||||
: configured;
|
||||
return raw.Split(
|
||||
',',
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
|
||||
)
|
||||
.Select(x => x.ToLowerInvariant())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
private static (int Width, int Height) ScaleDown(int width, int height)
|
||||
{
|
||||
if (width <= MaxWidth)
|
||||
@@ -279,16 +277,6 @@ public sealed class FfmpegMediaProcessor(
|
||||
return (MaxWidth, scaledHeight);
|
||||
}
|
||||
|
||||
private static string Fmt(double value) =>
|
||||
value.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
|
||||
private static string Tail(string text)
|
||||
{
|
||||
text = text.Trim();
|
||||
const int max = 500;
|
||||
return text.Length <= max ? text : text[^max..];
|
||||
}
|
||||
|
||||
private sealed record ProbeInfo(
|
||||
double Duration,
|
||||
int Width,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
|
||||
/// <summary>Общие текстовые хелперы для сборки аргументов ffmpeg и разбора его вывода.</summary>
|
||||
internal static class FfmpegText
|
||||
{
|
||||
/// <summary>Формат double для фильтров/аргументов ffmpeg (инвариантная культура, до 3 знаков).</summary>
|
||||
public static string Fmt(double value) => value.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>Хвост stderr для сообщения об ошибке (последние 500 символов).</summary>
|
||||
public static string Tail(string text)
|
||||
{
|
||||
text = text.Trim();
|
||||
const int max = 500;
|
||||
return text.Length <= max ? text : text[^max..];
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ public sealed class MediaPathResolver
|
||||
OriginalsDir = Path.Combine(_root, "originals");
|
||||
AssetsDir = Path.Combine(_root, "assets");
|
||||
BumpersDir = Path.Combine(_root, "bumpers");
|
||||
MetadataDir = Path.Combine(_root, "metadata");
|
||||
ImagesDir = Path.Combine(_root, "images");
|
||||
}
|
||||
|
||||
@@ -30,9 +29,6 @@ public sealed class MediaPathResolver
|
||||
/// <summary>Сырые файлы шаблонов заставок (фон/музыка) по каналам — не режутся на HLS.</summary>
|
||||
public string BumpersDir { get; }
|
||||
|
||||
/// <summary>Картинки метаданных (постеры/кадры), скачанные локально.</summary>
|
||||
public string MetadataDir { get; }
|
||||
|
||||
/// <summary>Общий реестр изображений (галерея): файлы images/{imageId}{ext}.</summary>
|
||||
public string ImagesDir { get; }
|
||||
|
||||
@@ -43,47 +39,9 @@ public sealed class MediaPathResolver
|
||||
Directory.CreateDirectory(OriginalsDir);
|
||||
Directory.CreateDirectory(AssetsDir);
|
||||
Directory.CreateDirectory(BumpersDir);
|
||||
Directory.CreateDirectory(MetadataDir);
|
||||
Directory.CreateDirectory(ImagesDir);
|
||||
}
|
||||
|
||||
public string MetadataShowDir(Guid showId) =>
|
||||
EnsureWithinRoot(Path.Combine(MetadataDir, "shows", showId.ToString("N")));
|
||||
|
||||
/// <summary>Абсолютный путь к файлу постера шоу (extension — с точкой).</summary>
|
||||
public string MetadataShowPosterPath(Guid showId, string extension) =>
|
||||
EnsureWithinRoot(
|
||||
Path.Combine(MetadataDir, "shows", showId.ToString("N"), "poster" + extension)
|
||||
);
|
||||
|
||||
/// <summary>Относительный путь постера от корня (для хранения в БД и отдачи).</summary>
|
||||
public string MetadataShowPosterRelative(Guid showId, string extension) =>
|
||||
$"metadata/shows/{showId:N}/poster{extension}";
|
||||
|
||||
public string MetadataEpisodeDir(Guid episodeId) =>
|
||||
EnsureWithinRoot(Path.Combine(MetadataDir, "episodes", episodeId.ToString("N")));
|
||||
|
||||
public string MetadataEpisodeStillPath(Guid episodeId, string extension) =>
|
||||
EnsureWithinRoot(
|
||||
Path.Combine(MetadataDir, "episodes", episodeId.ToString("N"), "still" + extension)
|
||||
);
|
||||
|
||||
public string MetadataEpisodeStillRelative(Guid episodeId, string extension) =>
|
||||
$"metadata/episodes/{episodeId:N}/still{extension}";
|
||||
|
||||
/// <summary>Абсолютный путь по относительному (с защитой от traversal) или null, если вне корня.</summary>
|
||||
public string? ResolveRelative(string relativePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return EnsureWithinRoot(Path.Combine(_root, relativePath));
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string BumperTemplateDir(Guid templateId) =>
|
||||
EnsureWithinRoot(Path.Combine(BumpersDir, templateId.ToString("N")));
|
||||
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
|
||||
/// <summary>Сигнальная очередь-будильник поверх Channel (id ассета используется лишь как сигнал).</summary>
|
||||
public sealed class MediaProcessingQueue : IMediaProcessingQueue
|
||||
{
|
||||
private readonly Channel<Guid> _channel = Channel.CreateUnbounded<Guid>(
|
||||
new UnboundedChannelOptions { SingleReader = true }
|
||||
);
|
||||
|
||||
public void Enqueue(Guid assetId) => _channel.Writer.TryWrite(assetId);
|
||||
|
||||
public async ValueTask WaitAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _channel.Reader.ReadAsync(cancellationToken);
|
||||
// Сдренировать накопившиеся сигналы — работу всё равно берём из БД пачкой.
|
||||
while (_channel.Reader.TryRead(out _)) { }
|
||||
}
|
||||
}
|
||||
/// <summary>Сигнальная очередь-будильник медиа-конвейера (см. <see cref="SignalQueue"/>).</summary>
|
||||
public sealed class MediaProcessingQueue : SignalQueue, IMediaProcessingQueue { }
|
||||
|
||||
@@ -63,13 +63,9 @@ internal static class ProcessRunner
|
||||
// (битый источник, -stream_loop и т.п.) не должен держать слот параллелизма/тик планировщика вечно.
|
||||
using var timeoutCts =
|
||||
timeout > TimeSpan.Zero ? new CancellationTokenSource(timeout) : null;
|
||||
using var linked =
|
||||
timeoutCts is null
|
||||
? null
|
||||
: CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken,
|
||||
timeoutCts.Token
|
||||
);
|
||||
using var linked = timeoutCts is null
|
||||
? null
|
||||
: CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
var waitToken = linked?.Token ?? cancellationToken;
|
||||
|
||||
try
|
||||
@@ -79,7 +75,10 @@ internal static class ProcessRunner
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
TryKill(process);
|
||||
if (timeoutCts is { IsCancellationRequested: true } && !cancellationToken.IsCancellationRequested)
|
||||
if (
|
||||
timeoutCts is { IsCancellationRequested: true }
|
||||
&& !cancellationToken.IsCancellationRequested
|
||||
)
|
||||
throw new TimeoutException(
|
||||
$"Процесс {fileName} превысил таймаут {timeout.TotalSeconds:0}с и был прерван."
|
||||
);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Базовая сигнальная очередь-будильник поверх Channel: id ассета используется лишь как сигнал,
|
||||
/// а работу фоновый обработчик всё равно берёт из БД пачкой (по статусу Pending). Поэтому потеря
|
||||
/// сигнала при рестарте не теряет задачи — они подхватываются из базы.
|
||||
/// </summary>
|
||||
public abstract class SignalQueue
|
||||
{
|
||||
private readonly Channel<Guid> _channel = Channel.CreateUnbounded<Guid>(
|
||||
new UnboundedChannelOptions { SingleReader = true }
|
||||
);
|
||||
|
||||
/// <summary>Разбудить обработчик: появился ассет в статусе Pending.</summary>
|
||||
public void Enqueue(Guid assetId) => _channel.Writer.TryWrite(assetId);
|
||||
|
||||
/// <summary>Ждать сигнала о новой работе (с дренажом накопленных).</summary>
|
||||
public async ValueTask WaitAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _channel.Reader.ReadAsync(cancellationToken);
|
||||
// Сдренировать накопившиеся сигналы — работу всё равно берём из БД пачкой.
|
||||
while (_channel.Reader.TryRead(out _)) { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user