Refactor bumper template background management: rename background upload endpoint to reflect new functionality, update related command and handler to use image ID instead of extension, and adjust data models to support image references. Remove obsolete background handling code and enhance UI components for improved image selection and display.
This commit is contained in:
@@ -75,7 +75,7 @@ public static class ChannelEndpoints
|
|||||||
.MapDelete("/{id:guid}/bumper/templates/{templateId:guid}/audio", ClearTemplateAudio)
|
.MapDelete("/{id:guid}/bumper/templates/{templateId:guid}/audio", ClearTemplateAudio)
|
||||||
.Produces(StatusCodes.Status204NoContent);
|
.Produces(StatusCodes.Status204NoContent);
|
||||||
admin
|
admin
|
||||||
.MapPut("/{id:guid}/bumper/templates/{templateId:guid}/background", UploadTemplateBackground)
|
.MapPut("/{id:guid}/bumper/templates/{templateId:guid}/background", SetTemplateBackground)
|
||||||
.Produces(StatusCodes.Status204NoContent);
|
.Produces(StatusCodes.Status204NoContent);
|
||||||
admin
|
admin
|
||||||
.MapDelete("/{id:guid}/bumper/templates/{templateId:guid}/background", ClearTemplateBackground)
|
.MapDelete("/{id:guid}/bumper/templates/{templateId:guid}/background", ClearTemplateBackground)
|
||||||
@@ -333,27 +333,18 @@ public static class ChannelEndpoints
|
|||||||
return result.ToHttpResult();
|
return result.ToHttpResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> UploadTemplateBackground(
|
private static async Task<IResult> SetTemplateBackground(
|
||||||
Guid id,
|
Guid id,
|
||||||
Guid templateId,
|
Guid templateId,
|
||||||
string fileName,
|
SetBumperTemplateBackgroundBody body,
|
||||||
HttpRequest request,
|
|
||||||
IBumperTemplateStorage storage,
|
|
||||||
ISender sender,
|
ISender sender,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (ResolveBumperExtension(fileName, request, BumperFiles.BackgroundExtensions) is not { } ext)
|
|
||||||
return ChannelErrors.InvalidBumperFile.ToProblem();
|
|
||||||
|
|
||||||
await storage.SaveBackgroundAsync(templateId, ext, request.Body, cancellationToken);
|
|
||||||
|
|
||||||
var result = await sender.Send(
|
var result = await sender.Send(
|
||||||
new SetBumperTemplateBackgroundCommand(id, templateId, ext),
|
new SetBumperTemplateBackgroundCommand(id, templateId, body.ImageId),
|
||||||
cancellationToken
|
cancellationToken
|
||||||
);
|
);
|
||||||
if (!result.IsSuccess)
|
|
||||||
storage.DeleteBackground(templateId);
|
|
||||||
return result.ToHttpResult();
|
return result.ToHttpResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,6 +530,8 @@ public sealed record AddChannelAdBody(Guid MediaAssetId);
|
|||||||
|
|
||||||
public sealed record AddBumperTemplateBody(string Name);
|
public sealed record AddBumperTemplateBody(string Name);
|
||||||
|
|
||||||
|
public sealed record SetBumperTemplateBackgroundBody(Guid ImageId);
|
||||||
|
|
||||||
public sealed record UpdateBumperTemplateBody(
|
public sealed record UpdateBumperTemplateBody(
|
||||||
string Name,
|
string Name,
|
||||||
string BackgroundColor,
|
string BackgroundColor,
|
||||||
@@ -547,24 +540,11 @@ public sealed record UpdateBumperTemplateBody(
|
|||||||
string TextColor
|
string TextColor
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>Ограничения на загружаемые файлы блока заставки (звук/фон-картинка).</summary>
|
/// <summary>Ограничения на загружаемый звук блока заставки (фон-картинка — через общий реестр).</summary>
|
||||||
internal static class BumperFiles
|
internal static class BumperFiles
|
||||||
{
|
{
|
||||||
public const long MaxBytes = 200L * 1024 * 1024; // 200 МБ
|
public const long MaxBytes = 200L * 1024 * 1024; // 200 МБ
|
||||||
|
|
||||||
// Фон блока — только картинка (видео-фоны в новой модели не поддерживаются).
|
|
||||||
public static readonly IReadOnlySet<string> BackgroundExtensions = new HashSet<string>(
|
|
||||||
StringComparer.OrdinalIgnoreCase
|
|
||||||
)
|
|
||||||
{
|
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".png",
|
|
||||||
".webp",
|
|
||||||
".bmp",
|
|
||||||
".gif",
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly IReadOnlySet<string> AudioExtensions = new HashSet<string>(
|
public static readonly IReadOnlySet<string> AudioExtensions = new HashSet<string>(
|
||||||
StringComparer.OrdinalIgnoreCase
|
StringComparer.OrdinalIgnoreCase
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using LiteCqrs;
|
using LiteCqrs;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using TeleWave.Api.Common;
|
using TeleWave.Api.Common;
|
||||||
using TeleWave.Application.Common.Interfaces;
|
using TeleWave.Application.Common.Interfaces;
|
||||||
using TeleWave.Application.Images.DeleteImage;
|
using TeleWave.Application.Images.DeleteImage;
|
||||||
@@ -48,12 +47,7 @@ public static class MetadataEndpoints
|
|||||||
.Produces(StatusCodes.Status204NoContent);
|
.Produces(StatusCodes.Status204NoContent);
|
||||||
admin.MapPost("/shows/{showId:guid}/refresh-episodes", RefreshEpisodes).Produces<int>();
|
admin.MapPost("/shows/{showId:guid}/refresh-episodes", RefreshEpisodes).Produces<int>();
|
||||||
|
|
||||||
// Кадры серий отдаём публично (просто картинки, id не угадать) — чтобы работал <img src>.
|
// Постеры шоу и кадры серий теперь в общем реестре и отдаются по /api/images/{id}.
|
||||||
// Постеры шоу теперь в общем реестре и отдаются по /api/images/{id}.
|
|
||||||
app.MapGet("/api/metadata/episodes/{episodeId:guid}/still", ServeStill)
|
|
||||||
.WithTags("Metadata")
|
|
||||||
.Produces(StatusCodes.Status200OK);
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,37 +175,6 @@ public static class MetadataEndpoints
|
|||||||
);
|
);
|
||||||
return result.ToHttpResult();
|
return result.ToHttpResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> ServeStill(
|
|
||||||
Guid episodeId,
|
|
||||||
IAppDbContext dbContext,
|
|
||||||
IMetadataImageStore imageStore,
|
|
||||||
CancellationToken cancellationToken
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var path = await dbContext.Shows.AsNoTracking()
|
|
||||||
.SelectMany(s => s.Episodes)
|
|
||||||
.Where(e => e.Id == episodeId)
|
|
||||||
.Select(e => e.StillPath)
|
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
return ServeImage(path, imageStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IResult ServeImage(string? relativePath, IMetadataImageStore imageStore)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(relativePath))
|
|
||||||
return Results.NotFound();
|
|
||||||
var abs = imageStore.ResolveAbsolutePath(relativePath);
|
|
||||||
return abs is null ? Results.NotFound() : Results.File(abs, ContentTypeFor(Path.GetExtension(abs)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ContentTypeFor(string extension) =>
|
|
||||||
extension.ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
".png" => "image/png",
|
|
||||||
".webp" => "image/webp",
|
|
||||||
_ => "image/jpeg",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record ApplyMetadataBody(string Provider, string ExternalId);
|
public sealed record ApplyMetadataBody(string Provider, string ExternalId);
|
||||||
|
|||||||
+3
-5
@@ -5,10 +5,8 @@ using TeleWave.Application.Common.Models;
|
|||||||
|
|
||||||
namespace TeleWave.Application.Broadcast.Bumpers;
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
public sealed class ClearBumperTemplateBackgroundCommandHandler(
|
public sealed class ClearBumperTemplateBackgroundCommandHandler(IAppDbContext dbContext)
|
||||||
IAppDbContext dbContext,
|
: ICommandHandler<ClearBumperTemplateBackgroundCommand, Result>
|
||||||
IBumperTemplateStorage storage
|
|
||||||
) : ICommandHandler<ClearBumperTemplateBackgroundCommand, Result>
|
|
||||||
{
|
{
|
||||||
public async Task<Result> Handle(
|
public async Task<Result> Handle(
|
||||||
ClearBumperTemplateBackgroundCommand command,
|
ClearBumperTemplateBackgroundCommand command,
|
||||||
@@ -25,8 +23,8 @@ public sealed class ClearBumperTemplateBackgroundCommandHandler(
|
|||||||
if (template is null)
|
if (template is null)
|
||||||
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
|
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
|
||||||
|
|
||||||
|
// Отвязываем фон; сама картинка остаётся в галерее.
|
||||||
template.ClearBackgroundImage();
|
template.ClearBackgroundImage();
|
||||||
storage.DeleteBackground(command.TemplateId);
|
|
||||||
return Result.Success();
|
return Result.Success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -12,6 +12,7 @@ public sealed class RenderBumperPreviewQueryHandler(
|
|||||||
IAppDbContext dbContext,
|
IAppDbContext dbContext,
|
||||||
IBumperRenderer renderer,
|
IBumperRenderer renderer,
|
||||||
IBumperTemplateStorage storage,
|
IBumperTemplateStorage storage,
|
||||||
|
IImageStore imageStore,
|
||||||
IOptions<BumperOptions> bumperOptions,
|
IOptions<BumperOptions> bumperOptions,
|
||||||
IOptions<StreamingOptions> streamingOptions
|
IOptions<StreamingOptions> streamingOptions
|
||||||
) : IQueryHandler<RenderBumperPreviewQuery, Result<Guid>>
|
) : IQueryHandler<RenderBumperPreviewQuery, Result<Guid>>
|
||||||
@@ -40,6 +41,18 @@ public sealed class RenderBumperPreviewQueryHandler(
|
|||||||
|
|
||||||
var (fromName, toName) = await SampleNamesAsync(channel, cancellationToken);
|
var (fromName, toName) = await SampleNamesAsync(channel, cancellationToken);
|
||||||
|
|
||||||
|
// Фон блока — из общего реестра по id.
|
||||||
|
string? backgroundPath = null;
|
||||||
|
if (template.BackgroundImageId is { } bgId)
|
||||||
|
{
|
||||||
|
var bgExt = await dbContext.Images.AsNoTracking()
|
||||||
|
.Where(i => i.Id == bgId)
|
||||||
|
.Select(i => i.FileExtension)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
if (bgExt is not null)
|
||||||
|
backgroundPath = imageStore.ResolvePath(bgId, bgExt);
|
||||||
|
}
|
||||||
|
|
||||||
var seconds =
|
var seconds =
|
||||||
template.AudioDurationSeconds is { } d and > 0 ? d : DefaultBumperDurationSeconds;
|
template.AudioDurationSeconds is { } d and > 0 ? d : DefaultBumperDurationSeconds;
|
||||||
var aligned = (int)(
|
var aligned = (int)(
|
||||||
@@ -59,7 +72,7 @@ public sealed class RenderBumperPreviewQueryHandler(
|
|||||||
fromName,
|
fromName,
|
||||||
channel.BumperNextLabel,
|
channel.BumperNextLabel,
|
||||||
toName,
|
toName,
|
||||||
storage.BackgroundPath(template.Id, template.BackgroundImageExtension),
|
backgroundPath,
|
||||||
storage.AudioPath(template.Id, template.AudioExtension),
|
storage.AudioPath(template.Id, template.AudioExtension),
|
||||||
// Постер зависит от конкретного «следующего» шоу — в превью не подставляем.
|
// Постер зависит от конкретного «следующего» шоу — в превью не подставляем.
|
||||||
null
|
null
|
||||||
|
|||||||
+2
-2
@@ -3,9 +3,9 @@ using TeleWave.Application.Common.Models;
|
|||||||
|
|
||||||
namespace TeleWave.Application.Broadcast.Bumpers;
|
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||||
|
|
||||||
/// <summary>Отметить загруженную фон-картинку блока (расширение — с точкой).</summary>
|
/// <summary>Привязать фон-картинку блока по ссылке на изображение из реестра (галерея).</summary>
|
||||||
public sealed record SetBumperTemplateBackgroundCommand(
|
public sealed record SetBumperTemplateBackgroundCommand(
|
||||||
Guid ChannelId,
|
Guid ChannelId,
|
||||||
Guid TemplateId,
|
Guid TemplateId,
|
||||||
string Extension
|
Guid ImageId
|
||||||
) : ICommand<Result>;
|
) : ICommand<Result>;
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ public sealed class SetBumperTemplateBackgroundCommandHandler(IAppDbContext dbCo
|
|||||||
if (template is null)
|
if (template is null)
|
||||||
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
|
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
|
||||||
|
|
||||||
template.SetBackgroundImage(command.Extension);
|
template.SetBackgroundImage(command.ImageId);
|
||||||
return Result.Success();
|
return Result.Success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public sealed record BumperTemplateDto(
|
|||||||
string BackgroundColor2,
|
string BackgroundColor2,
|
||||||
string AccentColor,
|
string AccentColor,
|
||||||
string TextColor,
|
string TextColor,
|
||||||
bool HasBackground,
|
Guid? BackgroundImageId,
|
||||||
bool HasAudio,
|
bool HasAudio,
|
||||||
double? AudioDurationSeconds
|
double? AudioDurationSeconds
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
|||||||
t.BackgroundColor2,
|
t.BackgroundColor2,
|
||||||
t.AccentColor,
|
t.AccentColor,
|
||||||
t.TextColor,
|
t.TextColor,
|
||||||
t.BackgroundImageExtension is not null,
|
t.BackgroundImageId,
|
||||||
t.AudioExtension is not null,
|
t.AudioExtension is not null,
|
||||||
t.AudioDurationSeconds
|
t.AudioDurationSeconds
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -216,6 +216,25 @@ public sealed class ScheduleGenerator(
|
|||||||
)
|
)
|
||||||
posterByShow[p.Id] = (p.ImageId, abs);
|
posterByShow[p.Id] = (p.ImageId, abs);
|
||||||
|
|
||||||
|
// Фон-картинки блоков (из реестра) — абсолютные пути по id.
|
||||||
|
var bgImageIds = channel.BumperTemplates
|
||||||
|
.Where(t => t.BackgroundImageId != null)
|
||||||
|
.Select(t => t.BackgroundImageId!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var bgExtById = await dbContext.Images.AsNoTracking()
|
||||||
|
.Where(i => bgImageIds.Contains(i.Id))
|
||||||
|
.Select(i => new { i.Id, i.FileExtension })
|
||||||
|
.ToDictionaryAsync(i => i.Id, i => i.FileExtension, cancellationToken);
|
||||||
|
var bgByTemplate = new Dictionary<Guid, string>();
|
||||||
|
foreach (var t in channel.BumperTemplates)
|
||||||
|
if (
|
||||||
|
t.BackgroundImageId is { } bgId
|
||||||
|
&& bgExtById.TryGetValue(bgId, out var bgExt)
|
||||||
|
&& imageStore.ResolvePath(bgId, bgExt) is { } bgAbs
|
||||||
|
)
|
||||||
|
bgByTemplate[t.Id] = bgAbs;
|
||||||
|
|
||||||
// Кандидаты из кэша + статусы их ассетов (годятся только Ready — файлы могли удалить).
|
// Кандидаты из кэша + статусы их ассетов (годятся только Ready — файлы могли удалить).
|
||||||
var cached = await dbContext.BumperAssets.AsNoTracking()
|
var cached = await dbContext.BumperAssets.AsNoTracking()
|
||||||
.Where(b => fromIds.Contains(b.FromShowId) && toIds.Contains(b.ToShowId))
|
.Where(b => fromIds.Contains(b.FromShowId) && toIds.Contains(b.ToShowId))
|
||||||
@@ -245,6 +264,7 @@ public sealed class ScheduleGenerator(
|
|||||||
var poster = posterByShow.TryGetValue(combo.To, out var pr) ? pr : default;
|
var poster = posterByShow.TryGetValue(combo.To, out var pr) ? pr : default;
|
||||||
var posterToken = poster.ImageId == Guid.Empty ? "-" : poster.ImageId.ToString();
|
var posterToken = poster.ImageId == Guid.Empty ? "-" : poster.ImageId.ToString();
|
||||||
var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath;
|
var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath;
|
||||||
|
var bgAbs = bgByTemplate.GetValueOrDefault(template.Id);
|
||||||
var aligned = AlignedDurationSeconds(TemplateDurationSeconds(template));
|
var aligned = AlignedDurationSeconds(TemplateDurationSeconds(template));
|
||||||
var signature = ComputeSignature(channel, template, fromName, toName, aligned, posterToken);
|
var signature = ComputeSignature(channel, template, fromName, toName, aligned, posterToken);
|
||||||
|
|
||||||
@@ -272,6 +292,7 @@ public sealed class ScheduleGenerator(
|
|||||||
aligned,
|
aligned,
|
||||||
signature,
|
signature,
|
||||||
posterAbs,
|
posterAbs,
|
||||||
|
bgAbs,
|
||||||
cancellationToken
|
cancellationToken
|
||||||
);
|
);
|
||||||
result[combo] = assetId;
|
result[combo] = assetId;
|
||||||
@@ -300,14 +321,22 @@ public sealed class ScheduleGenerator(
|
|||||||
int alignedDurationSeconds,
|
int alignedDurationSeconds,
|
||||||
string signature,
|
string signature,
|
||||||
string? posterAbsolutePath,
|
string? posterAbsolutePath,
|
||||||
|
string? backgroundAbsolutePath,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var asset = MediaAsset.RegisterGenerated($"Заставка: {fromName} → {toName}");
|
var asset = MediaAsset.RegisterGenerated($"Заставка: {fromName} → {toName}");
|
||||||
var posterAbs = posterAbsolutePath;
|
|
||||||
var render = await bumperRenderer.RenderAsync(
|
var render = await bumperRenderer.RenderAsync(
|
||||||
asset.Id,
|
asset.Id,
|
||||||
BuildSpec(channel, template, alignedDurationSeconds, fromName, toName, posterAbs),
|
BuildSpec(
|
||||||
|
channel,
|
||||||
|
template,
|
||||||
|
alignedDurationSeconds,
|
||||||
|
fromName,
|
||||||
|
toName,
|
||||||
|
posterAbsolutePath,
|
||||||
|
backgroundAbsolutePath
|
||||||
|
),
|
||||||
cancellationToken
|
cancellationToken
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -335,7 +364,8 @@ public sealed class ScheduleGenerator(
|
|||||||
int alignedDurationSeconds,
|
int alignedDurationSeconds,
|
||||||
string fromName,
|
string fromName,
|
||||||
string toName,
|
string toName,
|
||||||
string? posterAbsolutePath
|
string? posterAbsolutePath,
|
||||||
|
string? backgroundAbsolutePath
|
||||||
) =>
|
) =>
|
||||||
new(
|
new(
|
||||||
alignedDurationSeconds,
|
alignedDurationSeconds,
|
||||||
@@ -350,7 +380,7 @@ public sealed class ScheduleGenerator(
|
|||||||
fromName,
|
fromName,
|
||||||
channel.BumperNextLabel,
|
channel.BumperNextLabel,
|
||||||
toName,
|
toName,
|
||||||
bumperStorage.BackgroundPath(template.Id, template.BackgroundImageExtension),
|
backgroundAbsolutePath,
|
||||||
bumperStorage.AudioPath(template.Id, template.AudioExtension),
|
bumperStorage.AudioPath(template.Id, template.AudioExtension),
|
||||||
posterAbsolutePath
|
posterAbsolutePath
|
||||||
);
|
);
|
||||||
@@ -397,7 +427,7 @@ public sealed class ScheduleGenerator(
|
|||||||
template.AccentColor,
|
template.AccentColor,
|
||||||
template.TextColor,
|
template.TextColor,
|
||||||
template.Revision,
|
template.Revision,
|
||||||
template.BackgroundImageExtension ?? "-",
|
template.BackgroundImageId?.ToString() ?? "-",
|
||||||
template.AudioExtension ?? "-",
|
template.AudioExtension ?? "-",
|
||||||
fromName,
|
fromName,
|
||||||
toName,
|
toName,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace TeleWave.Application.Common.Interfaces;
|
namespace TeleWave.Application.Common.Interfaces;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Хранилище сырых файлов блоков заставок (звук и фон-картинка) под bumpers/{templateId}. В отличие
|
/// Хранилище звука блоков заставок под bumpers/{templateId}. Фон-картинка блока живёт в общем реестре
|
||||||
/// от обычных ассетов эти файлы НЕ режутся на HLS — они подаются как входы в рендер заставки.
|
/// изображений (см. IImageStore). Звук НЕ режется на HLS — подаётся входом в рендер заставки.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IBumperTemplateStorage
|
public interface IBumperTemplateStorage
|
||||||
{
|
{
|
||||||
@@ -13,22 +13,11 @@ public interface IBumperTemplateStorage
|
|||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
);
|
);
|
||||||
|
|
||||||
Task SaveBackgroundAsync(
|
|
||||||
Guid templateId,
|
|
||||||
string extension,
|
|
||||||
Stream content,
|
|
||||||
CancellationToken cancellationToken
|
|
||||||
);
|
|
||||||
|
|
||||||
void DeleteAudio(Guid templateId);
|
void DeleteAudio(Guid templateId);
|
||||||
void DeleteBackground(Guid templateId);
|
|
||||||
|
|
||||||
/// <summary>Удалить все файлы блока (при удалении самого блока).</summary>
|
/// <summary>Удалить все файлы блока (при удалении самого блока).</summary>
|
||||||
void DeleteTemplate(Guid templateId);
|
void DeleteTemplate(Guid templateId);
|
||||||
|
|
||||||
/// <summary>Абсолютный путь к загруженному звуку или null (нет расширения / файл отсутствует).</summary>
|
/// <summary>Абсолютный путь к загруженному звуку или null (нет расширения / файл отсутствует).</summary>
|
||||||
string? AudioPath(Guid templateId, string? extension);
|
string? AudioPath(Guid templateId, string? extension);
|
||||||
|
|
||||||
/// <summary>Абсолютный путь к загруженной фон-картинке или null.</summary>
|
|
||||||
string? BackgroundPath(Guid templateId, string? extension);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
namespace TeleWave.Application.Common.Interfaces;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Локальное хранилище картинок метаданных (постеры/кадры) под metadata/ в корне хранилища. Скачивает
|
|
||||||
/// изображения к себе, чтобы не зависеть от внешнего CDN на этапе показа.
|
|
||||||
/// </summary>
|
|
||||||
public interface IMetadataImageStore
|
|
||||||
{
|
|
||||||
/// <summary>Скачивает кадр серии по URL. Возвращает относительный путь или null при ошибке.</summary>
|
|
||||||
Task<string?> DownloadEpisodeStillAsync(
|
|
||||||
Guid episodeId,
|
|
||||||
string url,
|
|
||||||
CancellationToken cancellationToken
|
|
||||||
);
|
|
||||||
|
|
||||||
void DeleteEpisodeImages(Guid episodeId);
|
|
||||||
|
|
||||||
/// <summary>Абсолютный путь к файлу по относительному (с защитой от traversal) или null, если вне корня.</summary>
|
|
||||||
string? ResolveAbsolutePath(string relativePath);
|
|
||||||
}
|
|
||||||
@@ -44,7 +44,7 @@ public sealed class GetShowQueryHandler(IAppDbContext dbContext)
|
|||||||
e.Episode,
|
e.Episode,
|
||||||
e.Title,
|
e.Title,
|
||||||
e.Overview,
|
e.Overview,
|
||||||
e.StillPath is not null,
|
e.StillImageId,
|
||||||
e.AirDate
|
e.AirDate
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed record EpisodeDto(
|
|||||||
int? Episode,
|
int? Episode,
|
||||||
string? Title,
|
string? Title,
|
||||||
string? Overview,
|
string? Overview,
|
||||||
bool HasStill,
|
Guid? StillImageId,
|
||||||
DateOnly? AirDate
|
DateOnly? AirDate
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+22
-6
@@ -3,13 +3,15 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using TeleWave.Application.Common.Interfaces;
|
using TeleWave.Application.Common.Interfaces;
|
||||||
using TeleWave.Application.Common.Models;
|
using TeleWave.Application.Common.Models;
|
||||||
using TeleWave.Application.Library;
|
using TeleWave.Application.Library;
|
||||||
|
using TeleWave.Domain.Images;
|
||||||
|
|
||||||
namespace TeleWave.Application.Metadata.RefreshEpisodes;
|
namespace TeleWave.Application.Metadata.RefreshEpisodes;
|
||||||
|
|
||||||
public sealed class RefreshShowEpisodesMetadataCommandHandler(
|
public sealed class RefreshShowEpisodesMetadataCommandHandler(
|
||||||
IAppDbContext dbContext,
|
IAppDbContext dbContext,
|
||||||
IMetadataProviderResolver resolver,
|
IMetadataProviderResolver resolver,
|
||||||
IMetadataImageStore imageStore
|
IImageDownloader downloader,
|
||||||
|
IImageStore imageStore
|
||||||
) : ICommandHandler<RefreshShowEpisodesMetadataCommand, Result<int>>
|
) : ICommandHandler<RefreshShowEpisodesMetadataCommand, Result<int>>
|
||||||
{
|
{
|
||||||
public async Task<Result<int>> Handle(
|
public async Task<Result<int>> Handle(
|
||||||
@@ -66,15 +68,29 @@ public sealed class RefreshShowEpisodesMetadataCommandHandler(
|
|||||||
if (meta is null)
|
if (meta is null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
string? stillPath = null;
|
Guid? stillImageId = null;
|
||||||
if (!string.IsNullOrEmpty(meta.StillUrl))
|
if (!string.IsNullOrEmpty(meta.StillUrl))
|
||||||
stillPath = await imageStore.DownloadEpisodeStillAsync(
|
{
|
||||||
episode.Id,
|
var downloaded = await downloader.DownloadAsync(meta.StillUrl, cancellationToken);
|
||||||
meta.StillUrl,
|
if (downloaded is not null)
|
||||||
|
{
|
||||||
|
var image = Image.Create(
|
||||||
|
ImageCategory.EpisodeStill,
|
||||||
|
downloaded.Extension,
|
||||||
|
meta.Title
|
||||||
|
);
|
||||||
|
dbContext.Images.Add(image);
|
||||||
|
await imageStore.SaveAsync(
|
||||||
|
image.Id,
|
||||||
|
downloaded.Extension,
|
||||||
|
downloaded.Content,
|
||||||
cancellationToken
|
cancellationToken
|
||||||
);
|
);
|
||||||
|
stillImageId = image.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
episode.ApplyMetadata(meta.Title, meta.Overview, stillPath, meta.AirDate);
|
episode.ApplyMetadata(meta.Title, meta.Overview, stillImageId, meta.AirDate);
|
||||||
updated++;
|
updated++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext)
|
|||||||
e.Id,
|
e.Id,
|
||||||
e.Title,
|
e.Title,
|
||||||
e.Overview,
|
e.Overview,
|
||||||
e.StillPath,
|
e.StillImageId,
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
var episodeByKey = episodes
|
var episodeByKey = episodes
|
||||||
@@ -82,7 +82,7 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext)
|
|||||||
episode?.Id,
|
episode?.Id,
|
||||||
episode?.Title,
|
episode?.Title,
|
||||||
episode?.Overview,
|
episode?.Overview,
|
||||||
episode?.StillPath is not null
|
episode?.StillImageId
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public sealed record PublicEpgEntryDto(
|
|||||||
Guid? EpisodeId,
|
Guid? EpisodeId,
|
||||||
string? EpisodeTitle,
|
string? EpisodeTitle,
|
||||||
string? EpisodeOverview,
|
string? EpisodeOverview,
|
||||||
bool EpisodeHasStill
|
Guid? EpisodeStillImageId
|
||||||
);
|
);
|
||||||
|
|
||||||
public sealed record LiveSegmentDto(Guid AssetId, int LocalIndex, bool Discontinuity);
|
public sealed record LiveSegmentDto(Guid AssetId, int LocalIndex, bool Discontinuity);
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ public class BumperTemplate
|
|||||||
public string AccentColor { get; private set; } = DefaultAccentColor;
|
public string AccentColor { get; private set; } = DefaultAccentColor;
|
||||||
public string TextColor { get; private set; } = DefaultTextColor;
|
public string TextColor { get; private set; } = DefaultTextColor;
|
||||||
|
|
||||||
/// <summary>Расширение загруженной фон-картинки (с точкой) или null — тогда фон градиент/постер.</summary>
|
/// <summary>Фон-картинка блока — ссылка на запись реестра изображений или null (тогда фон градиент/постер).</summary>
|
||||||
public string? BackgroundImageExtension { get; private set; }
|
public Guid? BackgroundImageId { get; private set; }
|
||||||
|
|
||||||
/// <summary>Расширение загруженного звука (с точкой) или null — тогда синтезируется джингл.</summary>
|
/// <summary>Расширение загруженного звука (с точкой) или null — тогда синтезируется джингл.</summary>
|
||||||
public string? AudioExtension { get; private set; }
|
public string? AudioExtension { get; private set; }
|
||||||
@@ -59,7 +59,7 @@ public class BumperTemplate
|
|||||||
BackgroundColor2 = DefaultBackgroundColor2,
|
BackgroundColor2 = DefaultBackgroundColor2,
|
||||||
AccentColor = DefaultAccentColor,
|
AccentColor = DefaultAccentColor,
|
||||||
TextColor = DefaultTextColor,
|
TextColor = DefaultTextColor,
|
||||||
BackgroundImageExtension = null,
|
BackgroundImageId = null,
|
||||||
AudioExtension = null,
|
AudioExtension = null,
|
||||||
AudioDurationSeconds = null,
|
AudioDurationSeconds = null,
|
||||||
Revision = 0,
|
Revision = 0,
|
||||||
@@ -99,18 +99,18 @@ public class BumperTemplate
|
|||||||
Revision++;
|
Revision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Отметить загруженную фон-картинку (extension — с точкой, нижний регистр). Меняет ревизию.</summary>
|
/// <summary>Привязать фон-картинку блока (ссылка на реестр изображений). Меняет ревизию.</summary>
|
||||||
public void SetBackgroundImage(string extension)
|
public void SetBackgroundImage(Guid imageId)
|
||||||
{
|
{
|
||||||
BackgroundImageExtension = extension;
|
BackgroundImageId = imageId;
|
||||||
Revision++;
|
Revision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearBackgroundImage()
|
public void ClearBackgroundImage()
|
||||||
{
|
{
|
||||||
if (BackgroundImageExtension is null)
|
if (BackgroundImageId is null)
|
||||||
return;
|
return;
|
||||||
BackgroundImageExtension = null;
|
BackgroundImageId = null;
|
||||||
Revision++;
|
Revision++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ public class ShowEpisode
|
|||||||
public string? Title { get; private set; }
|
public string? Title { get; private set; }
|
||||||
public string? Overview { get; private set; }
|
public string? Overview { get; private set; }
|
||||||
|
|
||||||
/// <summary>Относительный путь локального кадра или null.</summary>
|
/// <summary>Кадр серии — ссылка на запись общего реестра изображений или null.</summary>
|
||||||
public string? StillPath { get; private set; }
|
public Guid? StillImageId { get; private set; }
|
||||||
public DateOnly? AirDate { get; private set; }
|
public DateOnly? AirDate { get; private set; }
|
||||||
|
|
||||||
public DateTimeOffset CreatedAt { get; private set; }
|
public DateTimeOffset CreatedAt { get; private set; }
|
||||||
@@ -43,13 +43,13 @@ public class ShowEpisode
|
|||||||
Episode = episode;
|
Episode = episode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Применить метаданные серии (кадр — уже скачанный локально — может быть null).</summary>
|
/// <summary>Применить метаданные серии (кадр — уже зарегистрирован в реестре — может быть null).</summary>
|
||||||
public void ApplyMetadata(string? title, string? overview, string? stillPath, DateOnly? airDate)
|
public void ApplyMetadata(string? title, string? overview, Guid? stillImageId, DateOnly? airDate)
|
||||||
{
|
{
|
||||||
Title = title;
|
Title = title;
|
||||||
Overview = overview;
|
Overview = overview;
|
||||||
if (stillPath is not null)
|
if (stillImageId is not null)
|
||||||
StillPath = stillPath;
|
StillImageId = stillImageId;
|
||||||
AirDate = airDate;
|
AirDate = airDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ public class ShowEpisode
|
|||||||
{
|
{
|
||||||
Title = null;
|
Title = null;
|
||||||
Overview = null;
|
Overview = null;
|
||||||
StillPath = null;
|
StillImageId = null;
|
||||||
AirDate = null;
|
AirDate = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ public static class DependencyInjection
|
|||||||
services.AddSingleton<IMetadataProvider, TmdbMetadataProvider>();
|
services.AddSingleton<IMetadataProvider, TmdbMetadataProvider>();
|
||||||
services.AddSingleton<IMetadataProvider, OmdbMetadataProvider>();
|
services.AddSingleton<IMetadataProvider, OmdbMetadataProvider>();
|
||||||
services.AddSingleton<IMetadataProviderResolver, MetadataProviderResolver>();
|
services.AddSingleton<IMetadataProviderResolver, MetadataProviderResolver>();
|
||||||
services.AddSingleton<IMetadataImageStore, MetadataImageStore>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Планировщик расписания: генератор, источник случайности и фоновый сервис горизонта.</summary>
|
/// <summary>Планировщик расписания: генератор, источник случайности и фоновый сервис горизонта.</summary>
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ using TeleWave.Application.Common.Interfaces;
|
|||||||
namespace TeleWave.Infrastructure.Media;
|
namespace TeleWave.Infrastructure.Media;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Файловое хранилище блоков заставок: сырые звук/фон под bumpers/{templateId}/{kind}{ext}.
|
/// Файловое хранилище звука блоков заставок: bumpers/{templateId}/audio{ext}. На блок — не более
|
||||||
/// На блок — не более одного файла каждого вида (при загрузке старый удаляется).
|
/// одного файла (при загрузке старый удаляется). Фон-картинка блока хранится в общем реестре.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemplateStorage
|
public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemplateStorage
|
||||||
{
|
{
|
||||||
private const string Audio = "audio";
|
private const string Audio = "audio";
|
||||||
private const string Background = "background";
|
|
||||||
|
|
||||||
public Task SaveAudioAsync(
|
public Task SaveAudioAsync(
|
||||||
Guid templateId,
|
Guid templateId,
|
||||||
@@ -18,17 +17,8 @@ public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemp
|
|||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
) => SaveAsync(templateId, Audio, extension, content, cancellationToken);
|
) => SaveAsync(templateId, Audio, extension, content, cancellationToken);
|
||||||
|
|
||||||
public Task SaveBackgroundAsync(
|
|
||||||
Guid templateId,
|
|
||||||
string extension,
|
|
||||||
Stream content,
|
|
||||||
CancellationToken cancellationToken
|
|
||||||
) => SaveAsync(templateId, Background, extension, content, cancellationToken);
|
|
||||||
|
|
||||||
public void DeleteAudio(Guid templateId) => DeleteKind(templateId, Audio);
|
public void DeleteAudio(Guid templateId) => DeleteKind(templateId, Audio);
|
||||||
|
|
||||||
public void DeleteBackground(Guid templateId) => DeleteKind(templateId, Background);
|
|
||||||
|
|
||||||
public void DeleteTemplate(Guid templateId)
|
public void DeleteTemplate(Guid templateId)
|
||||||
{
|
{
|
||||||
var dir = paths.BumperTemplateDir(templateId);
|
var dir = paths.BumperTemplateDir(templateId);
|
||||||
@@ -39,9 +29,6 @@ public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemp
|
|||||||
public string? AudioPath(Guid templateId, string? extension) =>
|
public string? AudioPath(Guid templateId, string? extension) =>
|
||||||
ResolvePath(templateId, Audio, extension);
|
ResolvePath(templateId, Audio, extension);
|
||||||
|
|
||||||
public string? BackgroundPath(Guid templateId, string? extension) =>
|
|
||||||
ResolvePath(templateId, Background, extension);
|
|
||||||
|
|
||||||
private async Task SaveAsync(
|
private async Task SaveAsync(
|
||||||
Guid templateId,
|
Guid templateId,
|
||||||
string kind,
|
string kind,
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
using TeleWave.Application.Common.Interfaces;
|
|
||||||
using TeleWave.Infrastructure.Media;
|
|
||||||
|
|
||||||
namespace TeleWave.Infrastructure.Metadata;
|
|
||||||
|
|
||||||
/// <summary>Скачивает и хранит картинки метаданных локально под metadata/shows/{id}.</summary>
|
|
||||||
public sealed class MetadataImageStore(MediaPathResolver paths, IHttpClientFactory httpFactory)
|
|
||||||
: IMetadataImageStore
|
|
||||||
{
|
|
||||||
public async Task<string?> DownloadEpisodeStillAsync(
|
|
||||||
Guid episodeId,
|
|
||||||
string url,
|
|
||||||
CancellationToken cancellationToken
|
|
||||||
)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var client = httpFactory.CreateClient("metadata");
|
|
||||||
using var response = await client.GetAsync(url, cancellationToken);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var ext = ExtensionFor(url, response.Content.Headers.ContentType?.MediaType);
|
|
||||||
var dir = paths.MetadataEpisodeDir(episodeId);
|
|
||||||
Directory.CreateDirectory(dir);
|
|
||||||
RemoveExisting(dir, "still");
|
|
||||||
|
|
||||||
var abs = paths.MetadataEpisodeStillPath(episodeId, ext);
|
|
||||||
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
||||||
await using (var fs = File.Create(abs))
|
|
||||||
await content.CopyToAsync(fs, cancellationToken);
|
|
||||||
return paths.MetadataEpisodeStillRelative(episodeId, ext);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or IOException)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteEpisodeImages(Guid episodeId)
|
|
||||||
{
|
|
||||||
var dir = paths.MetadataEpisodeDir(episodeId);
|
|
||||||
if (Directory.Exists(dir))
|
|
||||||
Directory.Delete(dir, recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? ResolveAbsolutePath(string relativePath)
|
|
||||||
{
|
|
||||||
var abs = paths.ResolveRelative(relativePath);
|
|
||||||
return abs is not null && File.Exists(abs) ? abs : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RemoveExisting(string dir, string baseName)
|
|
||||||
{
|
|
||||||
foreach (var file in Directory.EnumerateFiles(dir, baseName + ".*"))
|
|
||||||
File.Delete(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ExtensionFor(string url, string? mediaType) =>
|
|
||||||
mediaType switch
|
|
||||||
{
|
|
||||||
"image/png" => ".png",
|
|
||||||
"image/webp" => ".webp",
|
|
||||||
"image/jpeg" => ".jpg",
|
|
||||||
_ => Path.GetExtension(new Uri(url).AbsolutePath) is { Length: > 1 } e ? e.ToLowerInvariant() : ".jpg",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+887
@@ -0,0 +1,887 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using TeleWave.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TeleWave.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260725085149_EpisodeStillAndBumperBgImages")]
|
||||||
|
partial class EpisodeStillAndBumperBgImages
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.10")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("RoleId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Auth.RefreshToken", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("RefreshTokens");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperAsset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("FromShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("MediaAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Signature")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ToShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FromShowId", "ToShowId", "Signature");
|
||||||
|
|
||||||
|
b.ToTable("BumperAssets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTemplate", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AccentColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<double?>("AudioDurationSeconds")
|
||||||
|
.HasColumnType("double precision");
|
||||||
|
|
||||||
|
b.Property<string>("AudioExtension")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("BackgroundColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("BackgroundColor2")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("BackgroundImageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Revision")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TextColor")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "Position");
|
||||||
|
|
||||||
|
b.ToTable("BumperTemplate");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("AdInsertion")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("AdsPerBreak")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BumperFont")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BumperMinIntervalMinutes")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("BumperNextLabel")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("BumperNowLabel")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("BumperOnlyBetweenDifferentShows")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("BumperSelection")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("BumpersEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("EpochUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("FillerAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<int>("NextAdIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("NextBumperIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Channels");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelAd", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("MediaAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "Position");
|
||||||
|
|
||||||
|
b.ToTable("ChannelAd");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelShow", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("BlockMode")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BlockValue")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("NextEpisodeIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("ShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Weight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "ShowId");
|
||||||
|
|
||||||
|
b.ToTable("ChannelShow");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.OverrideShow", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProgrammingOverrideId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Weight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProgrammingOverrideId");
|
||||||
|
|
||||||
|
b.ToTable("OverrideShow");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ProgrammingOverride", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("EndsAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Mode")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartsAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "StartsAtUtc", "EndsAtUtc");
|
||||||
|
|
||||||
|
b.ToTable("ProgrammingOverride");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ScheduleEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ChannelId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("EndsAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int?>("EpisodeIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("MediaAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartsAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "EndsAtUtc");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "ShowId");
|
||||||
|
|
||||||
|
b.HasIndex("ChannelId", "StartsAtUtc");
|
||||||
|
|
||||||
|
b.ToTable("ScheduleEntries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Images.Image", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Category")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("FileExtension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFileName")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Category", "CreatedAt");
|
||||||
|
|
||||||
|
b.ToTable("Images");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Library.Show", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<int>("Kind")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("MetadataExternalId")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("MetadataProvider")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("PosterImageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int?>("Year")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Library.ShowEpisode", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateOnly?>("AirDate")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int?>("Episode")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("MediaAssetId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Overview")
|
||||||
|
.HasMaxLength(4096)
|
||||||
|
.HasColumnType("character varying(4096)");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("Season")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("ShowId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("StillImageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MediaAssetId");
|
||||||
|
|
||||||
|
b.HasIndex("ShowId", "Position");
|
||||||
|
|
||||||
|
b.ToTable("ShowEpisode");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Media.MediaAsset", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AudioCodec")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<TimeSpan?>("Duration")
|
||||||
|
.HasColumnType("interval");
|
||||||
|
|
||||||
|
b.Property<string>("ErrorMessage")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<int?>("Height")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalExtension")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalFileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("RelativePath")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<int?>("SegmentCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("SegmentSeconds")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Source")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("VideoCodec")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<int?>("Width")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("MediaAssets");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Settings.AppSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.HasKey("Key");
|
||||||
|
|
||||||
|
b.ToTable("AppSettings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Infrastructure.Identity.AppRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSystem")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Infrastructure.Identity.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsBlocked")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Infrastructure.Identity.AppUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTemplate", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
|
.WithMany("BumperTemplates")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelAd", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
|
.WithMany("Ads")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelShow", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
|
.WithMany("Shows")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.OverrideShow", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.ProgrammingOverride", null)
|
||||||
|
.WithMany("Shows")
|
||||||
|
.HasForeignKey("ProgrammingOverrideId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ProgrammingOverride", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||||
|
.WithMany("Overrides")
|
||||||
|
.HasForeignKey("ChannelId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Library.ShowEpisode", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("TeleWave.Domain.Library.Show", null)
|
||||||
|
.WithMany("Episodes")
|
||||||
|
.HasForeignKey("ShowId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Ads");
|
||||||
|
|
||||||
|
b.Navigation("BumperTemplates");
|
||||||
|
|
||||||
|
b.Navigation("Overrides");
|
||||||
|
|
||||||
|
b.Navigation("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Broadcast.ProgrammingOverride", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Shows");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TeleWave.Domain.Library.Show", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Episodes");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TeleWave.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class EpisodeStillAndBumperBgImages : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "StillImageId",
|
||||||
|
table: "ShowEpisode",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "BackgroundImageId",
|
||||||
|
table: "BumperTemplate",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
// Кадры серий → реестр (Category=2 EpisodeStill); файлы перекладывает RelocateLegacyImagesAsync.
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
DECLARE r RECORD; img uuid;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT "Id", "StillPath" FROM "ShowEpisode" WHERE "StillPath" IS NOT NULL LOOP
|
||||||
|
img := gen_random_uuid();
|
||||||
|
INSERT INTO "Images" ("Id", "Category", "FileExtension", "OriginalFileName", "CreatedAt")
|
||||||
|
VALUES (img, 2, lower(coalesce(substring(r."StillPath" from '\.[^.]*$'), '.jpg')), 'still', now());
|
||||||
|
UPDATE "ShowEpisode" SET "StillImageId" = img WHERE "Id" = r."Id";
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
// Фоны блоков заставок → реестр (Category=3 BumperBackground).
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
DECLARE r RECORD; img uuid;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT "Id", "BackgroundImageExtension" FROM "BumperTemplate" WHERE "BackgroundImageExtension" IS NOT NULL LOOP
|
||||||
|
img := gen_random_uuid();
|
||||||
|
INSERT INTO "Images" ("Id", "Category", "FileExtension", "OriginalFileName", "CreatedAt")
|
||||||
|
VALUES (img, 3, lower(r."BackgroundImageExtension"), 'background', now());
|
||||||
|
UPDATE "BumperTemplate" SET "BackgroundImageId" = img WHERE "Id" = r."Id";
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "StillPath",
|
||||||
|
table: "ShowEpisode");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BackgroundImageExtension",
|
||||||
|
table: "BumperTemplate");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "StillImageId",
|
||||||
|
table: "ShowEpisode");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BackgroundImageId",
|
||||||
|
table: "BumperTemplate");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "StillPath",
|
||||||
|
table: "ShowEpisode",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BackgroundImageExtension",
|
||||||
|
table: "BumperTemplate",
|
||||||
|
type: "character varying(16)",
|
||||||
|
maxLength: 16,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -215,9 +215,8 @@ namespace TeleWave.Infrastructure.Migrations
|
|||||||
.HasMaxLength(32)
|
.HasMaxLength(32)
|
||||||
.HasColumnType("character varying(32)");
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
b.Property<string>("BackgroundImageExtension")
|
b.Property<Guid?>("BackgroundImageId")
|
||||||
.HasMaxLength(16)
|
.HasColumnType("uuid");
|
||||||
.HasColumnType("character varying(16)");
|
|
||||||
|
|
||||||
b.Property<Guid>("ChannelId")
|
b.Property<Guid>("ChannelId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
@@ -554,9 +553,8 @@ namespace TeleWave.Infrastructure.Migrations
|
|||||||
b.Property<Guid>("ShowId")
|
b.Property<Guid>("ShowId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<string>("StillPath")
|
b.Property<Guid?>("StillImageId")
|
||||||
.HasMaxLength(256)
|
.HasColumnType("uuid");
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasMaxLength(512)
|
.HasMaxLength(512)
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ public class BumperTemplateConfiguration : IEntityTypeConfiguration<BumperTempla
|
|||||||
builder.Property(x => x.BackgroundColor2).IsRequired().HasMaxLength(32);
|
builder.Property(x => x.BackgroundColor2).IsRequired().HasMaxLength(32);
|
||||||
builder.Property(x => x.AccentColor).IsRequired().HasMaxLength(32);
|
builder.Property(x => x.AccentColor).IsRequired().HasMaxLength(32);
|
||||||
builder.Property(x => x.TextColor).IsRequired().HasMaxLength(32);
|
builder.Property(x => x.TextColor).IsRequired().HasMaxLength(32);
|
||||||
builder.Property(x => x.BackgroundImageExtension).HasMaxLength(16);
|
|
||||||
builder.Property(x => x.AudioExtension).HasMaxLength(16);
|
builder.Property(x => x.AudioExtension).HasMaxLength(16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,5 @@ public class ShowEpisodeConfiguration : IEntityTypeConfiguration<ShowEpisode>
|
|||||||
builder.HasIndex(x => x.MediaAssetId);
|
builder.HasIndex(x => x.MediaAssetId);
|
||||||
builder.Property(x => x.Title).HasMaxLength(512);
|
builder.Property(x => x.Title).HasMaxLength(512);
|
||||||
builder.Property(x => x.Overview).HasMaxLength(4096);
|
builder.Property(x => x.Overview).HasMaxLength(4096);
|
||||||
builder.Property(x => x.StillPath).HasMaxLength(256);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,31 +31,56 @@ public static class MigrationExtensions
|
|||||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
var paths = scope.ServiceProvider.GetRequiredService<MediaPathResolver>();
|
var paths = scope.ServiceProvider.GetRequiredService<MediaPathResolver>();
|
||||||
|
|
||||||
|
Directory.CreateDirectory(paths.ImagesDir);
|
||||||
|
|
||||||
|
// Постеры шоу: metadata/shows/{showId}/poster{ext} → images/{imageId}{ext}.
|
||||||
var posters = await dbContext.Shows.AsNoTracking()
|
var posters = await dbContext.Shows.AsNoTracking()
|
||||||
.Where(s => s.PosterImageId != null)
|
.Where(s => s.PosterImageId != null)
|
||||||
.Join(
|
.Join(
|
||||||
dbContext.Images,
|
dbContext.Images,
|
||||||
s => s.PosterImageId,
|
s => s.PosterImageId,
|
||||||
i => i.Id,
|
i => i.Id,
|
||||||
(s, i) => new
|
(s, i) => new { EntityId = s.Id, ImageId = i.Id, i.FileExtension }
|
||||||
{
|
|
||||||
ShowId = s.Id,
|
|
||||||
ImageId = i.Id,
|
|
||||||
i.FileExtension,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
foreach (var p in posters)
|
foreach (var p in posters)
|
||||||
{
|
Relocate(paths.ImagePath(p.ImageId, p.FileExtension), paths.MetadataShowPosterPath(p.EntityId, p.FileExtension));
|
||||||
var target = paths.ImagePath(p.ImageId, p.FileExtension);
|
|
||||||
if (File.Exists(target))
|
|
||||||
continue;
|
|
||||||
var legacy = paths.MetadataShowPosterPath(p.ShowId, p.FileExtension);
|
|
||||||
if (!File.Exists(legacy))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Directory.CreateDirectory(paths.ImagesDir);
|
// Кадры серий: metadata/episodes/{episodeId}/still{ext} → images/{imageId}{ext}.
|
||||||
|
var stills = await dbContext.Shows.AsNoTracking()
|
||||||
|
.SelectMany(s => s.Episodes)
|
||||||
|
.Where(e => e.StillImageId != null)
|
||||||
|
.Join(
|
||||||
|
dbContext.Images,
|
||||||
|
e => e.StillImageId,
|
||||||
|
i => i.Id,
|
||||||
|
(e, i) => new { EntityId = e.Id, ImageId = i.Id, i.FileExtension }
|
||||||
|
)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
foreach (var s in stills)
|
||||||
|
Relocate(paths.ImagePath(s.ImageId, s.FileExtension), paths.MetadataEpisodeStillPath(s.EntityId, s.FileExtension));
|
||||||
|
|
||||||
|
// Фоны блоков заставок: bumpers/{templateId}/background{ext} → images/{imageId}{ext}.
|
||||||
|
var backgrounds = await dbContext.Channels.AsNoTracking()
|
||||||
|
.SelectMany(c => c.BumperTemplates)
|
||||||
|
.Where(t => t.BackgroundImageId != null)
|
||||||
|
.Join(
|
||||||
|
dbContext.Images,
|
||||||
|
t => t.BackgroundImageId,
|
||||||
|
i => i.Id,
|
||||||
|
(t, i) => new { EntityId = t.Id, ImageId = i.Id, i.FileExtension }
|
||||||
|
)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
foreach (var b in backgrounds)
|
||||||
|
Relocate(
|
||||||
|
paths.ImagePath(b.ImageId, b.FileExtension),
|
||||||
|
paths.BumperTemplateFilePath(b.EntityId, "background", b.FileExtension)
|
||||||
|
);
|
||||||
|
|
||||||
|
static void Relocate(string target, string legacy)
|
||||||
|
{
|
||||||
|
if (File.Exists(target) || !File.Exists(legacy))
|
||||||
|
return;
|
||||||
File.Move(legacy, target);
|
File.Move(legacy, target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import Hls from 'hls.js'
|
|||||||
import { type ReactNode, useEffect, useRef, useState } from 'react'
|
import { type ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ChevronDown, ChevronLeft, RefreshCw } from 'lucide-react'
|
import { ChevronDown, ChevronLeft, RefreshCw } from 'lucide-react'
|
||||||
|
import { imageUrl } from '@/features/admin/images/api'
|
||||||
|
import { ImageGallery } from '@/features/admin/images/ImageGallery'
|
||||||
import { getAccessToken, HttpError } from '@/shared/api/client'
|
import { getAccessToken, HttpError } from '@/shared/api/client'
|
||||||
import type {
|
import type {
|
||||||
AdInsertion,
|
AdInsertion,
|
||||||
@@ -41,11 +43,11 @@ import {
|
|||||||
removeChannelAd,
|
removeChannelAd,
|
||||||
removeChannelShow,
|
removeChannelShow,
|
||||||
renderBumperPreview,
|
renderBumperPreview,
|
||||||
|
setBumperTemplateBackground,
|
||||||
updateBumperTemplate,
|
updateBumperTemplate,
|
||||||
updateChannelSettings,
|
updateChannelSettings,
|
||||||
updateChannelShow,
|
updateChannelShow,
|
||||||
uploadBumperTemplateAudio,
|
uploadBumperTemplateAudio,
|
||||||
uploadBumperTemplateBackground,
|
|
||||||
} from './api'
|
} from './api'
|
||||||
|
|
||||||
function formatTime(iso: string) {
|
function formatTime(iso: string) {
|
||||||
@@ -654,17 +656,11 @@ function BumperTemplateEditor({
|
|||||||
onSaved={onChanged}
|
onSaved={onChanged}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
<BumperFileUpload
|
<BumperBackgroundField
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
templateId={template.id}
|
templateId={template.id}
|
||||||
kind="background"
|
backgroundImageId={template.backgroundImageId}
|
||||||
label={t('admin.channels.bumperBackground')}
|
onChanged={onChanged}
|
||||||
hint={t('admin.channels.bumperBackgroundHint')}
|
|
||||||
has={template.hasBackground}
|
|
||||||
accept="image/*"
|
|
||||||
upload={uploadBumperTemplateBackground}
|
|
||||||
clear={clearBumperTemplateBackground}
|
|
||||||
onSaved={onChanged}
|
|
||||||
onError={onError}
|
onError={onError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -758,6 +754,71 @@ function BumperPreviewPlayer({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BumperBackgroundField({
|
||||||
|
channelId,
|
||||||
|
templateId,
|
||||||
|
backgroundImageId,
|
||||||
|
onChanged,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
channelId: string
|
||||||
|
templateId: string
|
||||||
|
backgroundImageId: string | null
|
||||||
|
onChanged: () => void
|
||||||
|
onError: (e: unknown) => void
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [galleryOpen, setGalleryOpen] = useState(false)
|
||||||
|
|
||||||
|
const setBg = useMutation({
|
||||||
|
mutationFn: (imageId: string) => setBumperTemplateBackground(channelId, templateId, imageId),
|
||||||
|
onSuccess: onChanged,
|
||||||
|
onError,
|
||||||
|
})
|
||||||
|
const clearBg = useMutation({
|
||||||
|
mutationFn: () => clearBumperTemplateBackground(channelId, templateId),
|
||||||
|
onSuccess: onChanged,
|
||||||
|
onError,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label>
|
||||||
|
{t('admin.channels.bumperBackground')}{' '}
|
||||||
|
<span className={backgroundImageId ? 'text-emerald-500' : 'text-muted-foreground'}>
|
||||||
|
{backgroundImageId
|
||||||
|
? `· ${t('admin.channels.bumperFileLoaded')}`
|
||||||
|
: `· ${t('admin.channels.bumperFileDefault')}`}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<span className="text-xs text-muted-foreground">{t('admin.channels.bumperBackgroundHint')}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{backgroundImageId && (
|
||||||
|
<img
|
||||||
|
src={imageUrl(backgroundImageId)}
|
||||||
|
alt=""
|
||||||
|
className="h-10 w-16 shrink-0 rounded border border-border object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button size="sm" variant="outline" onClick={() => setGalleryOpen(true)}>
|
||||||
|
{t('admin.channels.bumperBackgroundPick')}
|
||||||
|
</Button>
|
||||||
|
{backgroundImageId && (
|
||||||
|
<Button size="sm" variant="ghost" disabled={clearBg.isPending} onClick={() => clearBg.mutate()}>
|
||||||
|
{t('admin.channels.bumperReset')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ImageGallery
|
||||||
|
open={galleryOpen}
|
||||||
|
onOpenChange={setGalleryOpen}
|
||||||
|
category="BumperBackground"
|
||||||
|
onSelect={(img) => setBg.mutate(img.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function BumperFileUpload({
|
function BumperFileUpload({
|
||||||
channelId,
|
channelId,
|
||||||
templateId,
|
templateId,
|
||||||
|
|||||||
@@ -137,8 +137,12 @@ export function uploadBumperTemplateAudio(id: string, templateId: string, file:
|
|||||||
return uploadBumperTemplateFile(id, templateId, 'audio', file)
|
return uploadBumperTemplateFile(id, templateId, 'audio', file)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadBumperTemplateBackground(id: string, templateId: string, file: File) {
|
/** Привязать фон-картинку блока по ссылке на изображение из реестра (галерея). */
|
||||||
return uploadBumperTemplateFile(id, templateId, 'background', file)
|
export function setBumperTemplateBackground(id: string, templateId: string, imageId: string) {
|
||||||
|
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/background`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: { imageId },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearBumperTemplateAudio(id: string, templateId: string) {
|
export function clearBumperTemplateAudio(id: string, templateId: string) {
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
} from '@/features/admin/media/episode-parse'
|
} from '@/features/admin/media/episode-parse'
|
||||||
import { formatDuration } from '@/features/admin/media/MediaPanel'
|
import { formatDuration } from '@/features/admin/media/MediaPanel'
|
||||||
import { ShowMetadataCard } from './ShowMetadataCard'
|
import { ShowMetadataCard } from './ShowMetadataCard'
|
||||||
import { addEpisode, episodeStillUrl, getShow, removeEpisode } from './api'
|
import { imageUrl } from '@/features/admin/images/api'
|
||||||
|
import { addEpisode, getShow, removeEpisode } from './api'
|
||||||
|
|
||||||
type Candidate = { asset: MediaAssetDto; parsed: ParsedEpisode }
|
type Candidate = { asset: MediaAssetDto; parsed: ParsedEpisode }
|
||||||
|
|
||||||
@@ -219,9 +220,9 @@ export function ShowDetail({ showId }: { showId: string }) {
|
|||||||
<td className="px-4 py-2 text-muted-foreground">{index + 1}</td>
|
<td className="px-4 py-2 text-muted-foreground">{index + 1}</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{episode.hasStill && (
|
{episode.stillImageId && (
|
||||||
<img
|
<img
|
||||||
src={episodeStillUrl(episode.id)}
|
src={imageUrl(episode.stillImageId)}
|
||||||
alt=""
|
alt=""
|
||||||
className="h-9 w-16 shrink-0 rounded object-cover"
|
className="h-9 w-16 shrink-0 rounded object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -80,11 +80,6 @@ export function setShowPoster(showId: string, imageId: string | null) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ссылка на локальный кадр серии. */
|
|
||||||
export function episodeStillUrl(episodeId: string, bust?: string) {
|
|
||||||
return `/api/metadata/episodes/${episodeId}/still${bust ? `?v=${encodeURIComponent(bust)}` : ''}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Довыгрузить метаданные серий из привязанного источника. Возвращает число обновлённых. */
|
/** Довыгрузить метаданные серий из привязанного источника. Возвращает число обновлённых. */
|
||||||
export function refreshEpisodesMetadata(showId: string) {
|
export function refreshEpisodesMetadata(showId: string) {
|
||||||
return apiRequest<number>(`/admin/metadata/shows/${showId}/refresh-episodes`, { method: 'POST' })
|
return apiRequest<number>(`/admin/metadata/shows/${showId}/refresh-episodes`, { method: 'POST' })
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { cn } from '@/shared/lib/cn'
|
|||||||
import { Badge } from '@/shared/ui/badge'
|
import { Badge } from '@/shared/ui/badge'
|
||||||
import { Button } from '@/shared/ui/button'
|
import { Button } from '@/shared/ui/button'
|
||||||
import { ChannelPlayer } from './ChannelPlayer'
|
import { ChannelPlayer } from './ChannelPlayer'
|
||||||
import { episodeStillUrl, getEpg, imageUrl, listChannels, watchChannel } from './api'
|
import { getEpg, imageUrl, listChannels, watchChannel } from './api'
|
||||||
|
|
||||||
function formatTime(iso: string) {
|
function formatTime(iso: string) {
|
||||||
return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||||
@@ -138,9 +138,9 @@ export function AirPage() {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{current && (
|
{current && (
|
||||||
<div className="crt-panel flex gap-3 rounded-md p-3">
|
<div className="crt-panel flex gap-3 rounded-md p-3">
|
||||||
{currentEntry?.episodeHasStill && currentEntry.episodeId ? (
|
{currentEntry?.episodeStillImageId ? (
|
||||||
<img
|
<img
|
||||||
src={episodeStillUrl(currentEntry.episodeId)}
|
src={imageUrl(currentEntry.episodeStillImageId)}
|
||||||
alt=""
|
alt=""
|
||||||
className="hidden h-24 w-40 shrink-0 rounded object-cover sm:block"
|
className="hidden h-24 w-40 shrink-0 rounded object-cover sm:block"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { apiRequest } from '@/shared/api/client'
|
import { apiRequest } from '@/shared/api/client'
|
||||||
import type { PublicChannelDto, PublicEpgEntryDto } from '@/shared/api/types'
|
import type { PublicChannelDto, PublicEpgEntryDto } from '@/shared/api/types'
|
||||||
|
|
||||||
/** Ссылка на изображение общего реестра (постеры) — по id из публичных DTO. */
|
/** Ссылка на изображение общего реестра (постеры/кадры) — по id из публичных DTO. */
|
||||||
export function imageUrl(imageId: string) {
|
export function imageUrl(imageId: string) {
|
||||||
return `/api/images/${imageId}`
|
return `/api/images/${imageId}`
|
||||||
}
|
}
|
||||||
/** Кадр серии (пока по-старому — публичный эндпоинт метаданных). */
|
|
||||||
export function episodeStillUrl(episodeId: string) {
|
|
||||||
return `/api/metadata/episodes/${episodeId}/still`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listChannels() {
|
export function listChannels() {
|
||||||
return apiRequest<PublicChannelDto[]>('/channels')
|
return apiRequest<PublicChannelDto[]>('/channels')
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export type EpisodeDto = {
|
|||||||
episode: number | null
|
episode: number | null
|
||||||
title: string | null
|
title: string | null
|
||||||
overview: string | null
|
overview: string | null
|
||||||
hasStill: boolean
|
stillImageId: string | null
|
||||||
airDate: string | null
|
airDate: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ export type BumperTemplateDto = {
|
|||||||
backgroundColor2: string
|
backgroundColor2: string
|
||||||
accentColor: string
|
accentColor: string
|
||||||
textColor: string
|
textColor: string
|
||||||
hasBackground: boolean
|
backgroundImageId: string | null
|
||||||
hasAudio: boolean
|
hasAudio: boolean
|
||||||
audioDurationSeconds: number | null
|
audioDurationSeconds: number | null
|
||||||
}
|
}
|
||||||
@@ -236,5 +236,5 @@ export type PublicEpgEntryDto = {
|
|||||||
episodeId: string | null
|
episodeId: string | null
|
||||||
episodeTitle: string | null
|
episodeTitle: string | null
|
||||||
episodeOverview: string | null
|
episodeOverview: string | null
|
||||||
episodeHasStill: boolean
|
episodeStillImageId: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ const resources = {
|
|||||||
bumperPreviewHint: 'Пример со звуком и анимацией (примерные названия шоу). Использует сохранённые настройки.',
|
bumperPreviewHint: 'Пример со звуком и анимацией (примерные названия шоу). Использует сохранённые настройки.',
|
||||||
bumperBackground: 'Фон-картинка',
|
bumperBackground: 'Фон-картинка',
|
||||||
bumperBackgroundHint: 'Картинка фона; иначе — постер шоу или градиент',
|
bumperBackgroundHint: 'Картинка фона; иначе — постер шоу или градиент',
|
||||||
|
bumperBackgroundPick: 'Выбрать из галереи',
|
||||||
bumperFileLoaded: 'загружено',
|
bumperFileLoaded: 'загружено',
|
||||||
bumperFileDefault: 'по умолчанию',
|
bumperFileDefault: 'по умолчанию',
|
||||||
bumperUpload: 'Загрузить',
|
bumperUpload: 'Загрузить',
|
||||||
@@ -533,6 +534,7 @@ const resources = {
|
|||||||
bumperPreviewHint: 'Sample with sound and animation (example show names). Uses saved settings.',
|
bumperPreviewHint: 'Sample with sound and animation (example show names). Uses saved settings.',
|
||||||
bumperBackground: 'Background image',
|
bumperBackground: 'Background image',
|
||||||
bumperBackgroundHint: 'Background image; otherwise the show poster or a gradient',
|
bumperBackgroundHint: 'Background image; otherwise the show poster or a gradient',
|
||||||
|
bumperBackgroundPick: 'Pick from gallery',
|
||||||
bumperFileLoaded: 'loaded',
|
bumperFileLoaded: 'loaded',
|
||||||
bumperFileDefault: 'default',
|
bumperFileDefault: 'default',
|
||||||
bumperUpload: 'Upload',
|
bumperUpload: 'Upload',
|
||||||
|
|||||||
Reference in New Issue
Block a user