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:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user