Refactor user and media endpoints: remove unused GetUser and GetMedia methods, streamline AdminUserEndpoints and MediaEndpoints, and enhance metadata handling in MetadataEndpoints. Update Program.cs to remove legacy image relocation logic and improve overall code clarity.
build / backend (push) Successful in 1m14s
build / frontend (push) Successful in 35s
tests / backend-tests (push) Successful in 3m16s

This commit is contained in:
Leonid Pershin
2026-07-26 02:58:17 +03:00
parent 44e954920c
commit a0533a8ed8
50 changed files with 230 additions and 849 deletions
@@ -0,0 +1,53 @@
using System.Text.Json;
namespace TeleWave.Infrastructure.Metadata;
/// <summary>Общие для провайдеров метаданных хелперы: HTTP-загрузка JSON и чтение полей.</summary>
internal static class MetadataJson
{
/// <summary>GET+parse через клиент "metadata"; бросает при не-2xx/сетевой ошибке (для поиска — показать сбой).</summary>
public static async Task<JsonDocument> GetAsync(
IHttpClientFactory httpFactory,
string url,
CancellationToken cancellationToken
)
{
var client = httpFactory.CreateClient("metadata");
using var response = await client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
return await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
}
/// <summary>Как <see cref="GetAsync"/>, но глотает ошибки в null (для get/episode — деградируем мягко).</summary>
public static async Task<JsonDocument?> TryGetAsync(
IHttpClientFactory httpFactory,
string url,
CancellationToken cancellationToken
)
{
try
{
return await GetAsync(httpFactory, url, cancellationToken);
}
catch (Exception ex)
when (ex is HttpRequestException or JsonException or TaskCanceledException)
{
return null;
}
}
public static string? GetString(JsonElement el, string name) =>
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
public static int? GetInt(JsonElement el, string name) =>
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number
? v.GetInt32()
: null;
/// <summary>Год из первых 4 символов строки даты/года ("YYYY-MM-DD" или "YYYY").</summary>
public static int? YearFrom(string? value) =>
value is { Length: >= 4 } && int.TryParse(value.AsSpan(0, 4), out var y) ? y : null;
}
@@ -3,6 +3,7 @@ using System.Text.Json;
using Microsoft.Extensions.Options;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Metadata;
using static TeleWave.Infrastructure.Metadata.MetadataJson;
namespace TeleWave.Infrastructure.Metadata;
@@ -23,7 +24,7 @@ public sealed class OmdbMetadataProvider(
{
var url =
$"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&type=series&s={Uri.EscapeDataString(query)}";
using var doc = await GetJsonAsync(url, cancellationToken);
using var doc = await GetAsync(httpFactory, url, cancellationToken);
if (!doc.RootElement.TryGetProperty("Search", out var search))
return [];
@@ -52,7 +53,7 @@ public sealed class OmdbMetadataProvider(
)
{
var url = $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}";
using var doc = await TryGetJsonAsync(url, cancellationToken);
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
if (doc is null || !IsResponseTrue(doc.RootElement))
return null;
var root = doc.RootElement;
@@ -75,7 +76,7 @@ public sealed class OmdbMetadataProvider(
var url =
$"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}"
+ $"&Season={season}&Episode={episode}";
using var doc = await TryGetJsonAsync(url, cancellationToken);
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
if (doc is null || !IsResponseTrue(doc.RootElement))
return null;
var root = doc.RootElement;
@@ -95,7 +96,7 @@ public sealed class OmdbMetadataProvider(
{
var url =
$"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}&Season={season}";
using var doc = await TryGetJsonAsync(url, cancellationToken);
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
if (
doc is null
|| !IsResponseTrue(doc.RootElement)
@@ -106,53 +107,14 @@ public sealed class OmdbMetadataProvider(
return episodes.GetArrayLength();
}
/// <summary>GET+parse; бросает при не-2xx/сетевой ошибке (для поиска — чтобы показать сбой).</summary>
private async Task<JsonDocument> GetJsonAsync(string url, CancellationToken cancellationToken)
{
var client = httpFactory.CreateClient("metadata");
using var response = await client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
return await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
}
/// <summary>Как GetJsonAsync, но глотает ошибки в null (для get/episode — деградируем мягко).</summary>
private async Task<JsonDocument?> TryGetJsonAsync(
string url,
CancellationToken cancellationToken
)
{
try
{
return await GetJsonAsync(url, cancellationToken);
}
catch (Exception ex)
when (ex is HttpRequestException or JsonException or TaskCanceledException)
{
return null;
}
}
private static bool IsResponseTrue(JsonElement root) =>
GetString(root, "Response") is { } r
&& r.Equals("True", StringComparison.OrdinalIgnoreCase);
private static string? GetString(JsonElement el, string name) =>
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
/// <summary>OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null.</summary>
private static string? Clean(string? value) =>
string.IsNullOrWhiteSpace(value) || value == "N/A" ? null : value;
private static int? YearFrom(string? year)
{
if (year is { Length: >= 4 } && int.TryParse(year.AsSpan(0, 4), out var y))
return y;
return null;
}
private static DateOnly? DateFrom(string? released) =>
DateTime.TryParse(released, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)
? DateOnly.FromDateTime(dt)
@@ -3,6 +3,7 @@ using System.Text.Json;
using Microsoft.Extensions.Options;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Metadata;
using static TeleWave.Infrastructure.Metadata.MetadataJson;
namespace TeleWave.Infrastructure.Metadata;
@@ -26,7 +27,7 @@ public sealed class TmdbMetadataProvider(
var url =
$"{Tmdb.BaseUrl}/search/tv?api_key={Tmdb.ApiKey}&language={_options.Language}"
+ $"&include_adult=false&query={Uri.EscapeDataString(query)}";
using var doc = await GetJsonAsync(url, cancellationToken);
using var doc = await GetAsync(httpFactory, url, cancellationToken);
if (!doc.RootElement.TryGetProperty("results", out var results))
return [];
@@ -56,7 +57,7 @@ public sealed class TmdbMetadataProvider(
{
var url =
$"{Tmdb.BaseUrl}/tv/{externalId}?api_key={Tmdb.ApiKey}&language={_options.Language}";
using var doc = await TryGetJsonAsync(url, cancellationToken);
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
if (doc is null)
return null;
var root = doc.RootElement;
@@ -79,7 +80,7 @@ public sealed class TmdbMetadataProvider(
var url =
$"{Tmdb.BaseUrl}/tv/{externalId}/season/{season}/episode/{episode}"
+ $"?api_key={Tmdb.ApiKey}&language={_options.Language}";
using var doc = await TryGetJsonAsync(url, cancellationToken);
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
if (doc is null)
return null;
var root = doc.RootElement;
@@ -100,7 +101,7 @@ public sealed class TmdbMetadataProvider(
var url =
$"{Tmdb.BaseUrl}/tv/{externalId}/season/{season}"
+ $"?api_key={Tmdb.ApiKey}&language={_options.Language}";
using var doc = await TryGetJsonAsync(url, cancellationToken);
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
if (
doc is null
|| !doc.RootElement.TryGetProperty("episodes", out var episodes)
@@ -116,46 +117,6 @@ public sealed class TmdbMetadataProvider(
private string? StillUrl(string? path) =>
string.IsNullOrEmpty(path) ? null : $"{Tmdb.ImageBaseUrl}/{Tmdb.StillSize}{path}";
/// <summary>GET+parse; бросает при не-2xx/сетевой ошибке (для поиска — чтобы показать сбой).</summary>
private async Task<JsonDocument> GetJsonAsync(string url, CancellationToken cancellationToken)
{
var client = httpFactory.CreateClient("metadata");
using var response = await client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
return await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
}
/// <summary>Как GetJsonAsync, но глотает ошибки в null (для get/episode — деградируем мягко).</summary>
private async Task<JsonDocument?> TryGetJsonAsync(
string url,
CancellationToken cancellationToken
)
{
try
{
return await GetJsonAsync(url, cancellationToken);
}
catch (Exception ex)
when (ex is HttpRequestException or JsonException or TaskCanceledException)
{
return null;
}
}
private static string? GetString(JsonElement el, string name) =>
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
private static int? GetInt(JsonElement el, string name) =>
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.Number
? v.GetInt32()
: null;
private static int? YearFrom(string? date) =>
date is { Length: >= 4 } && int.TryParse(date.AsSpan(0, 4), out var y) ? y : null;
private static DateOnly? DateFrom(string? date) =>
DateOnly.TryParse(date, CultureInfo.InvariantCulture, out var d) ? d : null;
}