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)
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
admin
|
||||
.MapPut("/{id:guid}/bumper/templates/{templateId:guid}/background", UploadTemplateBackground)
|
||||
.MapPut("/{id:guid}/bumper/templates/{templateId:guid}/background", SetTemplateBackground)
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
admin
|
||||
.MapDelete("/{id:guid}/bumper/templates/{templateId:guid}/background", ClearTemplateBackground)
|
||||
@@ -333,27 +333,18 @@ public static class ChannelEndpoints
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> UploadTemplateBackground(
|
||||
private static async Task<IResult> SetTemplateBackground(
|
||||
Guid id,
|
||||
Guid templateId,
|
||||
string fileName,
|
||||
HttpRequest request,
|
||||
IBumperTemplateStorage storage,
|
||||
SetBumperTemplateBackgroundBody body,
|
||||
ISender sender,
|
||||
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(
|
||||
new SetBumperTemplateBackgroundCommand(id, templateId, ext),
|
||||
new SetBumperTemplateBackgroundCommand(id, templateId, body.ImageId),
|
||||
cancellationToken
|
||||
);
|
||||
if (!result.IsSuccess)
|
||||
storage.DeleteBackground(templateId);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
@@ -539,6 +530,8 @@ public sealed record AddChannelAdBody(Guid MediaAssetId);
|
||||
|
||||
public sealed record AddBumperTemplateBody(string Name);
|
||||
|
||||
public sealed record SetBumperTemplateBackgroundBody(Guid ImageId);
|
||||
|
||||
public sealed record UpdateBumperTemplateBody(
|
||||
string Name,
|
||||
string BackgroundColor,
|
||||
@@ -547,24 +540,11 @@ public sealed record UpdateBumperTemplateBody(
|
||||
string TextColor
|
||||
);
|
||||
|
||||
/// <summary>Ограничения на загружаемые файлы блока заставки (звук/фон-картинка).</summary>
|
||||
/// <summary>Ограничения на загружаемый звук блока заставки (фон-картинка — через общий реестр).</summary>
|
||||
internal static class BumperFiles
|
||||
{
|
||||
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>(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Api.Common;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Images.DeleteImage;
|
||||
@@ -48,12 +47,7 @@ public static class MetadataEndpoints
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
admin.MapPost("/shows/{showId:guid}/refresh-episodes", RefreshEpisodes).Produces<int>();
|
||||
|
||||
// Кадры серий отдаём публично (просто картинки, id не угадать) — чтобы работал <img src>.
|
||||
// Постеры шоу теперь в общем реестре и отдаются по /api/images/{id}.
|
||||
app.MapGet("/api/metadata/episodes/{episodeId:guid}/still", ServeStill)
|
||||
.WithTags("Metadata")
|
||||
.Produces(StatusCodes.Status200OK);
|
||||
|
||||
// Постеры шоу и кадры серий теперь в общем реестре и отдаются по /api/images/{id}.
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -181,37 +175,6 @@ public static class MetadataEndpoints
|
||||
);
|
||||
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);
|
||||
|
||||
+3
-5
@@ -5,10 +5,8 @@ using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||
|
||||
public sealed class ClearBumperTemplateBackgroundCommandHandler(
|
||||
IAppDbContext dbContext,
|
||||
IBumperTemplateStorage storage
|
||||
) : ICommandHandler<ClearBumperTemplateBackgroundCommand, Result>
|
||||
public sealed class ClearBumperTemplateBackgroundCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<ClearBumperTemplateBackgroundCommand, Result>
|
||||
{
|
||||
public async Task<Result> Handle(
|
||||
ClearBumperTemplateBackgroundCommand command,
|
||||
@@ -25,8 +23,8 @@ public sealed class ClearBumperTemplateBackgroundCommandHandler(
|
||||
if (template is null)
|
||||
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
|
||||
|
||||
// Отвязываем фон; сама картинка остаётся в галерее.
|
||||
template.ClearBackgroundImage();
|
||||
storage.DeleteBackground(command.TemplateId);
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -12,6 +12,7 @@ public sealed class RenderBumperPreviewQueryHandler(
|
||||
IAppDbContext dbContext,
|
||||
IBumperRenderer renderer,
|
||||
IBumperTemplateStorage storage,
|
||||
IImageStore imageStore,
|
||||
IOptions<BumperOptions> bumperOptions,
|
||||
IOptions<StreamingOptions> streamingOptions
|
||||
) : IQueryHandler<RenderBumperPreviewQuery, Result<Guid>>
|
||||
@@ -40,6 +41,18 @@ public sealed class RenderBumperPreviewQueryHandler(
|
||||
|
||||
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 =
|
||||
template.AudioDurationSeconds is { } d and > 0 ? d : DefaultBumperDurationSeconds;
|
||||
var aligned = (int)(
|
||||
@@ -59,7 +72,7 @@ public sealed class RenderBumperPreviewQueryHandler(
|
||||
fromName,
|
||||
channel.BumperNextLabel,
|
||||
toName,
|
||||
storage.BackgroundPath(template.Id, template.BackgroundImageExtension),
|
||||
backgroundPath,
|
||||
storage.AudioPath(template.Id, template.AudioExtension),
|
||||
// Постер зависит от конкретного «следующего» шоу — в превью не подставляем.
|
||||
null
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@ using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Broadcast.Bumpers;
|
||||
|
||||
/// <summary>Отметить загруженную фон-картинку блока (расширение — с точкой).</summary>
|
||||
/// <summary>Привязать фон-картинку блока по ссылке на изображение из реестра (галерея).</summary>
|
||||
public sealed record SetBumperTemplateBackgroundCommand(
|
||||
Guid ChannelId,
|
||||
Guid TemplateId,
|
||||
string Extension
|
||||
Guid ImageId
|
||||
) : ICommand<Result>;
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ public sealed class SetBumperTemplateBackgroundCommandHandler(IAppDbContext dbCo
|
||||
if (template is null)
|
||||
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
|
||||
|
||||
template.SetBackgroundImage(command.Extension);
|
||||
template.SetBackgroundImage(command.ImageId);
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed record BumperTemplateDto(
|
||||
string BackgroundColor2,
|
||||
string AccentColor,
|
||||
string TextColor,
|
||||
bool HasBackground,
|
||||
Guid? BackgroundImageId,
|
||||
bool HasAudio,
|
||||
double? AudioDurationSeconds
|
||||
);
|
||||
|
||||
@@ -75,7 +75,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext)
|
||||
t.BackgroundColor2,
|
||||
t.AccentColor,
|
||||
t.TextColor,
|
||||
t.BackgroundImageExtension is not null,
|
||||
t.BackgroundImageId,
|
||||
t.AudioExtension is not null,
|
||||
t.AudioDurationSeconds
|
||||
))
|
||||
|
||||
@@ -216,6 +216,25 @@ public sealed class ScheduleGenerator(
|
||||
)
|
||||
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 — файлы могли удалить).
|
||||
var cached = await dbContext.BumperAssets.AsNoTracking()
|
||||
.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 posterToken = poster.ImageId == Guid.Empty ? "-" : poster.ImageId.ToString();
|
||||
var posterAbs = poster.ImageId == Guid.Empty ? null : poster.AbsPath;
|
||||
var bgAbs = bgByTemplate.GetValueOrDefault(template.Id);
|
||||
var aligned = AlignedDurationSeconds(TemplateDurationSeconds(template));
|
||||
var signature = ComputeSignature(channel, template, fromName, toName, aligned, posterToken);
|
||||
|
||||
@@ -272,6 +292,7 @@ public sealed class ScheduleGenerator(
|
||||
aligned,
|
||||
signature,
|
||||
posterAbs,
|
||||
bgAbs,
|
||||
cancellationToken
|
||||
);
|
||||
result[combo] = assetId;
|
||||
@@ -300,14 +321,22 @@ public sealed class ScheduleGenerator(
|
||||
int alignedDurationSeconds,
|
||||
string signature,
|
||||
string? posterAbsolutePath,
|
||||
string? backgroundAbsolutePath,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var asset = MediaAsset.RegisterGenerated($"Заставка: {fromName} → {toName}");
|
||||
var posterAbs = posterAbsolutePath;
|
||||
var render = await bumperRenderer.RenderAsync(
|
||||
asset.Id,
|
||||
BuildSpec(channel, template, alignedDurationSeconds, fromName, toName, posterAbs),
|
||||
BuildSpec(
|
||||
channel,
|
||||
template,
|
||||
alignedDurationSeconds,
|
||||
fromName,
|
||||
toName,
|
||||
posterAbsolutePath,
|
||||
backgroundAbsolutePath
|
||||
),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
@@ -335,7 +364,8 @@ public sealed class ScheduleGenerator(
|
||||
int alignedDurationSeconds,
|
||||
string fromName,
|
||||
string toName,
|
||||
string? posterAbsolutePath
|
||||
string? posterAbsolutePath,
|
||||
string? backgroundAbsolutePath
|
||||
) =>
|
||||
new(
|
||||
alignedDurationSeconds,
|
||||
@@ -350,7 +380,7 @@ public sealed class ScheduleGenerator(
|
||||
fromName,
|
||||
channel.BumperNextLabel,
|
||||
toName,
|
||||
bumperStorage.BackgroundPath(template.Id, template.BackgroundImageExtension),
|
||||
backgroundAbsolutePath,
|
||||
bumperStorage.AudioPath(template.Id, template.AudioExtension),
|
||||
posterAbsolutePath
|
||||
);
|
||||
@@ -397,7 +427,7 @@ public sealed class ScheduleGenerator(
|
||||
template.AccentColor,
|
||||
template.TextColor,
|
||||
template.Revision,
|
||||
template.BackgroundImageExtension ?? "-",
|
||||
template.BackgroundImageId?.ToString() ?? "-",
|
||||
template.AudioExtension ?? "-",
|
||||
fromName,
|
||||
toName,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace TeleWave.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Хранилище сырых файлов блоков заставок (звук и фон-картинка) под bumpers/{templateId}. В отличие
|
||||
/// от обычных ассетов эти файлы НЕ режутся на HLS — они подаются как входы в рендер заставки.
|
||||
/// Хранилище звука блоков заставок под bumpers/{templateId}. Фон-картинка блока живёт в общем реестре
|
||||
/// изображений (см. IImageStore). Звук НЕ режется на HLS — подаётся входом в рендер заставки.
|
||||
/// </summary>
|
||||
public interface IBumperTemplateStorage
|
||||
{
|
||||
@@ -13,22 +13,11 @@ public interface IBumperTemplateStorage
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
Task SaveBackgroundAsync(
|
||||
Guid templateId,
|
||||
string extension,
|
||||
Stream content,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
void DeleteAudio(Guid templateId);
|
||||
void DeleteBackground(Guid templateId);
|
||||
|
||||
/// <summary>Удалить все файлы блока (при удалении самого блока).</summary>
|
||||
void DeleteTemplate(Guid templateId);
|
||||
|
||||
/// <summary>Абсолютный путь к загруженному звуку или null (нет расширения / файл отсутствует).</summary>
|
||||
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.Title,
|
||||
e.Overview,
|
||||
e.StillPath is not null,
|
||||
e.StillImageId,
|
||||
e.AirDate
|
||||
);
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed record EpisodeDto(
|
||||
int? Episode,
|
||||
string? Title,
|
||||
string? Overview,
|
||||
bool HasStill,
|
||||
Guid? StillImageId,
|
||||
DateOnly? AirDate
|
||||
);
|
||||
|
||||
|
||||
+24
-8
@@ -3,13 +3,15 @@ using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Application.Library;
|
||||
using TeleWave.Domain.Images;
|
||||
|
||||
namespace TeleWave.Application.Metadata.RefreshEpisodes;
|
||||
|
||||
public sealed class RefreshShowEpisodesMetadataCommandHandler(
|
||||
IAppDbContext dbContext,
|
||||
IMetadataProviderResolver resolver,
|
||||
IMetadataImageStore imageStore
|
||||
IImageDownloader downloader,
|
||||
IImageStore imageStore
|
||||
) : ICommandHandler<RefreshShowEpisodesMetadataCommand, Result<int>>
|
||||
{
|
||||
public async Task<Result<int>> Handle(
|
||||
@@ -66,15 +68,29 @@ public sealed class RefreshShowEpisodesMetadataCommandHandler(
|
||||
if (meta is null)
|
||||
continue;
|
||||
|
||||
string? stillPath = null;
|
||||
Guid? stillImageId = null;
|
||||
if (!string.IsNullOrEmpty(meta.StillUrl))
|
||||
stillPath = await imageStore.DownloadEpisodeStillAsync(
|
||||
episode.Id,
|
||||
meta.StillUrl,
|
||||
cancellationToken
|
||||
);
|
||||
{
|
||||
var downloaded = await downloader.DownloadAsync(meta.StillUrl, cancellationToken);
|
||||
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
|
||||
);
|
||||
stillImageId = image.Id;
|
||||
}
|
||||
}
|
||||
|
||||
episode.ApplyMetadata(meta.Title, meta.Overview, stillPath, meta.AirDate);
|
||||
episode.ApplyMetadata(meta.Title, meta.Overview, stillImageId, meta.AirDate);
|
||||
updated++;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext)
|
||||
e.Id,
|
||||
e.Title,
|
||||
e.Overview,
|
||||
e.StillPath,
|
||||
e.StillImageId,
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
var episodeByKey = episodes
|
||||
@@ -82,7 +82,7 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext)
|
||||
episode?.Id,
|
||||
episode?.Title,
|
||||
episode?.Overview,
|
||||
episode?.StillPath is not null
|
||||
episode?.StillImageId
|
||||
);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed record PublicEpgEntryDto(
|
||||
Guid? EpisodeId,
|
||||
string? EpisodeTitle,
|
||||
string? EpisodeOverview,
|
||||
bool EpisodeHasStill
|
||||
Guid? EpisodeStillImageId
|
||||
);
|
||||
|
||||
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 TextColor { get; private set; } = DefaultTextColor;
|
||||
|
||||
/// <summary>Расширение загруженной фон-картинки (с точкой) или null — тогда фон градиент/постер.</summary>
|
||||
public string? BackgroundImageExtension { get; private set; }
|
||||
/// <summary>Фон-картинка блока — ссылка на запись реестра изображений или null (тогда фон градиент/постер).</summary>
|
||||
public Guid? BackgroundImageId { get; private set; }
|
||||
|
||||
/// <summary>Расширение загруженного звука (с точкой) или null — тогда синтезируется джингл.</summary>
|
||||
public string? AudioExtension { get; private set; }
|
||||
@@ -59,7 +59,7 @@ public class BumperTemplate
|
||||
BackgroundColor2 = DefaultBackgroundColor2,
|
||||
AccentColor = DefaultAccentColor,
|
||||
TextColor = DefaultTextColor,
|
||||
BackgroundImageExtension = null,
|
||||
BackgroundImageId = null,
|
||||
AudioExtension = null,
|
||||
AudioDurationSeconds = null,
|
||||
Revision = 0,
|
||||
@@ -99,18 +99,18 @@ public class BumperTemplate
|
||||
Revision++;
|
||||
}
|
||||
|
||||
/// <summary>Отметить загруженную фон-картинку (extension — с точкой, нижний регистр). Меняет ревизию.</summary>
|
||||
public void SetBackgroundImage(string extension)
|
||||
/// <summary>Привязать фон-картинку блока (ссылка на реестр изображений). Меняет ревизию.</summary>
|
||||
public void SetBackgroundImage(Guid imageId)
|
||||
{
|
||||
BackgroundImageExtension = extension;
|
||||
BackgroundImageId = imageId;
|
||||
Revision++;
|
||||
}
|
||||
|
||||
public void ClearBackgroundImage()
|
||||
{
|
||||
if (BackgroundImageExtension is null)
|
||||
if (BackgroundImageId is null)
|
||||
return;
|
||||
BackgroundImageExtension = null;
|
||||
BackgroundImageId = null;
|
||||
Revision++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ public class ShowEpisode
|
||||
public string? Title { get; private set; }
|
||||
public string? Overview { get; private set; }
|
||||
|
||||
/// <summary>Относительный путь локального кадра или null.</summary>
|
||||
public string? StillPath { get; private set; }
|
||||
/// <summary>Кадр серии — ссылка на запись общего реестра изображений или null.</summary>
|
||||
public Guid? StillImageId { get; private set; }
|
||||
public DateOnly? AirDate { get; private set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
@@ -43,13 +43,13 @@ public class ShowEpisode
|
||||
Episode = episode;
|
||||
}
|
||||
|
||||
/// <summary>Применить метаданные серии (кадр — уже скачанный локально — может быть null).</summary>
|
||||
public void ApplyMetadata(string? title, string? overview, string? stillPath, DateOnly? airDate)
|
||||
/// <summary>Применить метаданные серии (кадр — уже зарегистрирован в реестре — может быть null).</summary>
|
||||
public void ApplyMetadata(string? title, string? overview, Guid? stillImageId, DateOnly? airDate)
|
||||
{
|
||||
Title = title;
|
||||
Overview = overview;
|
||||
if (stillPath is not null)
|
||||
StillPath = stillPath;
|
||||
if (stillImageId is not null)
|
||||
StillImageId = stillImageId;
|
||||
AirDate = airDate;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class ShowEpisode
|
||||
{
|
||||
Title = null;
|
||||
Overview = null;
|
||||
StillPath = null;
|
||||
StillImageId = null;
|
||||
AirDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,6 @@ public static class DependencyInjection
|
||||
services.AddSingleton<IMetadataProvider, TmdbMetadataProvider>();
|
||||
services.AddSingleton<IMetadataProvider, OmdbMetadataProvider>();
|
||||
services.AddSingleton<IMetadataProviderResolver, MetadataProviderResolver>();
|
||||
services.AddSingleton<IMetadataImageStore, MetadataImageStore>();
|
||||
}
|
||||
|
||||
/// <summary>Планировщик расписания: генератор, источник случайности и фоновый сервис горизонта.</summary>
|
||||
|
||||
@@ -3,13 +3,12 @@ using TeleWave.Application.Common.Interfaces;
|
||||
namespace TeleWave.Infrastructure.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Файловое хранилище блоков заставок: сырые звук/фон под bumpers/{templateId}/{kind}{ext}.
|
||||
/// На блок — не более одного файла каждого вида (при загрузке старый удаляется).
|
||||
/// Файловое хранилище звука блоков заставок: bumpers/{templateId}/audio{ext}. На блок — не более
|
||||
/// одного файла (при загрузке старый удаляется). Фон-картинка блока хранится в общем реестре.
|
||||
/// </summary>
|
||||
public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemplateStorage
|
||||
{
|
||||
private const string Audio = "audio";
|
||||
private const string Background = "background";
|
||||
|
||||
public Task SaveAudioAsync(
|
||||
Guid templateId,
|
||||
@@ -18,17 +17,8 @@ public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemp
|
||||
CancellationToken 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 DeleteBackground(Guid templateId) => DeleteKind(templateId, Background);
|
||||
|
||||
public void DeleteTemplate(Guid templateId)
|
||||
{
|
||||
var dir = paths.BumperTemplateDir(templateId);
|
||||
@@ -39,9 +29,6 @@ public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemp
|
||||
public string? AudioPath(Guid templateId, string? extension) =>
|
||||
ResolvePath(templateId, Audio, extension);
|
||||
|
||||
public string? BackgroundPath(Guid templateId, string? extension) =>
|
||||
ResolvePath(templateId, Background, extension);
|
||||
|
||||
private async Task SaveAsync(
|
||||
Guid templateId,
|
||||
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)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("BackgroundImageExtension")
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
b.Property<Guid?>("BackgroundImageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChannelId")
|
||||
.HasColumnType("uuid");
|
||||
@@ -554,9 +553,8 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
b.Property<Guid>("ShowId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("StillPath")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
b.Property<Guid?>("StillImageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(512)
|
||||
|
||||
@@ -69,7 +69,6 @@ public class BumperTemplateConfiguration : IEntityTypeConfiguration<BumperTempla
|
||||
builder.Property(x => x.BackgroundColor2).IsRequired().HasMaxLength(32);
|
||||
builder.Property(x => x.AccentColor).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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,5 @@ public class ShowEpisodeConfiguration : IEntityTypeConfiguration<ShowEpisode>
|
||||
builder.HasIndex(x => x.MediaAssetId);
|
||||
builder.Property(x => x.Title).HasMaxLength(512);
|
||||
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 paths = scope.ServiceProvider.GetRequiredService<MediaPathResolver>();
|
||||
|
||||
Directory.CreateDirectory(paths.ImagesDir);
|
||||
|
||||
// Постеры шоу: metadata/shows/{showId}/poster{ext} → images/{imageId}{ext}.
|
||||
var posters = await dbContext.Shows.AsNoTracking()
|
||||
.Where(s => s.PosterImageId != null)
|
||||
.Join(
|
||||
dbContext.Images,
|
||||
s => s.PosterImageId,
|
||||
i => i.Id,
|
||||
(s, i) => new
|
||||
{
|
||||
ShowId = s.Id,
|
||||
ImageId = i.Id,
|
||||
i.FileExtension,
|
||||
}
|
||||
(s, i) => new { EntityId = s.Id, ImageId = i.Id, i.FileExtension }
|
||||
)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var p in posters)
|
||||
{
|
||||
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;
|
||||
Relocate(paths.ImagePath(p.ImageId, p.FileExtension), paths.MetadataShowPosterPath(p.EntityId, p.FileExtension));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user