MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts, scan de code-barres, consultation hors-ligne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using MaBibli.Shared.Dtos;
|
||||
|
||||
namespace MaBibli.Api.Services.Isbn;
|
||||
|
||||
public interface IOpenLibraryClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Cherche l'édition correspondant à un ISBN chez OpenLibrary, auteurs résolus.
|
||||
/// Renvoie une liste vide si l'ISBN est inconnu ou la source injoignable.
|
||||
/// </summary>
|
||||
Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
|
||||
string isbn, string? urlCouverture, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client OpenLibrary — second rideau de la cascade, pour les livres étrangers et tout ce que
|
||||
/// la BnF ne connaît pas.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Bug à ne pas reproduire</b> (observé chez BookLogr) : <c>/isbn/{isbn}.json</c> ne donne pas
|
||||
/// le nom de l'auteur mais une référence <c>/authors/OL…A</c>. Sans second appel, l'auteur reste
|
||||
/// vide. Ce client fait ce second appel, et bascule sur l'œuvre rattachée quand l'édition ne
|
||||
/// porte aucun auteur (cas mesuré : Introduction to Algorithms).
|
||||
/// </remarks>
|
||||
public sealed class OpenLibraryClient(HttpClient http, ILogger<OpenLibraryClient> logger) : IOpenLibraryClient
|
||||
{
|
||||
public const string NomHttpClient = "openlibrary";
|
||||
|
||||
public const string UrlBase = "https://openlibrary.org/";
|
||||
|
||||
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
|
||||
string isbn, string? urlCouverture, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// /isbn/{isbn}.json répond en 302 vers /books/OL…M.json quand le livre existe, 404 sinon.
|
||||
// Le HttpClient suit la redirection par défaut.
|
||||
var edition = await LireAsync<OpenLibraryEdition>($"isbn/{isbn}.json", ct);
|
||||
if (edition is null)
|
||||
{
|
||||
return ([], null);
|
||||
}
|
||||
|
||||
var noms = await ResoudreAuteursAsync(edition, ct);
|
||||
var candidat = OpenLibraryMapper.VersCandidat(edition, noms, isbn, urlCouverture);
|
||||
return (candidat is null ? [] : [candidat], null);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException)
|
||||
{
|
||||
logger.LogWarning(ex, "OpenLibrary injoignable ou illisible pour l'ISBN {Isbn}", isbn);
|
||||
return ([], $"OpenLibrary injoignable ({ex.GetType().Name}) pour {isbn}.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> ResoudreAuteursAsync(OpenLibraryEdition edition, CancellationToken ct)
|
||||
{
|
||||
var cles = OpenLibraryMapper.ClesAuteurs(edition);
|
||||
|
||||
if (cles.Count == 0)
|
||||
{
|
||||
// Repli : certaines éditions n'ont pas de champ `authors`, seule l'œuvre en porte.
|
||||
var cleOeuvre = OpenLibraryMapper.CleOeuvre(edition);
|
||||
if (cleOeuvre is not null)
|
||||
{
|
||||
var oeuvre = await LireAsync<OpenLibraryWork>(cleOeuvre.TrimStart('/') + ".json", ct);
|
||||
if (oeuvre is not null)
|
||||
{
|
||||
cles = OpenLibraryMapper.ClesAuteurs(oeuvre);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var noms = new List<string>();
|
||||
foreach (var cle in cles)
|
||||
{
|
||||
var auteur = await LireAsync<OpenLibraryAuthor>(cle.TrimStart('/') + ".json", ct);
|
||||
var nom = auteur is null ? null : OpenLibraryMapper.Nom(auteur);
|
||||
if (nom is not null)
|
||||
{
|
||||
noms.Add(nom);
|
||||
}
|
||||
}
|
||||
|
||||
return noms;
|
||||
}
|
||||
|
||||
private async Task<T?> LireAsync<T>(string chemin, CancellationToken ct) where T : class
|
||||
{
|
||||
using var reponse = await http.GetAsync(chemin, ct);
|
||||
if (reponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!reponse.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogWarning("OpenLibrary a répondu {Code} pour {Chemin}", (int)reponse.StatusCode, chemin);
|
||||
return null;
|
||||
}
|
||||
|
||||
await using var flux = await reponse.Content.ReadAsStreamAsync(ct);
|
||||
return await JsonSerializer.DeserializeAsync<T>(flux, Json, ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user