Refactor Channel endpoints and data models: replace jingle functionality with bumper templates, update related commands and handlers, and enhance API routes for managing bumper templates. Remove obsolete jingle-related code and adjust channel data structures to support new bumper template features.

This commit is contained in:
Leonid Pershin
2026-07-25 11:02:16 +03:00
parent 27571a4ab6
commit 84c2867062
60 changed files with 2805 additions and 1045 deletions
@@ -3,67 +3,74 @@ using TeleWave.Application.Common.Interfaces;
namespace TeleWave.Infrastructure.Media;
/// <summary>
/// Файловое хранилище шаблонов заставок: сырые фон/музыка под bumpers/{channelId}/{kind}{ext}.
/// На канал — не более одного файла каждого вида (при загрузке старый удаляется).
/// Файловое хранилище блоков заставок: сырые звук/фон под bumpers/{templateId}/{kind}{ext}.
/// На блок — не более одного файла каждого вида (при загрузке старый удаляется).
/// </summary>
public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemplateStorage
{
private const string Audio = "audio";
private const string Background = "background";
private const string Music = "music";
public Task SaveAudioAsync(
Guid templateId,
string extension,
Stream content,
CancellationToken cancellationToken
) => SaveAsync(templateId, Audio, extension, content, cancellationToken);
public Task SaveBackgroundAsync(
Guid channelId,
Guid templateId,
string extension,
Stream content,
CancellationToken cancellationToken
) => SaveAsync(channelId, Background, extension, content, cancellationToken);
) => SaveAsync(templateId, Background, extension, content, cancellationToken);
public Task SaveMusicAsync(
Guid channelId,
string extension,
Stream content,
CancellationToken cancellationToken
) => SaveAsync(channelId, Music, extension, content, cancellationToken);
public void DeleteAudio(Guid templateId) => DeleteKind(templateId, Audio);
public void DeleteBackground(Guid channelId) => DeleteKind(channelId, Background);
public void DeleteBackground(Guid templateId) => DeleteKind(templateId, Background);
public void DeleteMusic(Guid channelId) => DeleteKind(channelId, Music);
public void DeleteTemplate(Guid templateId)
{
var dir = paths.BumperTemplateDir(templateId);
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
public string? BackgroundPath(Guid channelId, string? extension) =>
ResolvePath(channelId, Background, extension);
public string? AudioPath(Guid templateId, string? extension) =>
ResolvePath(templateId, Audio, extension);
public string? MusicPath(Guid channelId, string? extension) =>
ResolvePath(channelId, Music, extension);
public string? BackgroundPath(Guid templateId, string? extension) =>
ResolvePath(templateId, Background, extension);
private async Task SaveAsync(
Guid channelId,
Guid templateId,
string kind,
string extension,
Stream content,
CancellationToken cancellationToken
)
{
var dir = paths.BumperChannelDir(channelId);
var dir = paths.BumperTemplateDir(templateId);
Directory.CreateDirectory(dir);
RemoveExisting(dir, kind);
var path = paths.BumperFilePath(channelId, kind, NormalizeExtension(extension));
var path = paths.BumperTemplateFilePath(templateId, kind, NormalizeExtension(extension));
await using var fs = File.Create(path);
await content.CopyToAsync(fs, cancellationToken);
}
private void DeleteKind(Guid channelId, string kind)
private void DeleteKind(Guid templateId, string kind)
{
var dir = paths.BumperChannelDir(channelId);
var dir = paths.BumperTemplateDir(templateId);
if (Directory.Exists(dir))
RemoveExisting(dir, kind);
}
private string? ResolvePath(Guid channelId, string kind, string? extension)
private string? ResolvePath(Guid templateId, string kind, string? extension)
{
if (string.IsNullOrWhiteSpace(extension))
return null;
var path = paths.BumperFilePath(channelId, kind, NormalizeExtension(extension));
var path = paths.BumperTemplateFilePath(templateId, kind, NormalizeExtension(extension));
return File.Exists(path) ? path : null;
}
@@ -0,0 +1,56 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.Extensions.Options;
using TeleWave.Application.Common.Interfaces;
namespace TeleWave.Infrastructure.Media;
/// <summary>
/// Замер длительности аудиофайла через ffprobe (format.duration). Используется при загрузке звука
/// блока заставки, чтобы длина заставки шла по длине звука.
/// </summary>
public sealed class FfprobeAudioProbe(IOptions<MediaOptions> mediaOptions) : IAudioProbe
{
private readonly MediaOptions _media = mediaOptions.Value;
public async Task<TimeSpan?> TryGetDurationAsync(
string absolutePath,
CancellationToken cancellationToken
)
{
if (!File.Exists(absolutePath))
return null;
try
{
var result = await ProcessRunner.RunAsync(
_media.FfprobePath,
["-v", "quiet", "-print_format", "json", "-show_format", absolutePath],
lowPriority: false,
cancellationToken
);
if (result.ExitCode != 0)
return null;
using var doc = JsonDocument.Parse(result.StdOut);
if (
doc.RootElement.TryGetProperty("format", out var format)
&& format.TryGetProperty("duration", out var durEl)
&& double.TryParse(
durEl.GetString(),
NumberStyles.Float,
CultureInfo.InvariantCulture,
out var seconds
)
&& seconds > 0
)
return TimeSpan.FromSeconds(seconds);
}
catch (Exception ex) when (ex is JsonException or InvalidOperationException)
{
// Не удалось разобрать вывод ffprobe — длину не знаем.
}
return null;
}
}
@@ -79,12 +79,12 @@ public sealed class MediaPathResolver
}
}
public string BumperChannelDir(Guid channelId) =>
EnsureWithinRoot(Path.Combine(BumpersDir, channelId.ToString("N")));
public string BumperTemplateDir(Guid templateId) =>
EnsureWithinRoot(Path.Combine(BumpersDir, templateId.ToString("N")));
/// <summary>Путь к файлу шаблона заставки (kind — «background»/«music», extension — с точкой).</summary>
public string BumperFilePath(Guid channelId, string kind, string extension) =>
EnsureWithinRoot(Path.Combine(BumpersDir, channelId.ToString("N"), kind + extension));
/// <summary>Путь к файлу блока заставки (kind — «audio»/«background», extension — с точкой).</summary>
public string BumperTemplateFilePath(Guid templateId, string kind, string extension) =>
EnsureWithinRoot(Path.Combine(BumpersDir, templateId.ToString("N"), kind + extension));
public string OriginalPath(Guid assetId, string extension) =>
EnsureWithinRoot(Path.Combine(OriginalsDir, assetId.ToString("N") + extension));