Add TV bumpers functionality: introduce configuration options for bumpers in .env.example and appsettings.json, enhance ChannelEndpoints to manage jingles and bumper assets, and update Channel and ScheduleEntry models to support bumper logic. Implement validation for bumper settings and integrate bumper handling in scheduling logic.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
namespace TeleWave.Domain.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Кэш отрендеренной ТВ-заставки перехода. Один сгенерированный <see cref="MediaAssetId"/> на
|
||||
/// уникальную комбинацию (<see cref="FromShowId"/> → <see cref="ToShowId"/>) при данной
|
||||
/// <see cref="Signature"/> (хэш названий шоу и версии шаблона). Переиспользуется между днями и
|
||||
/// каналами; при смене названий/шаблона <see cref="Signature"/> меняется и рендерится новый ассет.
|
||||
/// </summary>
|
||||
public class BumperAsset
|
||||
{
|
||||
public Guid Id { get; private set; }
|
||||
public Guid FromShowId { get; private set; }
|
||||
public Guid ToShowId { get; private set; }
|
||||
|
||||
/// <summary>Хэш входных данных рендера (названия «из/в» + версия шаблона).</summary>
|
||||
public string Signature { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>Сгенерированный ассет-заставка (нарезан в assets/{id}, статус Ready).</summary>
|
||||
public Guid MediaAssetId { get; private set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
private BumperAsset() { }
|
||||
|
||||
public static BumperAsset Create(
|
||||
Guid fromShowId,
|
||||
Guid toShowId,
|
||||
string signature,
|
||||
Guid mediaAssetId
|
||||
) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
FromShowId = fromShowId,
|
||||
ToShowId = toShowId,
|
||||
Signature = signature,
|
||||
MediaAssetId = mediaAssetId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace TeleWave.Domain.Broadcast;
|
||||
|
||||
/// <summary>Гарнитура текста на заставке. Маппится на конкретный TTF в конфигурации рендерера.</summary>
|
||||
public enum BumperFont
|
||||
{
|
||||
/// <summary>Гротеск (DejaVu Sans Bold) — по умолчанию.</summary>
|
||||
Sans,
|
||||
|
||||
/// <summary>Антиква (DejaVu Serif Bold).</summary>
|
||||
Serif,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace TeleWave.Domain.Broadcast;
|
||||
|
||||
/// <summary>Какие заставки вставлять на переходах.</summary>
|
||||
public enum BumperMode
|
||||
{
|
||||
/// <summary>Только динамические «Сейчас/Далее», отрисованные по оформлению канала.</summary>
|
||||
Dynamic,
|
||||
|
||||
/// <summary>Только готовые ролики-джинглы из пула канала (по кругу).</summary>
|
||||
Static,
|
||||
|
||||
/// <summary>И то, и другое — чередуя на соседних переходах.</summary>
|
||||
Both,
|
||||
}
|
||||
@@ -10,6 +10,7 @@ public class Channel
|
||||
private readonly List<ChannelShow> _shows = new();
|
||||
private readonly List<ChannelAd> _ads = new();
|
||||
private readonly List<ProgrammingOverride> _overrides = new();
|
||||
private readonly List<ChannelJingle> _jingles = new();
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
@@ -22,6 +23,49 @@ public class Channel
|
||||
public AdInsertion AdInsertion { get; private set; }
|
||||
public int AdsPerBreak { get; private set; }
|
||||
|
||||
/// <summary>Вставлять ли ТВ-заставки на переходах между разными шоу.</summary>
|
||||
public bool BumpersEnabled { get; private set; }
|
||||
|
||||
/// <summary>Какие заставки вставлять: динамические «Сейчас/Далее», статичные джинглы или оба.</summary>
|
||||
public BumperMode BumperMode { get; private set; }
|
||||
|
||||
/// <summary>Расширение загруженного фона (с точкой) или null — тогда синтезируется градиент.</summary>
|
||||
public string? BumperBackgroundExtension { get; private set; }
|
||||
|
||||
/// <summary>Расширение загруженной музыки (с точкой) или null — тогда синтезируется джингл.</summary>
|
||||
public string? BumperMusicExtension { get; private set; }
|
||||
|
||||
/// <summary>Счётчик версии файлов заставки (фон/музыка). Входит в кэш-ключ, чтобы замена файла тем
|
||||
/// же именем пересобирала уже отрендеренные динамические заставки.</summary>
|
||||
public int BumperRevision { get; private set; }
|
||||
|
||||
/// <summary>Курсор ротации пула джинглов.</summary>
|
||||
public int NextJingleIndex { get; private set; }
|
||||
|
||||
// ── Оформление и правила ТВ-заставок (значения на канал; см. UpdateBumperSettings) ──
|
||||
public int BumperDurationSeconds { get; private set; }
|
||||
public string BumperBackgroundColor { get; private set; } = DefaultBackgroundColor;
|
||||
public string BumperBackgroundColor2 { get; private set; } = DefaultBackgroundColor2;
|
||||
public string BumperAccentColor { get; private set; } = DefaultAccentColor;
|
||||
public string BumperTextColor { get; private set; } = DefaultTextColor;
|
||||
public BumperFont BumperFont { get; private set; }
|
||||
public string BumperNowLabel { get; private set; } = DefaultNowLabel;
|
||||
public string BumperNextLabel { get; private set; } = DefaultNextLabel;
|
||||
|
||||
/// <summary>Не вставлять заставку чаще, чем раз в N минут (0 — на каждом подходящем переходе).</summary>
|
||||
public int BumperMinIntervalMinutes { get; private set; }
|
||||
|
||||
/// <summary>Ставить заставку только на смене шоу (иначе — и внутри марафона одного шоу).</summary>
|
||||
public bool BumperOnlyBetweenDifferentShows { get; private set; }
|
||||
|
||||
private const int DefaultBumperDurationSeconds = 8;
|
||||
private const string DefaultBackgroundColor = "0x0b1020";
|
||||
private const string DefaultBackgroundColor2 = "0x1e293b";
|
||||
private const string DefaultAccentColor = "0x38bdf8";
|
||||
private const string DefaultTextColor = "white";
|
||||
private const string DefaultNowLabel = "СЕЙЧАС";
|
||||
private const string DefaultNextLabel = "ДАЛЕЕ";
|
||||
|
||||
/// <summary>Ассет-заглушка на случай пустого расписания (аварийная подстраховка).</summary>
|
||||
public Guid? FillerAssetId { get; private set; }
|
||||
|
||||
@@ -36,6 +80,9 @@ public class Channel
|
||||
public IReadOnlyList<ChannelAd> Ads => _ads;
|
||||
public IReadOnlyList<ProgrammingOverride> Overrides => _overrides;
|
||||
|
||||
/// <summary>Пул джинглов-отбивок; порядок ротации — по <see cref="ChannelJingle.Position"/>.</summary>
|
||||
public IReadOnlyList<ChannelJingle> Jingles => _jingles;
|
||||
|
||||
private Channel() { }
|
||||
|
||||
public static Channel Create(string name, string slug, DateTimeOffset epochUtc) =>
|
||||
@@ -48,6 +95,22 @@ public class Channel
|
||||
EpochUtc = epochUtc,
|
||||
AdInsertion = AdInsertion.BetweenBlocks,
|
||||
AdsPerBreak = 1,
|
||||
BumpersEnabled = false,
|
||||
BumperMode = BumperMode.Dynamic,
|
||||
BumperBackgroundExtension = null,
|
||||
BumperMusicExtension = null,
|
||||
BumperRevision = 0,
|
||||
NextJingleIndex = 0,
|
||||
BumperDurationSeconds = DefaultBumperDurationSeconds,
|
||||
BumperBackgroundColor = DefaultBackgroundColor,
|
||||
BumperBackgroundColor2 = DefaultBackgroundColor2,
|
||||
BumperAccentColor = DefaultAccentColor,
|
||||
BumperTextColor = DefaultTextColor,
|
||||
BumperFont = BumperFont.Sans,
|
||||
BumperNowLabel = DefaultNowLabel,
|
||||
BumperNextLabel = DefaultNextLabel,
|
||||
BumperMinIntervalMinutes = 0,
|
||||
BumperOnlyBetweenDifferentShows = true,
|
||||
NextAdIndex = 0,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
@@ -57,6 +120,7 @@ public class Channel
|
||||
bool isEnabled,
|
||||
AdInsertion adInsertion,
|
||||
int adsPerBreak,
|
||||
bool bumpersEnabled,
|
||||
Guid? fillerAssetId
|
||||
)
|
||||
{
|
||||
@@ -64,9 +128,90 @@ public class Channel
|
||||
IsEnabled = isEnabled;
|
||||
AdInsertion = adInsertion;
|
||||
AdsPerBreak = adsPerBreak;
|
||||
BumpersEnabled = bumpersEnabled;
|
||||
FillerAssetId = fillerAssetId;
|
||||
}
|
||||
|
||||
/// <summary>Оформление и правила ТВ-заставок канала. Цвета — в нотации ffmpeg (0xRRGGBB или имя).</summary>
|
||||
public void UpdateBumperSettings(
|
||||
BumperMode mode,
|
||||
int durationSeconds,
|
||||
string backgroundColor,
|
||||
string backgroundColor2,
|
||||
string accentColor,
|
||||
string textColor,
|
||||
BumperFont font,
|
||||
string nowLabel,
|
||||
string nextLabel,
|
||||
int minIntervalMinutes,
|
||||
bool onlyBetweenDifferentShows
|
||||
)
|
||||
{
|
||||
BumperMode = mode;
|
||||
BumperDurationSeconds = durationSeconds;
|
||||
BumperBackgroundColor = backgroundColor;
|
||||
BumperBackgroundColor2 = backgroundColor2;
|
||||
BumperAccentColor = accentColor;
|
||||
BumperTextColor = textColor;
|
||||
BumperFont = font;
|
||||
BumperNowLabel = nowLabel;
|
||||
BumperNextLabel = nextLabel;
|
||||
BumperMinIntervalMinutes = Math.Max(0, minIntervalMinutes);
|
||||
BumperOnlyBetweenDifferentShows = onlyBetweenDifferentShows;
|
||||
}
|
||||
|
||||
/// <summary>Отметить загруженный фон (extension — с точкой, нижний регистр). Меняет ревизию.</summary>
|
||||
public void SetBumperBackground(string extension)
|
||||
{
|
||||
BumperBackgroundExtension = extension;
|
||||
BumperRevision++;
|
||||
}
|
||||
|
||||
public void ClearBumperBackground()
|
||||
{
|
||||
if (BumperBackgroundExtension is null)
|
||||
return;
|
||||
BumperBackgroundExtension = null;
|
||||
BumperRevision++;
|
||||
}
|
||||
|
||||
/// <summary>Отметить загруженную музыку (extension — с точкой, нижний регистр). Меняет ревизию.</summary>
|
||||
public void SetBumperMusic(string extension)
|
||||
{
|
||||
BumperMusicExtension = extension;
|
||||
BumperRevision++;
|
||||
}
|
||||
|
||||
public void ClearBumperMusic()
|
||||
{
|
||||
if (BumperMusicExtension is null)
|
||||
return;
|
||||
BumperMusicExtension = null;
|
||||
BumperRevision++;
|
||||
}
|
||||
|
||||
public ChannelJingle AddJingle(Guid mediaAssetId)
|
||||
{
|
||||
var nextPosition = _jingles.Count == 0 ? 0 : _jingles.Max(j => j.Position) + 1;
|
||||
var jingle = ChannelJingle.Create(Id, mediaAssetId, nextPosition);
|
||||
_jingles.Add(jingle);
|
||||
return jingle;
|
||||
}
|
||||
|
||||
public bool RemoveJingle(Guid channelJingleId)
|
||||
{
|
||||
var jingle = _jingles.FirstOrDefault(j => j.Id == channelJingleId);
|
||||
if (jingle is null)
|
||||
return false;
|
||||
_jingles.Remove(jingle);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HasJingle(Guid mediaAssetId) => _jingles.Any(j => j.MediaAssetId == mediaAssetId);
|
||||
|
||||
/// <summary>Планировщик двигает курсор пула джинглов по мере вставки отбивок.</summary>
|
||||
public void SetNextJingleIndex(int index) => NextJingleIndex = index;
|
||||
|
||||
public ChannelShow? FindShow(Guid channelShowId) => _shows.FirstOrDefault(s => s.Id == channelShowId);
|
||||
|
||||
public ChannelShow AddShow(Guid showId, int weight, BlockMode blockMode, int blockValue)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TeleWave.Domain.Broadcast;
|
||||
|
||||
/// <summary>Готовый ролик-джингл (отбивка) в пуле канала. Крутятся по кругу в порядке <see cref="Position"/>
|
||||
/// на переходах между шоу, когда режим заставок — Static или Both.</summary>
|
||||
public class ChannelJingle
|
||||
{
|
||||
public Guid Id { get; private set; }
|
||||
public Guid ChannelId { get; private set; }
|
||||
public Guid MediaAssetId { get; private set; }
|
||||
public int Position { get; private set; }
|
||||
|
||||
private ChannelJingle() { }
|
||||
|
||||
internal static ChannelJingle Create(Guid channelId, Guid mediaAssetId, int position) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ChannelId = channelId,
|
||||
MediaAssetId = mediaAssetId,
|
||||
Position = position,
|
||||
};
|
||||
}
|
||||
@@ -56,4 +56,23 @@ public class ScheduleEntry
|
||||
StartsAtUtc = startsAtUtc,
|
||||
EndsAtUtc = endsAtUtc,
|
||||
};
|
||||
|
||||
/// <summary>Заставка-переход. <paramref name="showId"/> — следующее шоу (для EPG/справки).</summary>
|
||||
public static ScheduleEntry Bumper(
|
||||
Guid channelId,
|
||||
Guid mediaAssetId,
|
||||
DateTimeOffset startsAtUtc,
|
||||
DateTimeOffset endsAtUtc,
|
||||
Guid? showId
|
||||
) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ChannelId = channelId,
|
||||
MediaAssetId = mediaAssetId,
|
||||
Kind = ScheduleEntryKind.Bumper,
|
||||
StartsAtUtc = startsAtUtc,
|
||||
EndsAtUtc = endsAtUtc,
|
||||
ShowId = showId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,4 +8,7 @@ public enum ScheduleEntryKind
|
||||
|
||||
/// <summary>Рекламная врезка.</summary>
|
||||
Ad,
|
||||
|
||||
/// <summary>ТВ-заставка на переходе между шоу («Сейчас: X · Далее: Y»).</summary>
|
||||
Bumper,
|
||||
}
|
||||
|
||||
@@ -19,14 +19,18 @@ public static class SchedulePlanner
|
||||
var byShowId = input.Shows.ToDictionary(s => s.ShowId);
|
||||
var nextEpisode = input.Shows.ToDictionary(s => s.ChannelShowId, s => s.NextEpisodeIndex);
|
||||
var nextAd = input.NextAdIndex;
|
||||
var nextJingle = input.NextJingleIndex;
|
||||
|
||||
// Есть ли вообще из чего строить эфир.
|
||||
var anyPlayable = input.Shows.Any(s => s.Weight > 0 && s.EpisodeAssetIds.Count > 0);
|
||||
if (!anyPlayable)
|
||||
return new PlannerResult(entries, nextEpisode, nextAd);
|
||||
return new PlannerResult(entries, nextEpisode, nextAd, nextJingle);
|
||||
|
||||
var cursor = input.StartTime;
|
||||
var iterations = 0;
|
||||
Guid? prevShowId = null;
|
||||
DateTimeOffset? lastBumperAt = null;
|
||||
var bumperCount = 0;
|
||||
|
||||
while (cursor < input.HorizonEnd && iterations++ < IterationBackstop)
|
||||
{
|
||||
@@ -35,6 +39,28 @@ public static class SchedulePlanner
|
||||
break;
|
||||
|
||||
var pick = WeightedPick(candidates, random);
|
||||
|
||||
// ТВ-заставка на переходе. Динамическую (Сейчас/Далее) резервируем слотом фикс. длины —
|
||||
// ассет отрендерит оркестратор; статичный джингл берём готовым из пула (реальная длина).
|
||||
if (
|
||||
prevShowId is { } prev
|
||||
&& input.Bumpers is { Enabled: true } bumper
|
||||
&& (!bumper.OnlyBetweenDifferentShows || prev != pick.ShowId)
|
||||
&& (
|
||||
bumper.MinInterval <= TimeSpan.Zero
|
||||
|| lastBumperAt is not { } last
|
||||
|| cursor - last >= bumper.MinInterval
|
||||
)
|
||||
)
|
||||
{
|
||||
var bumperStart = cursor;
|
||||
if (TryPlaceBumper(entries, bumper, bumperCount, prev, pick.ShowId, input, ref nextJingle, ref cursor))
|
||||
{
|
||||
lastBumperAt = bumperStart;
|
||||
bumperCount++;
|
||||
}
|
||||
}
|
||||
|
||||
var blockStart = cursor;
|
||||
|
||||
var block = CollectBlock(pick, nextEpisode, input, cursor);
|
||||
@@ -64,9 +90,80 @@ public static class SchedulePlanner
|
||||
// Защита от зацикливания, если длительности нулевые/отсутствуют — эфир не сдвинулся.
|
||||
if (cursor <= blockStart)
|
||||
break;
|
||||
|
||||
prevShowId = pick.ShowId;
|
||||
}
|
||||
|
||||
return new PlannerResult(entries, nextEpisode, nextAd);
|
||||
return new PlannerResult(entries, nextEpisode, nextAd, nextJingle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ставит одну заставку на переходе по режиму канала. Динамическая — плейсхолдер фикс. длины
|
||||
/// (ассет подставит оркестратор). Статичная — готовый джингл из пула (реальная длина, курсор
|
||||
/// двигается). В режиме Both типы чередуются; при пустом пуле Both уходит в динамику.
|
||||
/// Возвращает true, если заставка добавлена (курсор сдвинут).
|
||||
/// </summary>
|
||||
private static bool TryPlaceBumper(
|
||||
List<PlannedEntry> entries,
|
||||
PlannerBumperConfig bumper,
|
||||
int bumperCount,
|
||||
Guid fromShowId,
|
||||
Guid toShowId,
|
||||
PlannerInput input,
|
||||
ref int nextJingle,
|
||||
ref DateTimeOffset cursor
|
||||
)
|
||||
{
|
||||
var pool = bumper.JinglePool;
|
||||
var hasPool = pool is { Count: > 0 };
|
||||
|
||||
var wantStatic =
|
||||
bumper.Mode == BumperMode.Static
|
||||
|| (bumper.Mode == BumperMode.Both && bumperCount % 2 == 1);
|
||||
|
||||
// В режиме Both при пустом пуле показываем динамику.
|
||||
if (wantStatic && !hasPool && bumper.Mode == BumperMode.Both)
|
||||
wantStatic = false;
|
||||
|
||||
if (wantStatic)
|
||||
{
|
||||
if (!hasPool)
|
||||
return false; // Static без пула — вставлять нечего.
|
||||
|
||||
var idx = ((nextJingle % pool!.Count) + pool.Count) % pool.Count;
|
||||
var assetId = pool[idx];
|
||||
nextJingle++;
|
||||
var dur = DurationOf(assetId, input);
|
||||
if (dur <= TimeSpan.Zero)
|
||||
return false;
|
||||
|
||||
var end = cursor + dur;
|
||||
entries.Add(
|
||||
new PlannedEntry(assetId, ScheduleEntryKind.Bumper, cursor, end, null, null)
|
||||
);
|
||||
cursor = end;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Динамическая заставка «Сейчас/Далее» — плейсхолдер с парой шоу для рендера.
|
||||
if (bumper.Duration <= TimeSpan.Zero)
|
||||
return false;
|
||||
|
||||
var dynEnd = cursor + bumper.Duration;
|
||||
entries.Add(
|
||||
new PlannedEntry(
|
||||
Guid.Empty,
|
||||
ScheduleEntryKind.Bumper,
|
||||
cursor,
|
||||
dynEnd,
|
||||
toShowId,
|
||||
null,
|
||||
fromShowId,
|
||||
toShowId
|
||||
)
|
||||
);
|
||||
cursor = dynEnd;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<(PlannerShow Show, int Weight)> ResolvePolicy(
|
||||
|
||||
@@ -21,6 +21,20 @@ public sealed record PlannerOverride(
|
||||
|
||||
public sealed record PlannerOverrideShow(Guid ShowId, int Weight);
|
||||
|
||||
/// <summary>
|
||||
/// Политика ТВ-заставок на переходах. <see cref="Duration"/> должна быть кратна длине сегмента
|
||||
/// (генератор выравнивает). Планировщик резервирует под заставку слот этой длины, а конкретный
|
||||
/// сгенерированный ассет подставляет уже оркестратор.
|
||||
/// </summary>
|
||||
public sealed record PlannerBumperConfig(
|
||||
bool Enabled,
|
||||
TimeSpan Duration,
|
||||
bool OnlyBetweenDifferentShows,
|
||||
TimeSpan MinInterval,
|
||||
BumperMode Mode = BumperMode.Dynamic,
|
||||
IReadOnlyList<Guid>? JinglePool = null
|
||||
);
|
||||
|
||||
/// <summary>Полный вход планировщика для одного прогона по каналу.</summary>
|
||||
public sealed record PlannerInput(
|
||||
Guid ChannelId,
|
||||
@@ -32,22 +46,31 @@ public sealed record PlannerInput(
|
||||
IReadOnlyDictionary<Guid, TimeSpan> Durations,
|
||||
IReadOnlyList<PlannerOverride> Overrides,
|
||||
DateTimeOffset StartTime,
|
||||
DateTimeOffset HorizonEnd
|
||||
DateTimeOffset HorizonEnd,
|
||||
PlannerBumperConfig? Bumpers = null,
|
||||
int NextJingleIndex = 0
|
||||
);
|
||||
|
||||
/// <summary>Одна запланированная запись (ещё не доменная сущность).</summary>
|
||||
/// <summary>
|
||||
/// Одна запланированная запись (ещё не доменная сущность). Для заставок (<see cref="Kind"/> ==
|
||||
/// <see cref="ScheduleEntryKind.Bumper"/>) <see cref="MediaAssetId"/> пуст — его подставит
|
||||
/// оркестратор после рендера по паре (<see cref="FromShowId"/> → <see cref="ToShowId"/>).
|
||||
/// </summary>
|
||||
public sealed record PlannedEntry(
|
||||
Guid MediaAssetId,
|
||||
ScheduleEntryKind Kind,
|
||||
DateTimeOffset StartsAtUtc,
|
||||
DateTimeOffset EndsAtUtc,
|
||||
Guid? ShowId,
|
||||
int? EpisodeIndex
|
||||
int? EpisodeIndex,
|
||||
Guid? FromShowId = null,
|
||||
Guid? ToShowId = null
|
||||
);
|
||||
|
||||
/// <summary>Результат прогона: новые записи + обновлённые курсоры (серий по каждому ChannelShow и рекламы).</summary>
|
||||
public sealed record PlannerResult(
|
||||
IReadOnlyList<PlannedEntry> Entries,
|
||||
IReadOnlyDictionary<Guid, int> NextEpisodeIndexByChannelShow,
|
||||
int NextAdIndex
|
||||
int NextAdIndex,
|
||||
int NextJingleIndex
|
||||
);
|
||||
|
||||
@@ -55,6 +55,25 @@ public class MediaAsset
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ассет, сгенерированный системой (ТВ-заставка). Нарезку делает не общий пайплайн обработки, а
|
||||
/// специализированный рендерер, поэтому сразу помечаем <see cref="MarkReady"/> после создания.
|
||||
/// </summary>
|
||||
public static MediaAsset RegisterGenerated(string displayName)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
return new MediaAsset
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OriginalFileName = displayName,
|
||||
OriginalExtension = string.Empty,
|
||||
Source = MediaSource.Generated,
|
||||
Status = MediaAssetStatus.Pending,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
}
|
||||
|
||||
public void MarkProcessing()
|
||||
{
|
||||
Status = MediaAssetStatus.Processing;
|
||||
|
||||
@@ -8,4 +8,8 @@ public enum MediaSource
|
||||
|
||||
/// <summary>Положен вручную в inbox/ и подобран сканером.</summary>
|
||||
Inbox,
|
||||
|
||||
/// <summary>Сгенерирован системой (например, ТВ-заставка «Сейчас/Далее»), а не загружен человеком.
|
||||
/// Такие ассеты не показываются в списке медиа и создаются сразу готовыми (нарезка своя).</summary>
|
||||
Generated,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user