Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
|
||||
namespace TeleWave.Infrastructure.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Обслуживание, которое раньше не делалось вовсе: чистка отрендеренных заставок, на которые больше
|
||||
/// никто не ссылается. Они копятся при каждой смене пары шоу и при каждой правке блока заставки,
|
||||
/// а после перехода на длинное хранение расписания их станет заметно больше.
|
||||
///
|
||||
/// Идёт по расписанию отдельно от генерации: та держит advisory-лок канала, и подмешивать в неё
|
||||
/// удаление по всей таблице не нужно.
|
||||
/// </summary>
|
||||
public sealed class MaintenanceBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<MaintenanceBackgroundService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan Interval = TimeSpan.FromHours(6);
|
||||
private static readonly TimeSpan StartupDelay = TimeSpan.FromMinutes(2);
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(StartupDelay, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var timer = new PeriodicTimer(Interval);
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await TickAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка обслуживания");
|
||||
}
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task TickAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
|
||||
|
||||
// Осиротевшей считается заставка, чей ассет не встречается ни в одной записи расписания.
|
||||
// Границу по времени не ставим: окно хранения расписания уже определяет, что живо.
|
||||
var removed = await db
|
||||
.BumperAssets.Where(b =>
|
||||
!db.ScheduleEntries.Any(e => e.MediaAssetId == b.MediaAssetId)
|
||||
)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (removed > 0)
|
||||
logger.LogInformation("Обслуживание: удалено осиротевших заставок — {Count}", removed);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,96 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Application.Broadcast.Scheduling;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
|
||||
namespace TeleWave.Infrastructure.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Периодически достраивает расписание каждого включённого канала до горизонта. Расширение хвоста —
|
||||
/// без удаления существующих записей, поэтому эфир не «дёргается».
|
||||
/// </summary>
|
||||
public sealed class SchedulingBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<SchedulerOptions> options,
|
||||
ILogger<SchedulingBackgroundService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private readonly SchedulerOptions _options = options.Value;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Небольшая задержка на старте — дать примениться миграциям/сидингу.
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var timer = new PeriodicTimer(
|
||||
TimeSpan.FromMinutes(Math.Max(1, _options.TickMinutes))
|
||||
);
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await TickAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка тика планировщика");
|
||||
}
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task TickAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
|
||||
var generator = scope.ServiceProvider.GetRequiredService<ScheduleGenerator>();
|
||||
|
||||
var channelIds = await db
|
||||
.Channels.Where(c => c.IsEnabled)
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
foreach (var channelId in channelIds)
|
||||
{
|
||||
var added = await generator.GenerateAsync(
|
||||
channelId,
|
||||
now,
|
||||
regenerate: false,
|
||||
cancellationToken
|
||||
);
|
||||
if (added > 0)
|
||||
logger.LogInformation(
|
||||
"Канал {ChannelId}: добавлено {Count} записей расписания",
|
||||
channelId,
|
||||
added
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TeleWave.Application.Broadcast.Scheduling;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Programming.Planning;
|
||||
|
||||
namespace TeleWave.Infrastructure.Broadcast;
|
||||
|
||||
/// <summary>
|
||||
/// Периодически достраивает расписание каждого включённого канала до горизонта. Расширение хвоста —
|
||||
/// без удаления существующих записей, поэтому эфир не «дёргается».
|
||||
/// </summary>
|
||||
public sealed class SchedulingBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<SchedulerOptions> options,
|
||||
ILogger<SchedulingBackgroundService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private readonly SchedulerOptions _options = options.Value;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Небольшая задержка на старте — дать примениться миграциям/сидингу.
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var timer = new PeriodicTimer(
|
||||
TimeSpan.FromMinutes(Math.Max(1, _options.TickMinutes))
|
||||
);
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await TickAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Ошибка тика планировщика");
|
||||
}
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task TickAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
|
||||
var generator = scope.ServiceProvider.GetRequiredService<GridScheduleGenerator>();
|
||||
|
||||
// Эфир строит только шаблон сетки: канал без шаблона вещать не может и молча пропускается.
|
||||
var channelIds = await db
|
||||
.Channels.Where(c => c.IsEnabled && c.TemplateId != null)
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
foreach (var channelId in channelIds)
|
||||
{
|
||||
var report = await generator.GenerateAsync(
|
||||
channelId,
|
||||
now,
|
||||
rebuildFuture: false,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (report.Added > 0)
|
||||
logger.LogInformation(
|
||||
"Канал {ChannelId}: добавлено {Count} записей расписания",
|
||||
channelId,
|
||||
report.Added
|
||||
);
|
||||
|
||||
foreach (var warning in report.Warnings)
|
||||
logger.LogWarning(
|
||||
"Канал {ChannelId}, слот {SlotId}: {Warning} — {Details}",
|
||||
channelId,
|
||||
warning.SlotId,
|
||||
warning.Kind,
|
||||
warning.Details
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,167 +1,180 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Broadcast.Scheduling;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Streaming;
|
||||
using TeleWave.Domain.Broadcast.Scheduling;
|
||||
using TeleWave.Infrastructure.Broadcast;
|
||||
using TeleWave.Infrastructure.Identity;
|
||||
using TeleWave.Infrastructure.Media;
|
||||
using TeleWave.Infrastructure.Metadata;
|
||||
using TeleWave.Infrastructure.Persistence;
|
||||
using TeleWave.Infrastructure.Settings;
|
||||
using TeleWave.Infrastructure.Streaming;
|
||||
|
||||
namespace TeleWave.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Регистрация сервисов инфраструктуры: EF Core (PostgreSQL), Identity/JWT-аутентификация.
|
||||
/// </summary>
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration
|
||||
)
|
||||
{
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseNpgsql(
|
||||
configuration["ConnectionStrings:Default"]
|
||||
?? throw new InvalidOperationException(
|
||||
"Строка подключения 'ConnectionStrings:Default' не сконфигурирована."
|
||||
)
|
||||
)
|
||||
);
|
||||
services.AddScoped<IAppDbContext>(sp => sp.GetRequiredService<AppDbContext>());
|
||||
|
||||
services
|
||||
.AddIdentityCore<AppUser>(options =>
|
||||
{
|
||||
options.User.RequireUniqueEmail = false;
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequireUppercase = true;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
})
|
||||
.AddRoles<AppRole>()
|
||||
.AddEntityFrameworkStores<AppDbContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
|
||||
services.Configure<AdminSeedOptions>(
|
||||
configuration.GetSection(AdminSeedOptions.SectionName)
|
||||
);
|
||||
|
||||
var jwtOptions =
|
||||
configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>()
|
||||
?? throw new InvalidOperationException("Секция конфигурации 'Jwt' не задана.");
|
||||
|
||||
// Fail-fast на подписывающем ключе: этот же ключ подписывает и JWT, и stream-токены
|
||||
// (StreamTokenService), поэтому placeholder/короткий ключ из appsettings.json = полный обход
|
||||
// авторизации (можно сфорджить admin-JWT). Лучше не стартовать вовсе, чем стартовать уязвимым.
|
||||
// HMAC-SHA256 требует ключ не короче размера хеша (32 байта), иначе он ослаблен нулевым паддингом.
|
||||
if (Encoding.UTF8.GetByteCount(jwtOptions.SigningKey) < 32)
|
||||
throw new InvalidOperationException(
|
||||
"Jwt:SigningKey должен быть не короче 32 байт. Задайте криптостойкий секрет через конфигурацию/переменную окружения Jwt__SigningKey."
|
||||
);
|
||||
if (jwtOptions.SigningKey.Contains("change-me", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException(
|
||||
"Jwt:SigningKey использует значение-заглушку из appsettings.json. Переопределите его криптостойким секретом (Jwt__SigningKey)."
|
||||
);
|
||||
|
||||
services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = jwtOptions.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = jwtOptions.Audience,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(jwtOptions.SigningKey)
|
||||
),
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromSeconds(30),
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthorization();
|
||||
|
||||
services.AddScoped<IIdentityService, IdentityService>();
|
||||
services.AddScoped<IJwtTokenService, JwtTokenService>();
|
||||
services.AddScoped<IRefreshTokenService, RefreshTokenService>();
|
||||
services.AddScoped<IRoleService, RoleService>();
|
||||
services.AddScoped<ICurrentUser, CurrentUser>();
|
||||
services.AddScoped<ISiteSettings, SiteSettings>();
|
||||
services.AddScoped<DbInitializer>();
|
||||
|
||||
AddMedia(services, configuration);
|
||||
AddBroadcast(services, configuration);
|
||||
AddMetadata(services, configuration);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>Метаданные шоу/серий: провайдеры TMDb/OMDb, резолвер, локальное хранилище картинок.</summary>
|
||||
private static void AddMetadata(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<MetadataOptions>(configuration.GetSection(MetadataOptions.SectionName));
|
||||
services.AddHttpClient("metadata", client => client.Timeout = TimeSpan.FromSeconds(15));
|
||||
services.AddSingleton<IMetadataProvider, TmdbMetadataProvider>();
|
||||
services.AddSingleton<IMetadataProvider, OmdbMetadataProvider>();
|
||||
services.AddSingleton<IMetadataProviderResolver, MetadataProviderResolver>();
|
||||
}
|
||||
|
||||
/// <summary>Планировщик расписания: генератор, источник случайности и фоновый сервис горизонта.</summary>
|
||||
private static void AddBroadcast(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<SchedulerOptions>(
|
||||
configuration.GetSection(SchedulerOptions.SectionName)
|
||||
);
|
||||
services.Configure<StreamingOptions>(
|
||||
configuration.GetSection(StreamingOptions.SectionName)
|
||||
);
|
||||
services.Configure<BumperOptions>(configuration.GetSection(BumperOptions.SectionName));
|
||||
|
||||
services.AddSingleton<IRandomSource, SystemRandomSource>();
|
||||
services.AddSingleton<StreamTokenService>();
|
||||
services.AddSingleton<IBumperRenderer, FfmpegBumperRenderer>();
|
||||
services.AddScoped<ScheduleBumperResolver>();
|
||||
services.AddScoped<ScheduleGenerator>();
|
||||
services.AddHostedService<SchedulingBackgroundService>();
|
||||
}
|
||||
|
||||
/// <summary>Хранилище медиа, обработка ffmpeg, очередь и фоновые сервисы (очередь + inbox-сканер).</summary>
|
||||
private static void AddMedia(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<StorageOptions>(configuration.GetSection(StorageOptions.SectionName));
|
||||
services.Configure<MediaOptions>(configuration.GetSection(MediaOptions.SectionName));
|
||||
|
||||
services.AddSingleton<MediaPathResolver>();
|
||||
services.AddSingleton<IMediaStorage, FileSystemMediaStorage>();
|
||||
services.AddSingleton<IBumperTemplateStorage, BumperTemplateStorage>();
|
||||
services.AddSingleton<IImageStore, ImageStore>();
|
||||
services.AddSingleton<IImageDownloader, ImageDownloader>();
|
||||
services.AddSingleton<IAudioProbe, FfprobeAudioProbe>();
|
||||
services.AddSingleton<IMediaProcessor, FfmpegMediaProcessor>();
|
||||
services.AddSingleton<IMediaProcessingQueue, MediaProcessingQueue>();
|
||||
services.AddSingleton<IBumperRenderQueue, BumperRenderQueue>();
|
||||
|
||||
services.AddHostedService<MediaProcessingBackgroundService>();
|
||||
services.AddHostedService<InboxScannerBackgroundService>();
|
||||
services.AddHostedService<BumperRenderBackgroundService>();
|
||||
}
|
||||
}
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Broadcast.Scheduling;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Library.Genres;
|
||||
using TeleWave.Application.Programming.Groups;
|
||||
using TeleWave.Application.Programming.Planning;
|
||||
using TeleWave.Application.Programming.Templates;
|
||||
using TeleWave.Application.Streaming;
|
||||
using TeleWave.Domain.Broadcast.Scheduling;
|
||||
using TeleWave.Infrastructure.Broadcast;
|
||||
using TeleWave.Infrastructure.Identity;
|
||||
using TeleWave.Infrastructure.Library;
|
||||
using TeleWave.Infrastructure.Media;
|
||||
using TeleWave.Infrastructure.Metadata;
|
||||
using TeleWave.Infrastructure.Persistence;
|
||||
using TeleWave.Infrastructure.Settings;
|
||||
using TeleWave.Infrastructure.Streaming;
|
||||
|
||||
namespace TeleWave.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Регистрация сервисов инфраструктуры: EF Core (PostgreSQL), Identity/JWT-аутентификация.
|
||||
/// </summary>
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration
|
||||
)
|
||||
{
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseNpgsql(
|
||||
configuration["ConnectionStrings:Default"]
|
||||
?? throw new InvalidOperationException(
|
||||
"Строка подключения 'ConnectionStrings:Default' не сконфигурирована."
|
||||
)
|
||||
)
|
||||
);
|
||||
services.AddScoped<IAppDbContext>(sp => sp.GetRequiredService<AppDbContext>());
|
||||
|
||||
services
|
||||
.AddIdentityCore<AppUser>(options =>
|
||||
{
|
||||
options.User.RequireUniqueEmail = false;
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequireUppercase = true;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
})
|
||||
.AddRoles<AppRole>()
|
||||
.AddEntityFrameworkStores<AppDbContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
|
||||
services.Configure<AdminSeedOptions>(
|
||||
configuration.GetSection(AdminSeedOptions.SectionName)
|
||||
);
|
||||
|
||||
var jwtOptions =
|
||||
configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>()
|
||||
?? throw new InvalidOperationException("Секция конфигурации 'Jwt' не задана.");
|
||||
|
||||
// Fail-fast на подписывающем ключе: этот же ключ подписывает и JWT, и stream-токены
|
||||
// (StreamTokenService), поэтому placeholder/короткий ключ из appsettings.json = полный обход
|
||||
// авторизации (можно сфорджить admin-JWT). Лучше не стартовать вовсе, чем стартовать уязвимым.
|
||||
// HMAC-SHA256 требует ключ не короче размера хеша (32 байта), иначе он ослаблен нулевым паддингом.
|
||||
if (Encoding.UTF8.GetByteCount(jwtOptions.SigningKey) < 32)
|
||||
throw new InvalidOperationException(
|
||||
"Jwt:SigningKey должен быть не короче 32 байт. Задайте криптостойкий секрет через конфигурацию/переменную окружения Jwt__SigningKey."
|
||||
);
|
||||
if (jwtOptions.SigningKey.Contains("change-me", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException(
|
||||
"Jwt:SigningKey использует значение-заглушку из appsettings.json. Переопределите его криптостойким секретом (Jwt__SigningKey)."
|
||||
);
|
||||
|
||||
services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = jwtOptions.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = jwtOptions.Audience,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(jwtOptions.SigningKey)
|
||||
),
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromSeconds(30),
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthorization();
|
||||
|
||||
services.AddScoped<IIdentityService, IdentityService>();
|
||||
services.AddScoped<IJwtTokenService, JwtTokenService>();
|
||||
services.AddScoped<IRefreshTokenService, RefreshTokenService>();
|
||||
services.AddScoped<IRoleService, RoleService>();
|
||||
services.AddScoped<ICurrentUser, CurrentUser>();
|
||||
services.AddScoped<ISiteSettings, SiteSettings>();
|
||||
services.AddScoped<DbInitializer>();
|
||||
services.AddScoped<GenreSeeder>();
|
||||
services.AddScoped<GenreMatcher>();
|
||||
services.AddScoped<GroupElementResolver>();
|
||||
services.AddScoped<GroupStatsService>();
|
||||
services.AddScoped<GroupMembershipCleaner>();
|
||||
services.AddScoped<SlotWriter>();
|
||||
services.AddScoped<GroupExpander>();
|
||||
services.AddScoped<BumperResolver>();
|
||||
services.AddScoped<GridScheduleGenerator>();
|
||||
|
||||
AddMedia(services, configuration);
|
||||
AddBroadcast(services, configuration);
|
||||
AddMetadata(services, configuration);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>Метаданные шоу/серий: провайдеры TMDb/OMDb, резолвер, локальное хранилище картинок.</summary>
|
||||
private static void AddMetadata(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<MetadataOptions>(configuration.GetSection(MetadataOptions.SectionName));
|
||||
services.AddHttpClient("metadata", client => client.Timeout = TimeSpan.FromSeconds(15));
|
||||
services.AddSingleton<IMetadataProvider, TmdbMetadataProvider>();
|
||||
services.AddSingleton<IMetadataProvider, OmdbMetadataProvider>();
|
||||
services.AddSingleton<IMetadataProviderResolver, MetadataProviderResolver>();
|
||||
}
|
||||
|
||||
/// <summary>Планировщик расписания: генератор, источник случайности и фоновый сервис горизонта.</summary>
|
||||
private static void AddBroadcast(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<SchedulerOptions>(
|
||||
configuration.GetSection(SchedulerOptions.SectionName)
|
||||
);
|
||||
services.Configure<StreamingOptions>(
|
||||
configuration.GetSection(StreamingOptions.SectionName)
|
||||
);
|
||||
services.Configure<BumperOptions>(configuration.GetSection(BumperOptions.SectionName));
|
||||
|
||||
services.AddSingleton<IRandomSource, SystemRandomSource>();
|
||||
services.AddSingleton<StreamTokenService>();
|
||||
services.AddSingleton<IBumperRenderer, FfmpegBumperRenderer>();
|
||||
services.AddHostedService<SchedulingBackgroundService>();
|
||||
services.AddHostedService<MaintenanceBackgroundService>();
|
||||
}
|
||||
|
||||
/// <summary>Хранилище медиа, обработка ffmpeg, очередь и фоновые сервисы (очередь + inbox-сканер).</summary>
|
||||
private static void AddMedia(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<StorageOptions>(configuration.GetSection(StorageOptions.SectionName));
|
||||
services.Configure<MediaOptions>(configuration.GetSection(MediaOptions.SectionName));
|
||||
|
||||
services.AddSingleton<MediaPathResolver>();
|
||||
services.AddSingleton<IMediaStorage, FileSystemMediaStorage>();
|
||||
services.AddSingleton<IBumperTemplateStorage, BumperTemplateStorage>();
|
||||
services.AddSingleton<IImageStore, ImageStore>();
|
||||
services.AddSingleton<IImageDownloader, ImageDownloader>();
|
||||
services.AddSingleton<IAudioProbe, FfprobeAudioProbe>();
|
||||
services.AddSingleton<IMediaProcessor, FfmpegMediaProcessor>();
|
||||
services.AddSingleton<IMediaProcessingQueue, MediaProcessingQueue>();
|
||||
services.AddSingleton<IBumperRenderQueue, BumperRenderQueue>();
|
||||
|
||||
services.AddHostedService<MediaProcessingBackgroundService>();
|
||||
services.AddHostedService<InboxScannerBackgroundService>();
|
||||
services.AddHostedService<BumperRenderBackgroundService>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TeleWave.Infrastructure.Library;
|
||||
|
||||
namespace TeleWave.Infrastructure.Identity;
|
||||
|
||||
@@ -10,7 +11,11 @@ public static class DbInitializerExtensions
|
||||
)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
|
||||
var initializer = scope.ServiceProvider.GetRequiredService<DbInitializer>();
|
||||
await initializer.SeedAsync(cancellationToken);
|
||||
|
||||
var genreSeeder = scope.ServiceProvider.GetRequiredService<GenreSeeder>();
|
||||
await genreSeeder.SeedAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Infrastructure.Persistence;
|
||||
|
||||
namespace TeleWave.Infrastructure.Library;
|
||||
|
||||
/// <summary>
|
||||
/// Идемпотентный сидинг справочника жанров. Жанр опознаётся по <see cref="Genre.Slug"/>, поэтому
|
||||
/// переименование администратором сид переживает; недостающие псевдонимы дописываются и к уже
|
||||
/// существующим жанрам — так новые варианты написания приезжают с обновлением приложения.
|
||||
///
|
||||
/// Псевдоним, уже занятый другим жанром, пропускается: уникальность псевдонима — условие
|
||||
/// однозначного сопоставления метаданных, и ручная правка администратора имеет приоритет над сидом.
|
||||
/// </summary>
|
||||
public sealed class GenreSeeder(AppDbContext dbContext, ILogger<GenreSeeder> logger)
|
||||
{
|
||||
private sealed record GenreSeed(string Slug, string Name, string[] Aliases);
|
||||
|
||||
// Порядок массива задаёт SortOrder — так список в UI выглядит осмысленно, а не по алфавиту.
|
||||
// В псевдонимах: идентификаторы TMDb (точное сопоставление) и имена от TMDb/OMDb (запасное).
|
||||
private static readonly GenreSeed[] Seeds =
|
||||
[
|
||||
new("action", "Боевик", ["tmdb:28", "tmdb:10759", "action", "action & adventure"]),
|
||||
new("adventure", "Приключения", ["tmdb:12", "adventure"]),
|
||||
new("animation", "Мультфильм", ["tmdb:16", "animation", "анимация"]),
|
||||
new("comedy", "Комедия", ["tmdb:35", "comedy"]),
|
||||
new("crime", "Криминал", ["tmdb:80", "crime"]),
|
||||
new("drama", "Драма", ["tmdb:18", "drama"]),
|
||||
new("thriller", "Триллер", ["tmdb:53", "thriller"]),
|
||||
new("horror", "Ужасы", ["tmdb:27", "horror"]),
|
||||
new("sci-fi", "Фантастика", ["tmdb:878", "tmdb:10765", "science fiction", "sci-fi"]),
|
||||
new("fantasy", "Фэнтези", ["tmdb:14", "fantasy"]),
|
||||
new("mystery", "Детектив", ["tmdb:9648", "mystery"]),
|
||||
new("romance", "Мелодрама", ["tmdb:10749", "romance", "романтика"]),
|
||||
new("family", "Семейный", ["tmdb:10751", "family"]),
|
||||
new("kids", "Детский", ["tmdb:10762", "kids"]),
|
||||
new("music", "Музыкальный", ["tmdb:10402", "music", "musical"]),
|
||||
new("history", "Исторический", ["tmdb:36", "history"]),
|
||||
new("war", "Военный", ["tmdb:10752", "tmdb:10768", "war", "war & politics"]),
|
||||
new("western", "Вестерн", ["tmdb:37", "western"]),
|
||||
new("documentary", "Документальный", ["tmdb:99", "documentary"]),
|
||||
new("biography", "Биография", ["biography"]),
|
||||
new("sport", "Спорт", ["sport"]),
|
||||
new("news", "Новости", ["tmdb:10763", "news"]),
|
||||
new("reality", "Реалити", ["tmdb:10764", "reality"]),
|
||||
new("talk-show", "Ток-шоу", ["tmdb:10767", "talk", "talk-show"]),
|
||||
new("soap", "Мыльная опера", ["tmdb:10766", "soap"]),
|
||||
new("tv-movie", "Телефильм", ["tmdb:10770", "tv movie"]),
|
||||
new("short", "Короткометражка", ["short"]),
|
||||
];
|
||||
|
||||
public async Task SeedAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var existing = await dbContext
|
||||
.Genres.Include(g => g.Aliases)
|
||||
.ToDictionaryAsync(g => g.Slug, cancellationToken);
|
||||
|
||||
var takenAliases = existing
|
||||
.Values.SelectMany(g => g.Aliases.Select(a => new { a.Value, g.Slug }))
|
||||
.ToDictionary(x => x.Value, x => x.Slug);
|
||||
|
||||
var createdGenres = 0;
|
||||
var createdAliases = 0;
|
||||
|
||||
for (var i = 0; i < Seeds.Length; i++)
|
||||
{
|
||||
var seed = Seeds[i];
|
||||
if (!existing.TryGetValue(seed.Slug, out var genre))
|
||||
{
|
||||
genre = Genre.Create(seed.Name, seed.Slug, i, isSystem: true);
|
||||
dbContext.Genres.Add(genre);
|
||||
existing[seed.Slug] = genre;
|
||||
createdGenres++;
|
||||
}
|
||||
|
||||
// Само название и slug тоже годятся как варианты написания — провайдеры отдают то одно, то другое.
|
||||
foreach (var alias in seed.Aliases.Append(seed.Slug).Append(seed.Name))
|
||||
{
|
||||
var normalized = GenreAlias.Normalize(alias);
|
||||
if (normalized.Length == 0)
|
||||
continue;
|
||||
|
||||
if (takenAliases.TryGetValue(normalized, out var owner))
|
||||
{
|
||||
if (owner != seed.Slug)
|
||||
logger.LogDebug(
|
||||
"Genre alias '{Alias}' already belongs to '{Owner}' — skipped",
|
||||
normalized,
|
||||
owner
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
genre.AddAlias(normalized);
|
||||
takenAliases[normalized] = seed.Slug;
|
||||
createdAliases++;
|
||||
}
|
||||
}
|
||||
|
||||
if (createdGenres == 0 && createdAliases == 0)
|
||||
return;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation(
|
||||
"Seeded genres: {Genres} new, {Aliases} new aliases",
|
||||
createdGenres,
|
||||
createdAliases
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -62,10 +62,19 @@ public sealed class OmdbMetadataProvider(
|
||||
Clean(GetString(root, "Title")) ?? "—",
|
||||
YearFrom(GetString(root, "Year")),
|
||||
Clean(GetString(root, "Plot")),
|
||||
Clean(GetString(root, "Poster"))
|
||||
Clean(GetString(root, "Poster")),
|
||||
GenresFrom(Clean(GetString(root, "Genre")))
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>OMDb отдаёт жанры одной строкой через запятую: «Action, Sci-Fi».</summary>
|
||||
private static List<string> GenresFrom(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value)
|
||||
? []
|
||||
: value
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToList();
|
||||
|
||||
public async Task<EpisodeMetadata?> GetEpisodeAsync(
|
||||
string externalId,
|
||||
int season,
|
||||
|
||||
@@ -66,10 +66,32 @@ public sealed class TmdbMetadataProvider(
|
||||
GetString(root, "name") ?? "—",
|
||||
YearFrom(GetString(root, "first_air_date")),
|
||||
GetString(root, "overview"),
|
||||
PosterUrl(GetString(root, "poster_path"))
|
||||
PosterUrl(GetString(root, "poster_path")),
|
||||
GenresFrom(root)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Жанры из ответа TMDb в порядке источника: на каждый — сначала идентификатор
|
||||
/// (<c>tmdb:28</c>), затем локализованное название. Идентификатор точнее, название — запасной
|
||||
/// вариант, если справочник его ещё не знает.
|
||||
/// </summary>
|
||||
private static List<string> GenresFrom(JsonElement root)
|
||||
{
|
||||
var genres = new List<string>();
|
||||
if (!root.TryGetProperty("genres", out var array) || array.ValueKind != JsonValueKind.Array)
|
||||
return genres;
|
||||
|
||||
foreach (var item in array.EnumerateArray())
|
||||
{
|
||||
if (GetInt(item, "id") is { } id)
|
||||
genres.Add($"tmdb:{id.ToString(CultureInfo.InvariantCulture)}");
|
||||
if (GetString(item, "name") is { Length: > 0 } name)
|
||||
genres.Add(name);
|
||||
}
|
||||
return genres;
|
||||
}
|
||||
|
||||
public async Task<EpisodeMetadata?> GetEpisodeAsync(
|
||||
string externalId,
|
||||
int season,
|
||||
|
||||
+1130
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeleWave.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGenres : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Genres",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Slug = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
IsSystem = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Genres", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GenreAliases",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
GenreId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Value = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GenreAliases", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_GenreAliases_Genres_GenreId",
|
||||
column: x => x.GenreId,
|
||||
principalTable: "Genres",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ShowGenres",
|
||||
columns: table => new
|
||||
{
|
||||
ShowId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
GenreId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
IsPrimary = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ShowGenres", x => new { x.ShowId, x.GenreId });
|
||||
table.ForeignKey(
|
||||
name: "FK_ShowGenres_Genres_GenreId",
|
||||
column: x => x.GenreId,
|
||||
principalTable: "Genres",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ShowGenres_Shows_ShowId",
|
||||
column: x => x.ShowId,
|
||||
principalTable: "Shows",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GenreAliases_GenreId",
|
||||
table: "GenreAliases",
|
||||
column: "GenreId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GenreAliases_Value",
|
||||
table: "GenreAliases",
|
||||
column: "Value",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Genres_Slug",
|
||||
table: "Genres",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ShowGenres_GenreId",
|
||||
table: "ShowGenres",
|
||||
column: "GenreId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "GenreAliases");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ShowGenres");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Genres");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1130
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeleWave.Infrastructure.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Переход возрастной категории на шкалу, упорядоченную по возрастанию строгости:
|
||||
/// Kids(0) → Family(1) → Teen(2) → General(3) → Adult(4). Прежние значения (General=0, Kids=1,
|
||||
/// Adult=2) отсортированы не были, а правила планировщика формулируются как «не строже X»,
|
||||
/// поэтому порядок стал значимым.
|
||||
/// </summary>
|
||||
public partial class ShowAudienceScale : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Одним CASE, а не серией UPDATE: последовательные обновления перепутали бы категории
|
||||
// между собой (сначала General 0 → 3, а следом это же значение попало бы под правило
|
||||
// для старого Adult).
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE "Shows" SET "Audience" = CASE "Audience"
|
||||
WHEN 0 THEN 3
|
||||
WHEN 1 THEN 0
|
||||
WHEN 2 THEN 4
|
||||
ELSE "Audience"
|
||||
END;
|
||||
"""
|
||||
);
|
||||
|
||||
// Дефолт остался от первого добавления столбца и теперь означал бы «детское».
|
||||
// В модели значения по умолчанию нет — снимаем и в схеме.
|
||||
migrationBuilder.Sql("""ALTER TABLE "Shows" ALTER COLUMN "Audience" DROP DEFAULT;""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Family и Teen в старой шкале не существуют — схлопываются в General.
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE "Shows" SET "Audience" = CASE "Audience"
|
||||
WHEN 0 THEN 1
|
||||
WHEN 4 THEN 2
|
||||
ELSE 0
|
||||
END;
|
||||
"""
|
||||
);
|
||||
|
||||
migrationBuilder.Sql("""ALTER TABLE "Shows" ALTER COLUMN "Audience" SET DEFAULT 0;""");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1201
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeleWave.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCollections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Collections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
PosterImageId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Collections", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CollectionItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CollectionId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ShowId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CollectionItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CollectionItems_Collections_CollectionId",
|
||||
column: x => x.CollectionId,
|
||||
principalTable: "Collections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_CollectionItems_Shows_ShowId",
|
||||
column: x => x.ShowId,
|
||||
principalTable: "Shows",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CollectionItems_CollectionId_Position",
|
||||
table: "CollectionItems",
|
||||
columns: new[] { "CollectionId", "Position" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CollectionItems_CollectionId_ShowId",
|
||||
table: "CollectionItems",
|
||||
columns: new[] { "CollectionId", "ShowId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CollectionItems_ShowId",
|
||||
table: "CollectionItems",
|
||||
column: "ShowId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CollectionItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Collections");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1284
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeleWave.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGroups : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Groups",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
FilterJson = table.Column<string>(type: "jsonb", nullable: true),
|
||||
ItemCount = table.Column<int>(type: "integer", nullable: false),
|
||||
UnitCount = table.Column<int>(type: "integer", nullable: false),
|
||||
TotalDuration = table.Column<TimeSpan>(type: "interval", nullable: false),
|
||||
StatsComputedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Groups", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GroupItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
GroupId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ElementKind = table.Column<int>(type: "integer", nullable: false),
|
||||
ElementId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Weight = table.Column<int>(type: "integer", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GroupItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_GroupItems_Groups_GroupId",
|
||||
column: x => x.GroupId,
|
||||
principalTable: "Groups",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GroupItems_ElementKind_ElementId",
|
||||
table: "GroupItems",
|
||||
columns: new[] { "ElementKind", "ElementId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GroupItems_GroupId_ElementKind_ElementId",
|
||||
table: "GroupItems",
|
||||
columns: new[] { "GroupId", "ElementKind", "ElementId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GroupItems_GroupId_Position",
|
||||
table: "GroupItems",
|
||||
columns: new[] { "GroupId", "Position" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "GroupItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Groups");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1489
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeleWave.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddScheduleTemplate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<TimeOnly>(
|
||||
name: "DayStartTime",
|
||||
table: "Channels",
|
||||
type: "time without time zone",
|
||||
nullable: false,
|
||||
defaultValue: new TimeOnly(0, 0, 0));
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Number",
|
||||
table: "Channels",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "TemplateId",
|
||||
table: "Channels",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "UtcOffsetMinutes",
|
||||
table: "Channels",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ScheduleTemplates",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
FallbackGroupId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Revision = table.Column<int>(type: "integer", nullable: false),
|
||||
AppliedRevision = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ScheduleTemplates", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "GridLayers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TemplateId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
Priority = table.Column<int>(type: "integer", nullable: false),
|
||||
ApplicabilityJson = table.Column<string>(type: "jsonb", nullable: true),
|
||||
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsBackground = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_GridLayers", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_GridLayers_ScheduleTemplates_TemplateId",
|
||||
column: x => x.TemplateId,
|
||||
principalTable: "ScheduleTemplates",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Slots",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
LayerId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Weekday = table.Column<int>(type: "integer", nullable: true),
|
||||
TargetStart = table.Column<TimeOnly>(type: "time without time zone", nullable: false),
|
||||
TargetDurationMinutes = table.Column<int>(type: "integer", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Daypart = table.Column<int>(type: "integer", nullable: false),
|
||||
SlotKind = table.Column<int>(type: "integer", nullable: false),
|
||||
GroupId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
StrategyJson = table.Column<string>(type: "jsonb", nullable: true),
|
||||
RepeatSourceJson = table.Column<string>(type: "jsonb", nullable: true),
|
||||
BlockMode = table.Column<int>(type: "integer", nullable: false),
|
||||
BlockValue = table.Column<int>(type: "integer", nullable: false),
|
||||
OverflowPolicy = table.Column<int>(type: "integer", nullable: false),
|
||||
IsAnchor = table.Column<bool>(type: "boolean", nullable: false),
|
||||
MaxDriftMinutes = table.Column<int>(type: "integer", nullable: false),
|
||||
SnapToMinutes = table.Column<int>(type: "integer", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Slots", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Slots_GridLayers_LayerId",
|
||||
column: x => x.LayerId,
|
||||
principalTable: "GridLayers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Slots_Groups_GroupId",
|
||||
column: x => x.GroupId,
|
||||
principalTable: "Groups",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SlotStates",
|
||||
columns: table => new
|
||||
{
|
||||
SlotId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CurrentElementKind = table.Column<int>(type: "integer", nullable: true),
|
||||
CurrentElementId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
NextUnitIndex = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SlotStates", x => x.SlotId);
|
||||
table.ForeignKey(
|
||||
name: "FK_SlotStates_Slots_SlotId",
|
||||
column: x => x.SlotId,
|
||||
principalTable: "Slots",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Channels_Number",
|
||||
table: "Channels",
|
||||
column: "Number",
|
||||
unique: true,
|
||||
filter: "\"Number\" IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GridLayers_TemplateId_Priority",
|
||||
table: "GridLayers",
|
||||
columns: new[] { "TemplateId", "Priority" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ScheduleTemplates_ChannelId",
|
||||
table: "ScheduleTemplates",
|
||||
column: "ChannelId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Slots_GroupId",
|
||||
table: "Slots",
|
||||
column: "GroupId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Slots_LayerId_TargetStart",
|
||||
table: "Slots",
|
||||
columns: new[] { "LayerId", "TargetStart" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SlotStates");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Slots");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "GridLayers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ScheduleTemplates");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Channels_Number",
|
||||
table: "Channels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DayStartTime",
|
||||
table: "Channels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Number",
|
||||
table: "Channels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TemplateId",
|
||||
table: "Channels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UtcOffsetMinutes",
|
||||
table: "Channels");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1495
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeleWave.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ScheduleEntryTrace : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ScheduleEntries_ChannelId_ShowId",
|
||||
table: "ScheduleEntries");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "SlotId",
|
||||
table: "ScheduleEntries",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TraceJson",
|
||||
table: "ScheduleEntries",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ScheduleEntries_ChannelId_ShowId_StartsAtUtc",
|
||||
table: "ScheduleEntries",
|
||||
columns: new[] { "ChannelId", "ShowId", "StartsAtUtc" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ScheduleEntries_ChannelId_ShowId_StartsAtUtc",
|
||||
table: "ScheduleEntries");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SlotId",
|
||||
table: "ScheduleEntries");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TraceJson",
|
||||
table: "ScheduleEntries");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ScheduleEntries_ChannelId_ShowId",
|
||||
table: "ScheduleEntries",
|
||||
columns: new[] { "ChannelId", "ShowId" });
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1288
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeleWave.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class DropLegacyRotation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChannelAd");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChannelShowHour");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OverrideShow");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ChannelShow");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ProgrammingOverride");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AdInsertion",
|
||||
table: "Channels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AdsPerBreak",
|
||||
table: "Channels");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NextAdIndex",
|
||||
table: "Channels");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AdInsertion",
|
||||
table: "Channels",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AdsPerBreak",
|
||||
table: "Channels",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "NextAdIndex",
|
||||
table: "Channels",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChannelAd",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
MediaAssetId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChannelAd", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChannelAd_Channels_ChannelId",
|
||||
column: x => x.ChannelId,
|
||||
principalTable: "Channels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChannelShow",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
BlockMode = table.Column<int>(type: "integer", nullable: false),
|
||||
BlockValue = table.Column<int>(type: "integer", nullable: false),
|
||||
ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
NextEpisodeIndex = table.Column<int>(type: "integer", nullable: false),
|
||||
PreferredWeightMultiplier = table.Column<int>(type: "integer", nullable: false, defaultValue: 3),
|
||||
ShowId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Weight = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChannelShow", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChannelShow_Channels_ChannelId",
|
||||
column: x => x.ChannelId,
|
||||
principalTable: "Channels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ProgrammingOverride",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DayOfWeek = table.Column<int>(type: "integer", nullable: true),
|
||||
EndMinute = table.Column<int>(type: "integer", nullable: true),
|
||||
EndsAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
Mode = table.Column<int>(type: "integer", nullable: false),
|
||||
Recurrence = table.Column<int>(type: "integer", nullable: false),
|
||||
StartMinute = table.Column<int>(type: "integer", nullable: true),
|
||||
StartsAtUtc = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ProgrammingOverride", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ProgrammingOverride_Channels_ChannelId",
|
||||
column: x => x.ChannelId,
|
||||
principalTable: "Channels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ChannelShowHour",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ChannelShowId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
EndHour = table.Column<int>(type: "integer", nullable: false),
|
||||
StartHour = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ChannelShowHour", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ChannelShowHour_ChannelShow_ChannelShowId",
|
||||
column: x => x.ChannelShowId,
|
||||
principalTable: "ChannelShow",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OverrideShow",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ProgrammingOverrideId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ShowId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Weight = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OverrideShow", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OverrideShow_ProgrammingOverride_ProgrammingOverrideId",
|
||||
column: x => x.ProgrammingOverrideId,
|
||||
principalTable: "ProgrammingOverride",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChannelAd_ChannelId_Position",
|
||||
table: "ChannelAd",
|
||||
columns: new[] { "ChannelId", "Position" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChannelShow_ChannelId_ShowId",
|
||||
table: "ChannelShow",
|
||||
columns: new[] { "ChannelId", "ShowId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ChannelShowHour_ChannelShowId",
|
||||
table: "ChannelShowHour",
|
||||
column: "ChannelShowId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OverrideShow_ProgrammingOverrideId",
|
||||
table: "OverrideShow",
|
||||
column: "ProgrammingOverrideId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ProgrammingOverride_ChannelId_StartsAtUtc_EndsAtUtc",
|
||||
table: "ProgrammingOverride",
|
||||
columns: new[] { "ChannelId", "StartsAtUtc", "EndsAtUtc" });
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1380
File diff suppressed because it is too large
Load Diff
+116
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TeleWave.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddJunctionTemplates : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "JunctionAfterId",
|
||||
table: "Slots",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "JunctionBetweenId",
|
||||
table: "Slots",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "DefaultJunctionId",
|
||||
table: "ScheduleTemplates",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "JunctionTemplates",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ChannelId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_JunctionTemplates", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "JunctionElements",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
JunctionTemplateId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Position = table.Column<int>(type: "integer", nullable: false),
|
||||
Kind = table.Column<int>(type: "integer", nullable: false),
|
||||
GroupId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
BumperTemplateId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
AmountMode = table.Column<int>(type: "integer", nullable: false),
|
||||
AmountValue = table.Column<int>(type: "integer", nullable: false),
|
||||
IsRequired = table.Column<bool>(type: "boolean", nullable: false),
|
||||
ConditionsJson = table.Column<string>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_JunctionElements", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_JunctionElements_Groups_GroupId",
|
||||
column: x => x.GroupId,
|
||||
principalTable: "Groups",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_JunctionElements_JunctionTemplates_JunctionTemplateId",
|
||||
column: x => x.JunctionTemplateId,
|
||||
principalTable: "JunctionTemplates",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JunctionElements_GroupId",
|
||||
table: "JunctionElements",
|
||||
column: "GroupId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JunctionElements_JunctionTemplateId_Position",
|
||||
table: "JunctionElements",
|
||||
columns: new[] { "JunctionTemplateId", "Position" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JunctionTemplates_ChannelId",
|
||||
table: "JunctionTemplates",
|
||||
column: "ChannelId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "JunctionElements");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "JunctionTemplates");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "JunctionAfterId",
|
||||
table: "Slots");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "JunctionBetweenId",
|
||||
table: "Slots");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultJunctionId",
|
||||
table: "ScheduleTemplates");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,12 +318,6 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AdInsertion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("AdsPerBreak")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<double>("BumperEpisodeChangeChance")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
@@ -345,6 +339,9 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<TimeOnly>("DayStartTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("EpochUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -359,10 +356,10 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int>("NextAdIndex")
|
||||
b.Property<int>("NextBumperIndex")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("NextBumperIndex")
|
||||
b.Property<int?>("Number")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
@@ -370,151 +367,24 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid?>("TemplateId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("UtcOffsetMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Number")
|
||||
.IsUnique()
|
||||
.HasFilter("\"Number\" IS NOT NULL");
|
||||
|
||||
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<int>("PreferredWeightMultiplier")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(3);
|
||||
|
||||
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.ChannelShowHour", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChannelShowId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("EndHour")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("StartHour")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelShowId");
|
||||
|
||||
b.ToTable("ChannelShowHour");
|
||||
});
|
||||
|
||||
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<int?>("DayOfWeek")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("EndMinute")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndsAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Mode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Recurrence")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("StartMinute")
|
||||
.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")
|
||||
@@ -541,17 +411,23 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
b.Property<Guid?>("ShowId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("SlotId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("StartsAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("TraceJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId", "EndsAtUtc");
|
||||
|
||||
b.HasIndex("ChannelId", "ShowId");
|
||||
|
||||
b.HasIndex("ChannelId", "StartsAtUtc");
|
||||
|
||||
b.HasIndex("ChannelId", "ShowId", "StartsAtUtc");
|
||||
|
||||
b.ToTable("ScheduleEntries");
|
||||
});
|
||||
|
||||
@@ -582,6 +458,112 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
b.ToTable("Images");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.Collection", 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<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid?>("PosterImageId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Collections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.CollectionItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CollectionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("ShowId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShowId");
|
||||
|
||||
b.HasIndex("CollectionId", "Position");
|
||||
|
||||
b.HasIndex("CollectionId", "ShowId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CollectionItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.Genre", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Genres");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.GenreAlias", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("GenreId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GenreId");
|
||||
|
||||
b.HasIndex("Value")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("GenreAliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.Show", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -674,6 +656,24 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
b.ToTable("ShowEpisode");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.ShowGenre", b =>
|
||||
{
|
||||
b.Property<Guid>("ShowId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("GenreId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("ShowId", "GenreId");
|
||||
|
||||
b.HasIndex("GenreId");
|
||||
|
||||
b.ToTable("ShowGenres");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Media.MediaAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -747,6 +747,295 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
b.ToTable("MediaAssets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.GridLayer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ApplicabilityJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<bool>("IsBackground")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("TemplateId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId", "Priority");
|
||||
|
||||
b.ToTable("GridLayers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.Group", 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<string>("FilterJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("ItemCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<DateTimeOffset?>("StatsComputedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<TimeSpan>("TotalDuration")
|
||||
.HasColumnType("interval");
|
||||
|
||||
b.Property<int>("UnitCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Groups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.GroupItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ElementId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("ElementKind")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("GroupId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Weight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ElementKind", "ElementId");
|
||||
|
||||
b.HasIndex("GroupId", "Position");
|
||||
|
||||
b.HasIndex("GroupId", "ElementKind", "ElementId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("GroupItems");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.JunctionElement", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AmountMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("AmountValue")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("BumperTemplateId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ConditionsJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<Guid?>("GroupId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsRequired")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("JunctionTemplateId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Position")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("JunctionTemplateId", "Position");
|
||||
|
||||
b.ToTable("JunctionElements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.JunctionTemplate", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ChannelId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.ToTable("JunctionTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.ScheduleTemplate", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AppliedRevision")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("ChannelId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("DefaultJunctionId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("FallbackGroupId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int>("Revision")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
b.ToTable("ScheduleTemplates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.Slot", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BlockMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BlockValue")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Daypart")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("GroupId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsAnchor")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid?>("JunctionAfterId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("JunctionBetweenId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("LayerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("MaxDriftMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("OverflowPolicy")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("RepeatSourceJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("SlotKind")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("SnapToMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("StrategyJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("TargetDurationMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<TimeOnly>("TargetStart")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<int?>("Weekday")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GroupId");
|
||||
|
||||
b.HasIndex("LayerId", "TargetStart");
|
||||
|
||||
b.ToTable("Slots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.SlotState", b =>
|
||||
{
|
||||
b.Property<Guid>("SlotId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CurrentElementId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int?>("CurrentElementKind")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("NextUnitIndex")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("SlotId");
|
||||
|
||||
b.ToTable("SlotStates");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Settings.AppSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
@@ -933,47 +1222,26 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelAd", b =>
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.CollectionItem", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||
.WithMany("Ads")
|
||||
.HasForeignKey("ChannelId")
|
||||
b.HasOne("TeleWave.Domain.Library.Collection", null)
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("CollectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TeleWave.Domain.Library.Show", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ShowId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelShow", b =>
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.GenreAlias", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Broadcast.Channel", null)
|
||||
.WithMany("Shows")
|
||||
.HasForeignKey("ChannelId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelShowHour", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Broadcast.ChannelShow", null)
|
||||
.WithMany("PreferredHours")
|
||||
.HasForeignKey("ChannelShowId")
|
||||
.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")
|
||||
b.HasOne("TeleWave.Domain.Library.Genre", null)
|
||||
.WithMany("Aliases")
|
||||
.HasForeignKey("GenreId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
@@ -987,6 +1255,76 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.ShowGenre", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Library.Genre", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("GenreId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("TeleWave.Domain.Library.Show", null)
|
||||
.WithMany("Genres")
|
||||
.HasForeignKey("ShowId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.GridLayer", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Programming.ScheduleTemplate", null)
|
||||
.WithMany("Layers")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.GroupItem", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Programming.Group", null)
|
||||
.WithMany("Items")
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.JunctionElement", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Programming.Group", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("TeleWave.Domain.Programming.JunctionTemplate", null)
|
||||
.WithMany("Elements")
|
||||
.HasForeignKey("JunctionTemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.Slot", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Programming.Group", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("GroupId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("TeleWave.Domain.Programming.GridLayer", null)
|
||||
.WithMany("Slots")
|
||||
.HasForeignKey("LayerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.SlotState", b =>
|
||||
{
|
||||
b.HasOne("TeleWave.Domain.Programming.Slot", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("TeleWave.Domain.Programming.SlotState", "SlotId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTemplate", b =>
|
||||
{
|
||||
b.Navigation("Variants");
|
||||
@@ -994,28 +1332,44 @@ namespace TeleWave.Infrastructure.Migrations
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
|
||||
{
|
||||
b.Navigation("Ads");
|
||||
|
||||
b.Navigation("BumperTemplates");
|
||||
|
||||
b.Navigation("Overrides");
|
||||
|
||||
b.Navigation("Shows");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.ChannelShow", b =>
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.Collection", b =>
|
||||
{
|
||||
b.Navigation("PreferredHours");
|
||||
b.Navigation("Items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Broadcast.ProgrammingOverride", b =>
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.Genre", b =>
|
||||
{
|
||||
b.Navigation("Shows");
|
||||
b.Navigation("Aliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Library.Show", b =>
|
||||
{
|
||||
b.Navigation("Episodes");
|
||||
|
||||
b.Navigation("Genres");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.GridLayer", b =>
|
||||
{
|
||||
b.Navigation("Slots");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.Group", b =>
|
||||
{
|
||||
b.Navigation("Items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.JunctionTemplate", b =>
|
||||
{
|
||||
b.Navigation("Elements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("TeleWave.Domain.Programming.ScheduleTemplate", b =>
|
||||
{
|
||||
b.Navigation("Layers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
||||
@@ -1,69 +1,83 @@
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Domain.Auth;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Images;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Domain.Settings;
|
||||
using TeleWave.Infrastructure.Identity;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Корневой DbContext приложения: Identity-схема (пользователи/роли) + сущности домена
|
||||
/// (добавляются по мере реализации фич).
|
||||
/// </summary>
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
: IdentityDbContext<AppUser, AppRole, Guid>(options),
|
||||
IAppDbContext
|
||||
{
|
||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||
public DbSet<MediaAsset> MediaAssets => Set<MediaAsset>();
|
||||
public DbSet<Show> Shows => Set<Show>();
|
||||
public DbSet<Channel> Channels => Set<Channel>();
|
||||
public DbSet<ScheduleEntry> ScheduleEntries => Set<ScheduleEntry>();
|
||||
public DbSet<BumperTextVariant> BumperTextVariants => Set<BumperTextVariant>();
|
||||
public DbSet<BumperAsset> BumperAssets => Set<BumperAsset>();
|
||||
public DbSet<AppSetting> AppSettings => Set<AppSetting>();
|
||||
public DbSet<Image> Images => Set<Image>();
|
||||
|
||||
public Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken) =>
|
||||
Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
public Task AcquireChannelLockAsync(Guid channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
// pg_advisory_xact_lock(bigint) освобождается автоматически при завершении транзакции.
|
||||
// Ключ — стабильный int64 из GUID канала; коллизии между каналами лишь сериализуют их генерацию,
|
||||
// корректности не нарушают.
|
||||
var key = BitConverter.ToInt64(channelId.ToByteArray());
|
||||
return Database.ExecuteSqlAsync($"SELECT pg_advisory_xact_lock({key})", cancellationToken);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||
|
||||
// Guid-ключи доменных сущностей мы задаём сами в фабриках. Без этого EF считает выставленный
|
||||
// ключ признаком уже существующей строки и при добавлении дочерней сущности через коллекцию
|
||||
// отслеживаемого родителя (напр. show.AddEpisode) делает UPDATE вместо INSERT → «affected 0».
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (
|
||||
entityType.ClrType.Namespace?.StartsWith(
|
||||
"TeleWave.Domain",
|
||||
StringComparison.Ordinal
|
||||
) != true
|
||||
)
|
||||
continue;
|
||||
|
||||
var idProperty = entityType.FindProperty("Id");
|
||||
if (idProperty is not null && idProperty.ClrType == typeof(Guid))
|
||||
idProperty.ValueGenerated = ValueGenerated.Never;
|
||||
}
|
||||
}
|
||||
}
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Domain.Auth;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Images;
|
||||
using TeleWave.Domain.Library;
|
||||
using TeleWave.Domain.Media;
|
||||
using TeleWave.Domain.Programming;
|
||||
using TeleWave.Domain.Settings;
|
||||
using TeleWave.Infrastructure.Identity;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Корневой DbContext приложения: Identity-схема (пользователи/роли) + сущности домена
|
||||
/// (добавляются по мере реализации фич).
|
||||
/// </summary>
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
: IdentityDbContext<AppUser, AppRole, Guid>(options),
|
||||
IAppDbContext
|
||||
{
|
||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||
public DbSet<MediaAsset> MediaAssets => Set<MediaAsset>();
|
||||
public DbSet<Show> Shows => Set<Show>();
|
||||
public DbSet<Genre> Genres => Set<Genre>();
|
||||
public DbSet<GenreAlias> GenreAliases => Set<GenreAlias>();
|
||||
public DbSet<ShowGenre> ShowGenres => Set<ShowGenre>();
|
||||
public DbSet<Collection> Collections => Set<Collection>();
|
||||
public DbSet<CollectionItem> CollectionItems => Set<CollectionItem>();
|
||||
public DbSet<Group> Groups => Set<Group>();
|
||||
public DbSet<GroupItem> GroupItems => Set<GroupItem>();
|
||||
public DbSet<ScheduleTemplate> ScheduleTemplates => Set<ScheduleTemplate>();
|
||||
public DbSet<GridLayer> GridLayers => Set<GridLayer>();
|
||||
public DbSet<Slot> Slots => Set<Slot>();
|
||||
public DbSet<SlotState> SlotStates => Set<SlotState>();
|
||||
public DbSet<JunctionTemplate> JunctionTemplates => Set<JunctionTemplate>();
|
||||
public DbSet<JunctionElement> JunctionElements => Set<JunctionElement>();
|
||||
public DbSet<Channel> Channels => Set<Channel>();
|
||||
public DbSet<ScheduleEntry> ScheduleEntries => Set<ScheduleEntry>();
|
||||
public DbSet<BumperTextVariant> BumperTextVariants => Set<BumperTextVariant>();
|
||||
public DbSet<BumperAsset> BumperAssets => Set<BumperAsset>();
|
||||
public DbSet<AppSetting> AppSettings => Set<AppSetting>();
|
||||
public DbSet<Image> Images => Set<Image>();
|
||||
|
||||
public Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken) =>
|
||||
Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
public Task AcquireChannelLockAsync(Guid channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
// pg_advisory_xact_lock(bigint) освобождается автоматически при завершении транзакции.
|
||||
// Ключ — стабильный int64 из GUID канала; коллизии между каналами лишь сериализуют их генерацию,
|
||||
// корректности не нарушают.
|
||||
var key = BitConverter.ToInt64(channelId.ToByteArray());
|
||||
return Database.ExecuteSqlAsync($"SELECT pg_advisory_xact_lock({key})", cancellationToken);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||
|
||||
// Guid-ключи доменных сущностей мы задаём сами в фабриках. Без этого EF считает выставленный
|
||||
// ключ признаком уже существующей строки и при добавлении дочерней сущности через коллекцию
|
||||
// отслеживаемого родителя (напр. show.AddEpisode) делает UPDATE вместо INSERT → «affected 0».
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (
|
||||
entityType.ClrType.Namespace?.StartsWith(
|
||||
"TeleWave.Domain",
|
||||
StringComparison.Ordinal
|
||||
) != true
|
||||
)
|
||||
continue;
|
||||
|
||||
var idProperty = entityType.FindProperty("Id");
|
||||
if (idProperty is not null && idProperty.ClrType == typeof(Guid))
|
||||
idProperty.ValueGenerated = ValueGenerated.Never;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-133
@@ -1,133 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class ChannelConfiguration : IEntityTypeConfiguration<Channel>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Channel> builder)
|
||||
{
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(256);
|
||||
builder.Property(x => x.Slug).IsRequired().HasMaxLength(128);
|
||||
builder.HasIndex(x => x.Slug).IsUnique();
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Shows)
|
||||
.WithOne()
|
||||
.HasForeignKey(s => s.ChannelId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Shows).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Ads)
|
||||
.WithOne()
|
||||
.HasForeignKey(a => a.ChannelId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Ads).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.BumperTemplates)
|
||||
.WithOne()
|
||||
.HasForeignKey(t => t.ChannelId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.BumperTemplates).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Overrides)
|
||||
.WithOne()
|
||||
.HasForeignKey(o => o.ChannelId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Overrides).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChannelShowConfiguration : IEntityTypeConfiguration<ChannelShow>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ChannelShow> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.ChannelId, x.ShowId });
|
||||
builder
|
||||
.Property(x => x.PreferredWeightMultiplier)
|
||||
.HasDefaultValue(ChannelShow.DefaultPreferredWeightMultiplier);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.PreferredHours)
|
||||
.WithOne()
|
||||
.HasForeignKey(h => h.ChannelShowId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.PreferredHours).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChannelShowHourConfiguration : IEntityTypeConfiguration<ChannelShowHour>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ChannelShowHour> builder)
|
||||
{
|
||||
builder.HasIndex(x => x.ChannelShowId);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChannelAdConfiguration : IEntityTypeConfiguration<ChannelAd>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ChannelAd> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.ChannelId, x.Position });
|
||||
}
|
||||
}
|
||||
|
||||
public class BumperTemplateConfiguration : IEntityTypeConfiguration<BumperTemplate>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BumperTemplate> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.ChannelId, x.Position });
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(64);
|
||||
builder.Property(x => x.BackgroundColor).IsRequired().HasMaxLength(32);
|
||||
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.AudioExtension).HasMaxLength(16);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Variants)
|
||||
.WithOne()
|
||||
.HasForeignKey(v => v.BumperTemplateId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Variants).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class BumperTextVariantConfiguration : IEntityTypeConfiguration<BumperTextVariant>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BumperTextVariant> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.BumperTemplateId, x.Position });
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(64);
|
||||
builder.Property(x => x.NowLabel).IsRequired().HasMaxLength(64);
|
||||
builder.Property(x => x.NextLabel).IsRequired().HasMaxLength(64);
|
||||
builder.Property(x => x.Line1).IsRequired().HasMaxLength(120);
|
||||
builder.Property(x => x.Line2).IsRequired().HasMaxLength(120);
|
||||
builder.Property(x => x.Weight).HasDefaultValue(BumperTextVariant.DefaultWeight);
|
||||
}
|
||||
}
|
||||
|
||||
public class ProgrammingOverrideConfiguration : IEntityTypeConfiguration<ProgrammingOverride>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ProgrammingOverride> builder)
|
||||
{
|
||||
builder.HasIndex(x => new
|
||||
{
|
||||
x.ChannelId,
|
||||
x.StartsAtUtc,
|
||||
x.EndsAtUtc,
|
||||
});
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Shows)
|
||||
.WithOne()
|
||||
.HasForeignKey(s => s.ProgrammingOverrideId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Shows).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class ChannelConfiguration : IEntityTypeConfiguration<Channel>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Channel> builder)
|
||||
{
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(256);
|
||||
builder.Property(x => x.Slug).IsRequired().HasMaxLength(128);
|
||||
builder.HasIndex(x => x.Slug).IsUnique();
|
||||
|
||||
// Номер канала уникален среди заданных: переключение вверх-вниз по номерам иначе неоднозначно.
|
||||
builder.HasIndex(x => x.Number).IsUnique().HasFilter("\"Number\" IS NOT NULL");
|
||||
|
||||
builder
|
||||
.HasMany(x => x.BumperTemplates)
|
||||
.WithOne()
|
||||
.HasForeignKey(t => t.ChannelId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.BumperTemplates).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class BumperTemplateConfiguration : IEntityTypeConfiguration<BumperTemplate>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BumperTemplate> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.ChannelId, x.Position });
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(64);
|
||||
builder.Property(x => x.BackgroundColor).IsRequired().HasMaxLength(32);
|
||||
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.AudioExtension).HasMaxLength(16);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Variants)
|
||||
.WithOne()
|
||||
.HasForeignKey(v => v.BumperTemplateId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Variants).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class BumperTextVariantConfiguration : IEntityTypeConfiguration<BumperTextVariant>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BumperTextVariant> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.BumperTemplateId, x.Position });
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(64);
|
||||
builder.Property(x => x.NowLabel).IsRequired().HasMaxLength(64);
|
||||
builder.Property(x => x.NextLabel).IsRequired().HasMaxLength(64);
|
||||
builder.Property(x => x.Line1).IsRequired().HasMaxLength(120);
|
||||
builder.Property(x => x.Line2).IsRequired().HasMaxLength(120);
|
||||
builder.Property(x => x.Weight).HasDefaultValue(BumperTextVariant.DefaultWeight);
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class CollectionConfiguration : IEntityTypeConfiguration<Collection>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Collection> builder)
|
||||
{
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(256);
|
||||
builder.Property(x => x.Description).HasMaxLength(2048);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Items)
|
||||
.WithOne()
|
||||
.HasForeignKey(i => i.CollectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Items).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class CollectionItemConfiguration : IEntityTypeConfiguration<CollectionItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CollectionItem> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.CollectionId, x.Position });
|
||||
// Шоу входит в коллекцию не более одного раза — иначе порядок воспроизведения неоднозначен.
|
||||
builder.HasIndex(x => new { x.CollectionId, x.ShowId }).IsUnique();
|
||||
builder.HasIndex(x => x.ShowId);
|
||||
|
||||
// Удаление шоу из библиотеки убирает его и из коллекций: висячая позиция сломала бы
|
||||
// разворачивание коллекции в последовательность серий при генерации.
|
||||
builder
|
||||
.HasOne<Show>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.ShowId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using TeleWave.Domain.Library;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class GenreConfiguration : IEntityTypeConfiguration<Genre>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Genre> builder)
|
||||
{
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
builder.Property(x => x.Slug).IsRequired().HasMaxLength(64);
|
||||
builder.HasIndex(x => x.Slug).IsUnique();
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Aliases)
|
||||
.WithOne()
|
||||
.HasForeignKey(a => a.GenreId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Aliases).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class GenreAliasConfiguration : IEntityTypeConfiguration<GenreAlias>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<GenreAlias> builder)
|
||||
{
|
||||
builder.Property(x => x.Value).IsRequired().HasMaxLength(128);
|
||||
// Псевдоним однозначно указывает на жанр: иначе сопоставление метаданных стало бы неопределённым.
|
||||
builder.HasIndex(x => x.Value).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public class ShowGenreConfiguration : IEntityTypeConfiguration<ShowGenre>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ShowGenre> builder)
|
||||
{
|
||||
builder.HasKey(x => new { x.ShowId, x.GenreId });
|
||||
builder.HasIndex(x => x.GenreId);
|
||||
|
||||
// Restrict, а не Cascade: удаление жанра из справочника не должно молча снимать его со всех шоу.
|
||||
// Команда удаления проверяет использование заранее и возвращает управляемый конфликт.
|
||||
builder
|
||||
.HasOne<Genre>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.GenreId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using TeleWave.Domain.Programming;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class GroupConfiguration : IEntityTypeConfiguration<Group>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Group> builder)
|
||||
{
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(256);
|
||||
builder.Property(x => x.Description).HasMaxLength(2048);
|
||||
|
||||
// Правило набора хранится как jsonb: схему знает Application, а домен передаёт строку как есть.
|
||||
builder.Property(x => x.FilterJson).HasColumnType("jsonb");
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Items)
|
||||
.WithOne()
|
||||
.HasForeignKey(i => i.GroupId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Items).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupItemConfiguration : IEntityTypeConfiguration<GroupItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<GroupItem> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.GroupId, x.Position });
|
||||
// Элемент входит в группу не более одного раза — иначе вес и порядок становятся неоднозначны.
|
||||
builder.HasIndex(x => new
|
||||
{
|
||||
x.GroupId,
|
||||
x.ElementKind,
|
||||
x.ElementId,
|
||||
}).IsUnique();
|
||||
|
||||
// По этому индексу чистятся позиции при удалении шоу/коллекции: внешнего ключа на
|
||||
// полиморфную ссылку нет, удаление идёт командой.
|
||||
builder.HasIndex(x => new { x.ElementKind, x.ElementId });
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using TeleWave.Domain.Programming;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class JunctionTemplateConfiguration : IEntityTypeConfiguration<JunctionTemplate>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<JunctionTemplate> builder)
|
||||
{
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
builder.HasIndex(x => x.ChannelId);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Elements)
|
||||
.WithOne()
|
||||
.HasForeignKey(e => e.JunctionTemplateId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Elements).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class JunctionElementConfiguration : IEntityTypeConfiguration<JunctionElement>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<JunctionElement> builder)
|
||||
{
|
||||
builder.HasIndex(x => new { x.JunctionTemplateId, x.Position });
|
||||
builder.Property(x => x.ConditionsJson).HasColumnType("jsonb");
|
||||
|
||||
// Группа не удаляется, пока на неё ссылается врезка: иначе стык молча перестал бы работать.
|
||||
builder
|
||||
.HasOne<Group>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.GroupId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -11,6 +11,14 @@ public class ScheduleEntryConfiguration : IEntityTypeConfiguration<ScheduleEntry
|
||||
// Основные запросы эфира/EPG — по каналу и времени.
|
||||
builder.HasIndex(x => new { x.ChannelId, x.StartsAtUtc });
|
||||
builder.HasIndex(x => new { x.ChannelId, x.EndsAtUtc });
|
||||
builder.HasIndex(x => new { x.ChannelId, x.ShowId });
|
||||
// Остывание спрашивает «когда этот элемент играл в последний раз» — по этому индексу.
|
||||
builder.HasIndex(x => new
|
||||
{
|
||||
x.ChannelId,
|
||||
x.ShowId,
|
||||
x.StartsAtUtc,
|
||||
});
|
||||
|
||||
builder.Property(x => x.TraceJson).HasColumnType("jsonb");
|
||||
}
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using TeleWave.Domain.Programming;
|
||||
|
||||
namespace TeleWave.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class ScheduleTemplateConfiguration : IEntityTypeConfiguration<ScheduleTemplate>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ScheduleTemplate> builder)
|
||||
{
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(256);
|
||||
builder.HasIndex(x => x.ChannelId);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Layers)
|
||||
.WithOne()
|
||||
.HasForeignKey(l => l.TemplateId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Layers).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class GridLayerConfiguration : IEntityTypeConfiguration<GridLayer>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<GridLayer> builder)
|
||||
{
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
builder.Property(x => x.ApplicabilityJson).HasColumnType("jsonb");
|
||||
builder.HasIndex(x => new { x.TemplateId, x.Priority });
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Slots)
|
||||
.WithOne()
|
||||
.HasForeignKey(s => s.LayerId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Slots).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
public class SlotConfiguration : IEntityTypeConfiguration<Slot>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Slot> builder)
|
||||
{
|
||||
builder.Property(x => x.Title).IsRequired().HasMaxLength(256);
|
||||
builder.Property(x => x.StrategyJson).HasColumnType("jsonb");
|
||||
builder.Property(x => x.RepeatSourceJson).HasColumnType("jsonb");
|
||||
builder.HasIndex(x => new { x.LayerId, x.TargetStart });
|
||||
builder.HasIndex(x => x.GroupId);
|
||||
|
||||
// Группа не удаляется, пока на неё ссылается слот: иначе слот молча превратился бы в дыру,
|
||||
// и админ узнал бы об этом только из эфира.
|
||||
builder
|
||||
.HasOne<Group>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.GroupId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
public class SlotStateConfiguration : IEntityTypeConfiguration<SlotState>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SlotState> builder)
|
||||
{
|
||||
builder.HasKey(x => x.SlotId);
|
||||
|
||||
// Состояние живёт ровно столько же, сколько слот.
|
||||
builder
|
||||
.HasOne<Slot>()
|
||||
.WithOne()
|
||||
.HasForeignKey<SlotState>(x => x.SlotId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,13 @@ public class ShowConfiguration : IEntityTypeConfiguration<Show>
|
||||
.HasForeignKey(e => e.ShowId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Episodes).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
|
||||
builder
|
||||
.HasMany(x => x.Genres)
|
||||
.WithOne()
|
||||
.HasForeignKey(g => g.ShowId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Navigation(x => x.Genres).UsePropertyAccessMode(PropertyAccessMode.Field);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user