Skip to content
This repository has been archived by the owner on Apr 13, 2024. It is now read-only.

Commit

Permalink
Improves TVDB Api implementation
Browse files Browse the repository at this point in the history
* Removes Refit
* Uses JSON source generated contexts
* Publishes trimmed objects
  • Loading branch information
Marcos Cordeiro committed Mar 2, 2024
1 parent c10ff59 commit 720f96f
Show file tree
Hide file tree
Showing 14 changed files with 99 additions and 51 deletions.
2 changes: 1 addition & 1 deletion Wasari.Tvdb.Api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Wasari.Tvdb.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -11,6 +13,10 @@
options.AddPolicy(nameof(EpisodeCachePolicy), EpisodeCachePolicy.Instance);
});
builder.Services.AddScoped<TvdbEpisodesService>();
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Add(WasariTvdbApiResponseSourceContext.Default);
});

var app = builder.Build();
app.UseOutputCache();
Expand Down
22 changes: 12 additions & 10 deletions Wasari.Tvdb.Api/Services/TvdbEpisodesService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Wasari.Tvdb.Abstractions;
using Wasari.Tvdb.Models;

namespace Wasari.Tvdb.Api.Services;

Expand All @@ -14,11 +15,15 @@ public TvdbEpisodesService(ITvdbApi tvdbApi)
public async ValueTask<IResult> 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))
Expand All @@ -34,18 +39,13 @@ public async ValueTask<IResult> 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)
Expand All @@ -67,4 +67,6 @@ public async ValueTask<IResult> GetEpisodes(string query)
})
);
}
}
}

public record TvdbApiErrorResponse(int Status, string Title, string Detail);
1 change: 1 addition & 0 deletions Wasari.Tvdb.Api/Wasari.Tvdb.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

<ItemGroup>
Expand Down
9 changes: 9 additions & 0 deletions Wasari.Tvdb.Api/WasariTvdbApiResponseSourceContext.cs
Original file line number Diff line number Diff line change
@@ -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<WasariTvdbEpisode>))]
[JsonSerializable(typeof(TvdbApiErrorResponse))]
internal partial class WasariTvdbApiResponseSourceContext : JsonSerializerContext;
22 changes: 7 additions & 15 deletions Wasari.Tvdb/AppExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Extensions.Http;
using Refit;
using Wasari.Tvdb.Models;

namespace Wasari.Tvdb;

Expand All @@ -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
Expand All @@ -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<TvdbTokenHandler>(c => { c.BaseAddress = baseAddress.EnsureTrailingSlash(); });
services.AddRefitClient<ITvdbApi>()
services.AddHttpClient<ITvdbApi, TvdbApi>()
.ConfigureHttpClient(c =>
{
c.BaseAddress = baseAddress;
})
.AddHttpMessageHandler<TvdbTokenHandler>()
.ConfigureHttpClient(c => { c.BaseAddress = baseAddress.EnsureNoTrailingSlash(); })
.AddPolicyHandler(policy);
}
}
13 changes: 0 additions & 13 deletions Wasari.Tvdb/ITvdbApi.cs

This file was deleted.

2 changes: 1 addition & 1 deletion Wasari.Tvdb/MissingEnvironmentVariableException.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Wasari.Tvdb;

public class MissingEnvironmentVariableException : Exception
internal class MissingEnvironmentVariableException : Exception
{
public MissingEnvironmentVariableException(string variableName) : base($"Missing environment variable: {variableName}")
{
Expand Down
9 changes: 9 additions & 0 deletions Wasari.Tvdb/Models/ITvdbApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Wasari.Tvdb.Models;

public interface ITvdbApi
{
Task<TvdbResponse<IReadOnlyList<TvdbSearchResponseSeries>>?> SearchAsync(string query, string type = "series");

Task<TvdbResponse<TvdbSeries>?> GetSeriesAsync(string id, string seasonType = "default", string lang = "eng",
int page = 0);
}
5 changes: 5 additions & 0 deletions Wasari.Tvdb/Models/TvdbLoginRequest.cs
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 12 additions & 0 deletions Wasari.Tvdb/Models/TvdbSourceGenerationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace Wasari.Tvdb.Models;

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(TvdbResponse<IReadOnlyList<TvdbSearchResponseSeries>>))]
[JsonSerializable(typeof(TvdbResponse<TvdbSeries>))]
[JsonSerializable(typeof(TvdbResponse<TvdbTokenResponseData?>))]
[JsonSerializable(typeof(TvdbLoginRequest))]
internal partial class TvdbSourceGenerationContext : JsonSerializerContext
{
}
28 changes: 28 additions & 0 deletions Wasari.Tvdb/TvdbApi.cs
Original file line number Diff line number Diff line change
@@ -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<TvdbResponse<IReadOnlyList<TvdbSearchResponseSeries>>?> SearchAsync(string query,
string type = "series")
{
var url = $"/v4/search?query={query}&type={type}";
return _httpClient.GetFromJsonAsync(url, TvdbSourceGenerationContext.Default.TvdbResponseIReadOnlyListTvdbSearchResponseSeries);
}

public Task<TvdbResponse<TvdbSeries>?> 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);
}
}
17 changes: 7 additions & 10 deletions Wasari.Tvdb/TvdbTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -24,22 +25,18 @@ public TvdbTokenHandler(IMemoryCache memoryCache, HttpClient tvdbClient)

private async Task<string> 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<TvdbResponse<TvdbTokenResponseData>>(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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Expand Down
2 changes: 1 addition & 1 deletion Wasari.Tvdb/Wasari.Tvdb.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.2" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="7.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.0" />
</ItemGroup>

Expand Down

0 comments on commit 720f96f

Please sign in to comment.