using System.Net; using System.Text.Json; using MaBibli.Shared.Dtos; namespace MaBibli.Api.Services.Isbn; public interface IOpenLibraryClient { /// /// Cherche l'édition correspondant à un ISBN chez OpenLibrary, auteurs résolus. /// Renvoie une liste vide si l'ISBN est inconnu ou la source injoignable. /// Task<(IReadOnlyList Candidats, string? Avertissement)> RechercherAsync( string isbn, string? urlCouverture, CancellationToken ct = default); } /// /// Client OpenLibrary — second rideau de la cascade, pour les livres étrangers et tout ce que /// la BnF ne connaît pas. /// /// /// Bug à ne pas reproduire (observé chez BookLogr) : /isbn/{isbn}.json ne donne pas /// le nom de l'auteur mais une référence /authors/OL…A. 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). /// public sealed class OpenLibraryClient(HttpClient http, ILogger 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 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($"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> 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(cleOeuvre.TrimStart('/') + ".json", ct); if (oeuvre is not null) { cles = OpenLibraryMapper.ClesAuteurs(oeuvre); } } } var noms = new List(); foreach (var cle in cles) { var auteur = await LireAsync(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 LireAsync(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(flux, Json, ct); } }