From 720f96fe0a2e12a4c456a52e7b7211fcee743b7b Mon Sep 17 00:00:00 2001 From: Marcos Cordeiro Date: Sat, 2 Mar 2024 17:26:42 -0300 Subject: [PATCH] Improves TVDB Api implementation * Removes Refit * Uses JSON source generated contexts * Publishes trimmed objects --- Wasari.Tvdb.Api/Dockerfile | 2 +- Wasari.Tvdb.Api/Program.cs | 6 ++++ .../Services/TvdbEpisodesService.cs | 22 ++++++++------- Wasari.Tvdb.Api/Wasari.Tvdb.Api.csproj | 1 + .../WasariTvdbApiResponseSourceContext.cs | 9 ++++++ Wasari.Tvdb/AppExtensions.cs | 22 +++++---------- Wasari.Tvdb/ITvdbApi.cs | 13 --------- .../MissingEnvironmentVariableException.cs | 2 +- Wasari.Tvdb/Models/ITvdbApi.cs | 9 ++++++ Wasari.Tvdb/Models/TvdbLoginRequest.cs | 5 ++++ .../Models/TvdbSourceGenerationContext.cs | 12 ++++++++ Wasari.Tvdb/TvdbApi.cs | 28 +++++++++++++++++++ Wasari.Tvdb/TvdbTokenHandler.cs | 17 +++++------ Wasari.Tvdb/Wasari.Tvdb.csproj | 2 +- 14 files changed, 99 insertions(+), 51 deletions(-) create mode 100644 Wasari.Tvdb.Api/WasariTvdbApiResponseSourceContext.cs delete mode 100644 Wasari.Tvdb/ITvdbApi.cs create mode 100644 Wasari.Tvdb/Models/ITvdbApi.cs create mode 100644 Wasari.Tvdb/Models/TvdbLoginRequest.cs create mode 100644 Wasari.Tvdb/Models/TvdbSourceGenerationContext.cs create mode 100644 Wasari.Tvdb/TvdbApi.cs diff --git a/Wasari.Tvdb.Api/Dockerfile b/Wasari.Tvdb.Api/Dockerfile index e4ca85c..dc5c6ec 100644 --- a/Wasari.Tvdb.Api/Dockerfile +++ b/Wasari.Tvdb.Api/Dockerfile @@ -14,7 +14,7 @@ WORKDIR "/src/Wasari.Tvdb.Api" RUN dotnet build "Wasari.Tvdb.Api.csproj" -c Release -o /app/build FROM build AS publish -RUN dotnet publish "Wasari.Tvdb.Api.csproj" -c Release --no-self-contained -p:PublishReadyToRun=true -o /app/publish /p:UseAppHost=false +RUN dotnet publish "Wasari.Tvdb.Api.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app diff --git a/Wasari.Tvdb.Api/Program.cs b/Wasari.Tvdb.Api/Program.cs index 2ca0d43..05b2740 100644 --- a/Wasari.Tvdb.Api/Program.cs +++ b/Wasari.Tvdb.Api/Program.cs @@ -1,4 +1,6 @@ +using Microsoft.AspNetCore.Http.Json; using Wasari.Tvdb; +using Wasari.Tvdb.Api; using Wasari.Tvdb.Api.Policies; using Wasari.Tvdb.Api.Services; @@ -11,6 +13,10 @@ options.AddPolicy(nameof(EpisodeCachePolicy), EpisodeCachePolicy.Instance); }); builder.Services.AddScoped(); +builder.Services.Configure(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Add(WasariTvdbApiResponseSourceContext.Default); +}); var app = builder.Build(); app.UseOutputCache(); diff --git a/Wasari.Tvdb.Api/Services/TvdbEpisodesService.cs b/Wasari.Tvdb.Api/Services/TvdbEpisodesService.cs index 24d4a74..fce201f 100644 --- a/Wasari.Tvdb.Api/Services/TvdbEpisodesService.cs +++ b/Wasari.Tvdb.Api/Services/TvdbEpisodesService.cs @@ -1,4 +1,5 @@ using Wasari.Tvdb.Abstractions; +using Wasari.Tvdb.Models; namespace Wasari.Tvdb.Api.Services; @@ -14,11 +15,15 @@ public TvdbEpisodesService(ITvdbApi tvdbApi) public async ValueTask GetEpisodes(string query) { var searchResult = await TvdbApi.SearchAsync(query); + + if(searchResult is null) + return Results.BadRequest(new TvdbApiErrorResponse(StatusCodes.Status400BadRequest, "Invalid query", "No series found")); + var tvdbSearchResponseSeries = searchResult.Data; - var series = tvdbSearchResponseSeries.SingleOrDefaultIfMultiple(); + var series = tvdbSearchResponseSeries?.SingleOrDefaultIfMultiple(); - if (tvdbSearchResponseSeries.Count > 1) + if (tvdbSearchResponseSeries is { Count: > 1 }) { series ??= tvdbSearchResponseSeries .Where(i => string.Equals(i.Name, query, StringComparison.InvariantCultureIgnoreCase)) @@ -34,18 +39,13 @@ public async ValueTask GetEpisodes(string query) } if (series == null) - return Results.BadRequest(new - { - Status = StatusCodes.Status400BadRequest, - Title = "Invalid query", - Detail = tvdbSearchResponseSeries.Count > 0 ? "Multiple series found" : "No series found" - }); + return Results.BadRequest(new TvdbApiErrorResponse(StatusCodes.Status400BadRequest, "Invalid query", tvdbSearchResponseSeries is { Count: > 0 } ? "Multiple series found" : "No series found")); var seriesWithEpisodes = await TvdbApi.GetSeriesAsync(series.TvdbId); var currentEpiosdeNumber = 1; - return Results.Ok(seriesWithEpisodes.Data.Episodes + return Results.Ok(seriesWithEpisodes?.Data.Episodes .Where(i => !string.IsNullOrEmpty(i.Name)) .OrderBy(i => i.SeasonNumber) .ThenBy(i => i.Number) @@ -67,4 +67,6 @@ public async ValueTask GetEpisodes(string query) }) ); } -} \ No newline at end of file +} + +public record TvdbApiErrorResponse(int Status, string Title, string Detail); \ No newline at end of file diff --git a/Wasari.Tvdb.Api/Wasari.Tvdb.Api.csproj b/Wasari.Tvdb.Api/Wasari.Tvdb.Api.csproj index d1dff79..bea8588 100644 --- a/Wasari.Tvdb.Api/Wasari.Tvdb.Api.csproj +++ b/Wasari.Tvdb.Api/Wasari.Tvdb.Api.csproj @@ -5,6 +5,7 @@ enable enable Linux + true diff --git a/Wasari.Tvdb.Api/WasariTvdbApiResponseSourceContext.cs b/Wasari.Tvdb.Api/WasariTvdbApiResponseSourceContext.cs new file mode 100644 index 0000000..e1a2b41 --- /dev/null +++ b/Wasari.Tvdb.Api/WasariTvdbApiResponseSourceContext.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; +using Wasari.Tvdb.Abstractions; +using Wasari.Tvdb.Api.Services; + +namespace Wasari.Tvdb.Api; + +[JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(TvdbApiErrorResponse))] +internal partial class WasariTvdbApiResponseSourceContext : JsonSerializerContext; \ No newline at end of file diff --git a/Wasari.Tvdb/AppExtensions.cs b/Wasari.Tvdb/AppExtensions.cs index 6934411..3b12fe7 100644 --- a/Wasari.Tvdb/AppExtensions.cs +++ b/Wasari.Tvdb/AppExtensions.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Polly; using Polly.Extensions.Http; -using Refit; +using Wasari.Tvdb.Models; namespace Wasari.Tvdb; @@ -17,17 +17,6 @@ private static Uri EnsureTrailingSlash(this Uri uri) return new Uri(uriString); } - private static Uri EnsureNoTrailingSlash(this Uri uri) - { - var uriString = uri.ToString(); - if (uriString.EndsWith("/")) - uriString = uriString[..^1]; - else - return uri; - - return new Uri(uriString); - } - public static void AddTvdbServices(this IServiceCollection services) { var policy = HttpPolicyExtensions @@ -37,13 +26,16 @@ public static void AddTvdbServices(this IServiceCollection services) var baseAddress = Environment.GetEnvironmentVariable("TVDB_API_URL") is { } baseUrl ? new Uri(baseUrl) - : new Uri("https://api4.thetvdb.com/v4"); + : new Uri("https://api4.thetvdb.com"); services.AddMemoryCache(); services.AddHttpClient(c => { c.BaseAddress = baseAddress.EnsureTrailingSlash(); }); - services.AddRefitClient() + services.AddHttpClient() + .ConfigureHttpClient(c => + { + c.BaseAddress = baseAddress; + }) .AddHttpMessageHandler() - .ConfigureHttpClient(c => { c.BaseAddress = baseAddress.EnsureNoTrailingSlash(); }) .AddPolicyHandler(policy); } } \ No newline at end of file diff --git a/Wasari.Tvdb/ITvdbApi.cs b/Wasari.Tvdb/ITvdbApi.cs deleted file mode 100644 index e38cdb1..0000000 --- a/Wasari.Tvdb/ITvdbApi.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Refit; -using Wasari.Tvdb.Models; - -namespace Wasari.Tvdb; - -public interface ITvdbApi -{ - [Get("/search")] - Task>> SearchAsync(string query, string type = "series"); - - [Get("/series/{id}/episodes/{seasonType}/{lang}")] - Task> GetSeriesAsync(string id, string seasonType = "default", string lang = "eng", int page = 0); -} \ No newline at end of file diff --git a/Wasari.Tvdb/MissingEnvironmentVariableException.cs b/Wasari.Tvdb/MissingEnvironmentVariableException.cs index 23b829a..ff023e4 100644 --- a/Wasari.Tvdb/MissingEnvironmentVariableException.cs +++ b/Wasari.Tvdb/MissingEnvironmentVariableException.cs @@ -1,6 +1,6 @@ namespace Wasari.Tvdb; -public class MissingEnvironmentVariableException : Exception +internal class MissingEnvironmentVariableException : Exception { public MissingEnvironmentVariableException(string variableName) : base($"Missing environment variable: {variableName}") { diff --git a/Wasari.Tvdb/Models/ITvdbApi.cs b/Wasari.Tvdb/Models/ITvdbApi.cs new file mode 100644 index 0000000..2375905 --- /dev/null +++ b/Wasari.Tvdb/Models/ITvdbApi.cs @@ -0,0 +1,9 @@ +namespace Wasari.Tvdb.Models; + +public interface ITvdbApi +{ + Task>?> SearchAsync(string query, string type = "series"); + + Task?> GetSeriesAsync(string id, string seasonType = "default", string lang = "eng", + int page = 0); +} \ No newline at end of file diff --git a/Wasari.Tvdb/Models/TvdbLoginRequest.cs b/Wasari.Tvdb/Models/TvdbLoginRequest.cs new file mode 100644 index 0000000..9578300 --- /dev/null +++ b/Wasari.Tvdb/Models/TvdbLoginRequest.cs @@ -0,0 +1,5 @@ +using System.Text.Json.Serialization; + +namespace Wasari.Tvdb.Models; + +public record TvdbLoginRequest([property: JsonPropertyName("apikey")] string ApiKey, [property: JsonPropertyName("pin")] string Pin); \ No newline at end of file diff --git a/Wasari.Tvdb/Models/TvdbSourceGenerationContext.cs b/Wasari.Tvdb/Models/TvdbSourceGenerationContext.cs new file mode 100644 index 0000000..4fdf632 --- /dev/null +++ b/Wasari.Tvdb/Models/TvdbSourceGenerationContext.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Wasari.Tvdb.Models; + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(TvdbResponse>))] +[JsonSerializable(typeof(TvdbResponse))] +[JsonSerializable(typeof(TvdbResponse))] +[JsonSerializable(typeof(TvdbLoginRequest))] +internal partial class TvdbSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Wasari.Tvdb/TvdbApi.cs b/Wasari.Tvdb/TvdbApi.cs new file mode 100644 index 0000000..802de93 --- /dev/null +++ b/Wasari.Tvdb/TvdbApi.cs @@ -0,0 +1,28 @@ +using System.Net.Http.Json; +using Wasari.Tvdb.Models; + +namespace Wasari.Tvdb; + +internal class TvdbApi : ITvdbApi +{ + private readonly HttpClient _httpClient; + + public TvdbApi(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public Task>?> SearchAsync(string query, + string type = "series") + { + var url = $"/v4/search?query={query}&type={type}"; + return _httpClient.GetFromJsonAsync(url, TvdbSourceGenerationContext.Default.TvdbResponseIReadOnlyListTvdbSearchResponseSeries); + } + + public Task?> GetSeriesAsync(string id, string seasonType = "default", string lang = "eng", + int page = 0) + { + var url = $"/v4/series/{id}/episodes/{seasonType}/{lang}?page={page}"; + return _httpClient.GetFromJsonAsync(url, TvdbSourceGenerationContext.Default.TvdbResponseTvdbSeries); + } +} \ No newline at end of file diff --git a/Wasari.Tvdb/TvdbTokenHandler.cs b/Wasari.Tvdb/TvdbTokenHandler.cs index 5ca389e..7835b73 100644 --- a/Wasari.Tvdb/TvdbTokenHandler.cs +++ b/Wasari.Tvdb/TvdbTokenHandler.cs @@ -7,10 +7,11 @@ namespace Wasari.Tvdb; -public class TvdbTokenHandler : DelegatingHandler +internal class TvdbTokenHandler : DelegatingHandler { private const string TvdbTokenCacheKey = "tvdb_token"; private static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new(); + private static readonly TvdbLoginRequest TvdbLoginRequest = new(Environment.GetEnvironmentVariable("TVDB_API_KEY") ?? throw new MissingEnvironmentVariableException("TVDB_API_KEY"), Environment.GetEnvironmentVariable("TVDB_API_PIN") ?? "TVDB_API_KEY"); public TvdbTokenHandler(IMemoryCache memoryCache, HttpClient tvdbClient) { @@ -24,22 +25,18 @@ public TvdbTokenHandler(IMemoryCache memoryCache, HttpClient tvdbClient) private async Task GetToken(ICacheEntry e, CancellationToken cancellationToken) { - var response = await TvdbClient.PostAsJsonAsync("login", new - { - apikey = Environment.GetEnvironmentVariable("TVDB_API_KEY") ?? throw new MissingEnvironmentVariableException("TVDB_API_KEY"), - pin = Environment.GetEnvironmentVariable("TVDB_API_PIN") ?? "TVDB_API_KEY" - }, cancellationToken); + var response = await TvdbClient.PostAsJsonAsync("v4/login", TvdbLoginRequest, TvdbSourceGenerationContext.Default.TvdbLoginRequest, cancellationToken); response.EnsureSuccessStatusCode(); - var tokenResponse = await response.Content.ReadFromJsonAsync>(cancellationToken); + var tokenResponse = await response.Content.ReadFromJsonAsync(TvdbSourceGenerationContext.Default.TvdbResponseTvdbTokenResponseData, cancellationToken); - if (tokenResponse is not { Status: "success" }) throw new Exception("Failed to get token"); + if (tokenResponse is not { Status: "success" } || tokenResponse.Data == null) throw new Exception("Failed to get token"); - var jwt = JwtSecurityTokenHandler.ReadJwtToken(tokenResponse.Data.Token); + var jwt = JwtSecurityTokenHandler.ReadJwtToken(tokenResponse.Data?.Token); e.SetAbsoluteExpiration(jwt.ValidTo); - return tokenResponse.Data.Token; + return tokenResponse.Data!.Token; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/Wasari.Tvdb/Wasari.Tvdb.csproj b/Wasari.Tvdb/Wasari.Tvdb.csproj index 959e438..e595d3b 100644 --- a/Wasari.Tvdb/Wasari.Tvdb.csproj +++ b/Wasari.Tvdb/Wasari.Tvdb.csproj @@ -4,13 +4,13 @@ net8.0 enable enable + true -