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:
Leonid Pershin
2026-07-25 11:56:36 +03:00
parent a970eae7d2
commit 7efd616c29
35 changed files with 1227 additions and 279 deletions
@@ -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",
};
}
@@ -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
}
}
}
@@ -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);
}
}