Ajoute ISBN, tris et nouveautés d'auteur

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
LIMONIER Mathieu
2026-08-20 13:49:08 +02:00
co-authored by Copilot
parent 359f4f59c6
commit 6b8f08ef26
10 changed files with 441 additions and 13 deletions
@@ -2,6 +2,7 @@ using MaBibli.Api.Data;
using MaBibli.Api.Services.Isbn;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;
using MaBibli.Shared.Isbn;
using MaBibli.Shared.Textes;
using Microsoft.EntityFrameworkCore;
@@ -15,6 +16,9 @@ public interface IServiceBibliographie
/// </summary>
Task<BibliographieDto?> ObtenirAsync(int auteurId, string? utilisateur, CancellationToken ct = default);
Task<BibliographieDto?> ObtenirNouveautesAsync(
int auteurId, string? utilisateur, CancellationToken ct = default);
Task<bool> MasquerAsync(int auteurId, string titre, string? utilisateur, CancellationToken ct = default);
Task<bool> DemasquerAsync(int auteurId, string titre, string? utilisateur, CancellationToken ct = default);
@@ -101,6 +105,70 @@ public sealed class ServiceBibliographie(
};
}
public async Task<BibliographieDto?> ObtenirNouveautesAsync(
int auteurId, string? utilisateur, CancellationToken ct = default)
{
var auteur = await auteurs.ObtenirAsync(auteurId, ct);
if (auteur is null)
{
return null;
}
var recherche = bnf is IBnfClientNouveautes etendu
? await etendu.RechercherParAuteurNouveautesAsync(auteur.Nom, ct)
: await bnf.RechercherParAuteurAsync(auteur.Nom, ct);
var resultat = recherche.Resultat;
var possedes = await db.LivreAuteurs
.AsNoTracking()
.Where(la => la.AuteurId == auteurId)
.Select(la => new { la.LivreId, la.Livre!.Titre, la.Livre.Isbn })
.ToListAsync(ct);
var parTitre = new Dictionary<string, int>();
var parIsbn = new Dictionary<string, int>();
foreach (var livre in possedes)
{
var cle = CleOeuvre.Cle(livre.Titre);
if (cle.Length > 0)
{
parTitre.TryAdd(cle, livre.LivreId);
}
if (!string.IsNullOrWhiteSpace(livre.Isbn))
{
parIsbn.TryAdd(livre.Isbn, livre.LivreId);
}
}
var souhaits = await SouhaitsAsync(utilisateur, ct);
var masquees = await MasqueesAsync(auteurId, utilisateur, ct);
var oeuvres = Regrouper(resultat)
.Select(o => Confronter(o, parTitre, parIsbn, souhaits, masquees))
.ToList();
var seuil = AnneeSeuilNouveautes(
possedes.Select(l => (l.Titre, l.Isbn)), resultat.Notices);
// Une notice d'édition originale ne date pas la dernière édition de l'œuvre. Le
// regroupement conserve donc AnneeDerniereEdition, qui est la seule date pertinente
// pour répondre à « paru depuis ».
var nouveautes = FiltrerNouveautes(oeuvres, seuil);
return new BibliographieDto
{
Auteur = auteur,
Oeuvres = nouveautes,
NoticesAnnoncees = resultat.NombreAnnonce,
NoticesLues = resultat.NombreLu,
NoticesEcartees = resultat.EcarteesAutreAuteur + resultat.EcarteesTypeNonLivre,
Avertissement = recherche.Avertissement,
Etat = resultat.Etat,
EstNouveautes = true,
AnneeSeuilNouveautes = seuil,
SeuilNouveautesInconnu = seuil is null && possedes.Count > 0,
};
}
public async Task<bool> MasquerAsync(
int auteurId, string titre, string? utilisateur, CancellationToken ct = default)
{
@@ -222,6 +290,12 @@ public sealed class ServiceBibliographie(
{
Titre = reference.Titre,
Annee = reference.Annee,
AnneeDerniereEdition = ordonnees
.Select(n => AnneeTriable(n.Annee))
.Where(a => a is not null)
.Select(a => a!.Value)
.DefaultIfEmpty()
.Max() is var annee && annee > 0 ? annee : null,
Editeur = reference.Editeur,
Isbn = ordonnees.Select(n => n.Isbn).FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)),
NombreEditions = groupe.Count(),
@@ -258,6 +332,61 @@ public sealed class ServiceBibliographie(
};
}
public static int? AnneeSeuilNouveautes(
IEnumerable<(string Titre, string? Isbn)> possedes,
IReadOnlyList<NoticeAuteur> notices)
{
var annees = new List<int>();
foreach (var livre in possedes)
{
var correspondantes = !string.IsNullOrWhiteSpace(livre.Isbn)
? notices.Where(n => MemesIsbn(n.Isbn, livre.Isbn))
: notices.Where(n => n.CleTitre == CleOeuvre.Cle(livre.Titre));
annees.AddRange(correspondantes
.Select(n => AnneeTriable(n.Annee))
.Where(a => a is not null)
.Select(a => a!.Value));
}
return annees.Count == 0 ? null : annees.Max();
}
private static bool MemesIsbn(string? gauche, string? droite)
{
var a = IsbnUtils.Normaliser(gauche);
var b = IsbnUtils.Normaliser(droite);
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
{
return false;
}
if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return IsbnUtils.TryConvertirEnIsbn10(a, out var a10)
&& string.Equals(a10, b, StringComparison.OrdinalIgnoreCase)
|| IsbnUtils.TryConvertirEnIsbn10(b, out var b10)
&& string.Equals(b10, a, StringComparison.OrdinalIgnoreCase);
}
public static IReadOnlyList<OeuvreBibliographie> FiltrerNouveautes(
IEnumerable<OeuvreBibliographie> oeuvres, int? seuil)
{
if (seuil is null)
{
return [];
}
return oeuvres
.Where(o => o.AnneeDerniereEdition > seuil && o.ADecouvrir)
.OrderByDescending(o => o.AnneeDerniereEdition)
.ThenBy(o => o.Titre, StringComparer.CurrentCultureIgnoreCase)
.ToList();
}
/// <summary>
/// Année exploitable pour le tri, extraite d'une date BnF.
/// </summary>