diff --git a/backend/src/TeleWave.Api/Common/UploadLimits.cs b/backend/src/TeleWave.Api/Common/UploadLimits.cs
new file mode 100644
index 0000000..b954225
--- /dev/null
+++ b/backend/src/TeleWave.Api/Common/UploadLimits.cs
@@ -0,0 +1,21 @@
+using Microsoft.Extensions.Options;
+using TeleWave.Infrastructure.Media;
+
+namespace TeleWave.Api.Common;
+
+///
+/// Лимиты приёма файла, собранные в одно место. Они живут в двух разных секциях конфигурации
+/// (Media и Storage), но проверяются всегда вместе и только на входе загрузки —
+/// хендлеру незачем знать про обе секции и тащить два в сигнатуре.
+///
+public sealed class UploadLimits(
+ IOptions media,
+ IOptions storage
+)
+{
+ /// Потолок размера загружаемого файла.
+ public long MaxUploadBytes { get; } = media.Value.MaxUploadBytes;
+
+ /// Сколько места должно остаться свободным после приёма файла.
+ public long MinFreeSpaceBytes { get; } = storage.Value.MinFreeSpaceBytes;
+}
diff --git a/backend/src/TeleWave.Api/Endpoints/AdminUserEndpoints.cs b/backend/src/TeleWave.Api/Endpoints/AdminUserEndpoints.cs
index 34c0419..b7e2e1e 100644
--- a/backend/src/TeleWave.Api/Endpoints/AdminUserEndpoints.cs
+++ b/backend/src/TeleWave.Api/Endpoints/AdminUserEndpoints.cs
@@ -32,31 +32,21 @@ public static class AdminUserEndpoints
return app;
}
- ///
- /// Все параметры со значениями по умолчанию: обязательный bool в query превращает
- /// запрос без него в 400 ещё до хендлера — ошибку в таком виде отладить тяжело.
- ///
private static async Task ListUsers(
+ [AsParameters] ListUsersFilter filter,
ISender sender,
- CancellationToken cancellationToken,
- int page = 1,
- int pageSize = 20,
- string? search = null,
- Guid? roleId = null,
- bool? isBlocked = null,
- string? sort = null,
- bool desc = false
+ CancellationToken cancellationToken
)
{
var result = await sender.Send(
new ListUsersQuery(
- page <= 0 ? 1 : page,
- pageSize <= 0 ? 20 : pageSize,
- search,
- roleId,
- isBlocked,
- sort,
- desc
+ filter.Page is > 0 ? filter.Page.Value : 1,
+ filter.PageSize is > 0 ? filter.PageSize.Value : 20,
+ filter.Search,
+ filter.RoleId,
+ filter.IsBlocked,
+ filter.Sort,
+ filter.Desc ?? false
),
cancellationToken
);
@@ -126,6 +116,22 @@ public static class AdminUserEndpoints
}
}
+///
+/// Фильтр и страница списка пользователей. Все поля nullable намеренно: обязательный параметр в query
+/// превращает запрос без него в 400 ещё до хендлера — ошибку в таком виде отладить тяжело. Умолчания
+/// подставляет сам хендлер, а не объявление: при AsParameters обязательность определяется
+/// nullable-типом, а не наличием значения по умолчанию в конструкторе.
+///
+public sealed record ListUsersFilter(
+ int? Page,
+ int? PageSize,
+ string? Search,
+ Guid? RoleId,
+ bool? IsBlocked,
+ string? Sort,
+ bool? Desc
+);
+
public sealed record CreateUserBody(string UserName, string Password, Guid RoleId);
public sealed record ResetPasswordBody(string NewPassword);
diff --git a/backend/src/TeleWave.Api/Endpoints/ChannelEndpoints.Bumpers.cs b/backend/src/TeleWave.Api/Endpoints/ChannelEndpoints.Bumpers.cs
index 4368f9b..a5116db 100644
--- a/backend/src/TeleWave.Api/Endpoints/ChannelEndpoints.Bumpers.cs
+++ b/backend/src/TeleWave.Api/Endpoints/ChannelEndpoints.Bumpers.cs
@@ -66,9 +66,7 @@ public static partial class ChannelEndpoints
}
private static async Task UploadTemplateAudio(
- Guid id,
- Guid templateId,
- string fileName,
+ [AsParameters] BumperAudioUpload upload,
HttpRequest request,
IBumperTemplateStorage storage,
IAudioProbe probe,
@@ -76,23 +74,31 @@ public static partial class ChannelEndpoints
CancellationToken cancellationToken
)
{
- if (ResolveBumperExtension(fileName, request, BumperFiles.AudioExtensions) is not { } ext)
+ if (
+ ResolveBumperExtension(upload.FileName, request, BumperFiles.AudioExtensions)
+ is not { } ext
+ )
return ChannelErrors.InvalidBumperFile.ToProblem();
- await storage.SaveAudioAsync(templateId, ext, request.Body, cancellationToken);
+ await storage.SaveAudioAsync(upload.TemplateId, ext, request.Body, cancellationToken);
// Длина заставки идёт по длине звука — замеряем ffprobe (при неудаче 0 → дефолтная длина).
- var path = storage.AudioPath(templateId, ext);
+ var path = storage.AudioPath(upload.TemplateId, ext);
var duration = path is null
? null
: await probe.TryGetDurationAsync(path, cancellationToken);
var result = await sender.Send(
- new SetBumperTemplateAudioCommand(id, templateId, ext, duration?.TotalSeconds ?? 0),
+ new SetBumperTemplateAudioCommand(
+ upload.Id,
+ upload.TemplateId,
+ ext,
+ duration?.TotalSeconds ?? 0
+ ),
cancellationToken
);
if (!result.IsSuccess)
- await storage.DeleteAudioAsync(templateId, cancellationToken);
+ await storage.DeleteAudioAsync(upload.TemplateId, cancellationToken);
return result.ToHttpResult();
}
@@ -242,13 +248,12 @@ public static partial class ChannelEndpoints
return Results.Text(sb.ToString(), "application/vnd.apple.mpegurl");
}
- private static IResult PreviewSegment(
- Guid id,
- Guid templateId,
- Guid variantId,
- string file,
- MediaPathResolver paths
- )
+ ///
+ /// Сегмент превью. Канал и блок в маршруте есть, но хендлеру не нужны: каталог превью
+ /// адресуется подблоком (см. BumperPreview.AssetId), поэтому в сигнатуре их нет — незаявленные
+ /// параметры маршрута просто не связываются.
+ ///
+ private static IResult PreviewSegment(Guid variantId, string file, MediaPathResolver paths)
{
if (!SegmentFiles.IsSegmentName(file))
return Results.NotFound();
@@ -300,6 +305,13 @@ public sealed record UpdateBumperVariantBody(
int Weight
);
+///
+/// Адрес загружаемого звука: канал и блок из маршрута плюс имя исходного файла из query (по нему
+/// проверяется расширение). Свёрнуто в один параметр — кроме него хендлеру нужны ещё запрос, два
+/// сервиса, диспетчер и токен отмены, и плоским списком сигнатура перестаёт читаться.
+///
+public sealed record BumperAudioUpload(Guid Id, Guid TemplateId, string FileName);
+
/// Ограничения на загружаемый звук блока заставки (фон-картинка — через общий реестр).
internal static class BumperFiles
{
diff --git a/backend/src/TeleWave.Api/Endpoints/ImageEndpoints.cs b/backend/src/TeleWave.Api/Endpoints/ImageEndpoints.cs
index f8d0b47..dd68624 100644
--- a/backend/src/TeleWave.Api/Endpoints/ImageEndpoints.cs
+++ b/backend/src/TeleWave.Api/Endpoints/ImageEndpoints.cs
@@ -15,7 +15,9 @@ public static class ImageEndpoints
{
private const long MaxBytes = 50L * 1024 * 1024; // 50 МБ
- private static readonly IReadOnlySet AllowedExtensions = new HashSet(
+ // Тип поля — HashSet, а не IReadOnlySet: поле приватное, и через интерфейс Contains уходит
+ // в виртуальный вызов вместо прямого.
+ private static readonly HashSet AllowedExtensions = new(
StringComparer.OrdinalIgnoreCase
)
{
diff --git a/backend/src/TeleWave.Api/Endpoints/MediaEndpoints.cs b/backend/src/TeleWave.Api/Endpoints/MediaEndpoints.cs
index 6af816e..52bde5e 100644
--- a/backend/src/TeleWave.Api/Endpoints/MediaEndpoints.cs
+++ b/backend/src/TeleWave.Api/Endpoints/MediaEndpoints.cs
@@ -51,8 +51,7 @@ public static class MediaEndpoints
IMediaStorage storage,
IMediaProcessingQueue queue,
ISender sender,
- IOptions mediaOptions,
- IOptions storageOptions,
+ UploadLimits limits,
CancellationToken cancellationToken
)
{
@@ -71,7 +70,7 @@ public static class MediaEndpoints
);
var contentLength = request.ContentLength ?? 0;
- if (contentLength > mediaOptions.Value.MaxUploadBytes)
+ if (contentLength > limits.MaxUploadBytes)
return Results.Problem(
title: MediaErrors.FileTooLarge.Code,
detail: MediaErrors.FileTooLarge.Message,
@@ -79,7 +78,7 @@ public static class MediaEndpoints
);
var free = storage.GetAvailableFreeSpaceBytes();
- if (free - contentLength < storageOptions.Value.MinFreeSpaceBytes)
+ if (free - contentLength < limits.MinFreeSpaceBytes)
return Results.Problem(
title: MediaErrors.InsufficientStorage.Code,
detail: MediaErrors.InsufficientStorage.Message,
@@ -106,30 +105,20 @@ public static class MediaEndpoints
);
}
- ///
- /// Все параметры со значениями по умолчанию: обязательный bool в query заставлял
- /// минимальный API отвечать 400 на запросы без него — например, из выборок «все готовые ассеты»,
- /// которым сортировка не нужна.
- ///
private static async Task List(
+ [AsParameters] ListMediaFilter filter,
ISender sender,
- CancellationToken cancellationToken,
- int page = 1,
- int pageSize = 20,
- MediaAssetStatus[]? status = null,
- string? search = null,
- string? sort = null,
- bool desc = false
+ CancellationToken cancellationToken
)
{
var result = await sender.Send(
new ListMediaAssetsQuery(
- page <= 0 ? 1 : page,
- pageSize <= 0 ? 20 : pageSize,
- status ?? [],
- search,
- sort,
- desc
+ filter.Page is > 0 ? filter.Page.Value : 1,
+ filter.PageSize is > 0 ? filter.PageSize.Value : 20,
+ filter.Status ?? [],
+ filter.Search,
+ filter.Sort,
+ filter.Desc ?? false
),
cancellationToken
);
@@ -204,6 +193,21 @@ public static class MediaEndpoints
}
}
+///
+/// Фильтр и страница списка медиа. Все поля nullable намеренно: обязательный параметр в query
+/// заставлял минимальный API отвечать 400 на запросы без него — например, из выборок «все готовые
+/// ассеты», которым сортировка не нужна. Умолчания подставляет хендлер, а не объявление: при
+/// AsParameters обязательность определяется nullable-типом, а не значением по умолчанию.
+///
+public sealed record ListMediaFilter(
+ int? Page,
+ int? PageSize,
+ MediaAssetStatus[]? Status,
+ string? Search,
+ string? Sort,
+ bool? Desc
+);
+
public sealed record UploadMediaResponse(Guid Id);
public sealed record ImportManualInboxBody(IReadOnlyList Items, Guid ShowId);
diff --git a/backend/src/TeleWave.Api/Endpoints/StreamingEndpoints.cs b/backend/src/TeleWave.Api/Endpoints/StreamingEndpoints.cs
index ecd8de3..c9af4c9 100644
--- a/backend/src/TeleWave.Api/Endpoints/StreamingEndpoints.cs
+++ b/backend/src/TeleWave.Api/Endpoints/StreamingEndpoints.cs
@@ -51,8 +51,12 @@ public static class StreamingEndpoints
new ViewerFeaturesDto(await siteSettings.AreChannelNumbersEnabledAsync(cancellationToken))
);
+ ///
+ /// Выдаёт cookie доступа к эфиру. Канал в маршруте есть для симметрии с остальными
+ /// эндпоинтами, но токен не привязан к каналу — он подтверждает зрителя, а не подписку на
+ /// конкретную ленту, поэтому в сигнатуре slug не нужен.
+ ///
private static IResult Watch(
- string slug,
ICurrentUser currentUser,
StreamTokenService tokens,
HttpRequest request,
diff --git a/backend/src/TeleWave.Api/Program.cs b/backend/src/TeleWave.Api/Program.cs
index cbdc289..88d2c0f 100644
--- a/backend/src/TeleWave.Api/Program.cs
+++ b/backend/src/TeleWave.Api/Program.cs
@@ -61,6 +61,7 @@ builder.Services.Configure(options =>
});
builder.Services.AddHttpContextAccessor();
+builder.Services.AddSingleton();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);