Ajouter la liste d'envies personnelle, son export et la bibliographie par auteur

La liste d'envies vit dans une table séparée (LivreSouhaite) plutôt que dans un
statut de plus sur Livre : un livre souhaité n'est pas possédé, et le loger dans
Livres l'aurait fait entrer dans le catalogue, les compteurs et les prêts, au
prix d'un « et qui n'est pas souhaité » à répéter dans chaque lecture. La portée
est personnelle, comme le statut de lecture — mais ici Utilisateur est une vraie
frontière : toute lecture filtre dessus.

Bibliographie : SRU BnF interrogé par bib.author, vérifié le 2026-08-18. Deux
filtres mesurés sur des réponses réelles sont indispensables — le type de
document (l'index mêle livres audio, jeux et spectacles) et surtout l'auteur
réel de la notice, « all » rapprochant les mots sur l'ensemble des auteurs :
« Émile Zola » remonte sinon toute l'œuvre de sa fille Denise Le Blond-Zola.
Les rééditions sont regroupées par clé d'œuvre (183 notices Werber -> 51
œuvres). Le rapprochement avec l'étagère se fait par titre, pas par ISBN, qui
désigne une édition et non une œuvre ; ses limites sont dites à l'écran.

Export en deux formats, tous deux du texte sans dépendance : .txt groupé par
auteur pour la librairie, .csv à séparateur point-virgule et BOM pour le
tableur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-18 13:59:06 +02:00
co-authored by Claude Opus 5
parent 3a5a3a7763
commit 8b8fe3be6e
27 changed files with 3939 additions and 0 deletions
+31
View File
@@ -17,6 +17,13 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
public DbSet<Pret> Prets => Set<Pret>();
/// <summary>
/// Liste d'envies, <b>personnelle</b>. Table séparée des <see cref="Livres"/> : un livre
/// souhaité n'est pas un livre possédé, et ne doit apparaître ni au catalogue, ni dans les
/// compteurs, ni dans les prêts.
/// </summary>
public DbSet<LivreSouhaite> LivresSouhaites => Set<LivreSouhaite>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
@@ -90,6 +97,30 @@ public class MaBibliDbContext(DbContextOptions<MaBibliDbContext> options) : DbCo
refus.HasIndex(r => new { r.AuteurAId, r.AuteurBId }).IsUnique();
});
modelBuilder.Entity<LivreSouhaite>(souhait =>
{
souhait.Property(s => s.Utilisateur).IsRequired();
souhait.Property(s => s.Titre).IsRequired();
souhait.Property(s => s.TitreNormalise).IsRequired();
// Jamais nullable : SQLite tient deux NULL pour distincts, et l'unicité ci-dessous
// laisserait alors passer autant de doublons qu'on veut sur les envies sans auteur.
souhait.Property(s => s.AuteurNormalise).IsRequired().HasDefaultValue(string.Empty);
// Toute lecture part de là : la liste est personnelle, on la parcourt par personne.
souhait.HasIndex(s => s.Utilisateur, "IX_LivresSouhaites_Utilisateur");
// Une même œuvre ne s'ajoute qu'une fois par personne — l'écran de bibliographie
// rend le double clic facile. L'auteur fait partie de la clé : deux auteurs peuvent
// avoir publié deux « Œuvres » différentes, et l'un ne doit pas bloquer l'autre.
// Deux personnes peuvent en revanche souhaiter le même livre, d'où l'utilisateur
// en tête de l'index.
souhait.HasIndex(
s => new { s.Utilisateur, s.TitreNormalise, s.AuteurNormalise },
"IX_LivresSouhaites_Utilisateur_Oeuvre")
.IsUnique();
});
modelBuilder.Entity<Pret>(pret =>
{
pret.Property(p => p.Emprunteur).IsRequired();
@@ -0,0 +1,301 @@
// <auto-generated />
using System;
using MaBibli.Api.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MaBibli.Api.Data.Migrations
{
[DbContext(typeof(MaBibliDbContext))]
[Migration("20260818052834_ListeDEnvies")]
partial class ListeDEnvies
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.11");
modelBuilder.Entity("MaBibli.Shared.Entites.Auteur", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CleRegroupement")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Nom")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("NomNormalise")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CleRegroupement")
.IsUnique();
b.HasIndex("NomNormalise");
b.ToTable("Auteurs");
});
modelBuilder.Entity("MaBibli.Shared.Entites.Livre", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AjoutePar")
.HasColumnType("TEXT");
b.Property<string>("CoverUrl")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAjout")
.HasColumnType("TEXT");
b.Property<string>("Editeur")
.HasColumnType("TEXT");
b.Property<int>("Format")
.HasColumnType("INTEGER");
b.Property<string>("Isbn")
.HasColumnType("TEXT");
b.Property<string>("Titre")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TitreNormalise")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Isbn");
b.HasIndex("TitreNormalise");
b.ToTable("Livres");
});
modelBuilder.Entity("MaBibli.Shared.Entites.LivreAuteur", b =>
{
b.Property<int>("LivreId")
.HasColumnType("INTEGER");
b.Property<int>("AuteurId")
.HasColumnType("INTEGER");
b.Property<int>("Position")
.HasColumnType("INTEGER");
b.HasKey("LivreId", "AuteurId");
b.HasIndex("AuteurId");
b.ToTable("LivreAuteurs");
});
modelBuilder.Entity("MaBibli.Shared.Entites.LivreSouhaite", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Annee")
.HasColumnType("TEXT");
b.Property<string>("Auteur")
.HasColumnType("TEXT");
b.Property<string>("AuteurNormalise")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("");
b.Property<string>("CoverUrl")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAjout")
.HasColumnType("TEXT");
b.Property<string>("Editeur")
.HasColumnType("TEXT");
b.Property<string>("Isbn")
.HasColumnType("TEXT");
b.Property<string>("Note")
.HasColumnType("TEXT");
b.Property<string>("Titre")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TitreNormalise")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Utilisateur")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "Utilisateur" }, "IX_LivresSouhaites_Utilisateur");
b.HasIndex(new[] { "Utilisateur", "TitreNormalise", "AuteurNormalise" }, "IX_LivresSouhaites_Utilisateur_Oeuvre")
.IsUnique();
b.ToTable("LivresSouhaites");
});
modelBuilder.Entity("MaBibli.Shared.Entites.Pret", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DatePret")
.HasColumnType("TEXT");
b.Property<DateTime?>("DateRetour")
.HasColumnType("TEXT");
b.Property<string>("Emprunteur")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("LivreId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex(new[] { "LivreId" }, "IX_Prets_LivreId");
b.HasIndex(new[] { "LivreId" }, "IX_Prets_LivreId_EnCours")
.IsUnique()
.HasFilter("\"DateRetour\" IS NULL");
b.ToTable("Prets");
});
modelBuilder.Entity("MaBibli.Shared.Entites.RapprochementRefuse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AuteurAId")
.HasColumnType("INTEGER");
b.Property<int>("AuteurBId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("AuteurAId", "AuteurBId")
.IsUnique();
b.ToTable("RapprochementsRefuses");
});
modelBuilder.Entity("MaBibli.Shared.Entites.StatutLecture", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateMaj")
.HasColumnType("TEXT");
b.Property<int>("LivreId")
.HasColumnType("INTEGER");
b.Property<int>("Statut")
.HasColumnType("INTEGER");
b.Property<string>("Utilisateur")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Utilisateur");
b.HasIndex("LivreId", "Utilisateur")
.IsUnique();
b.ToTable("StatutsLecture");
});
modelBuilder.Entity("MaBibli.Shared.Entites.LivreAuteur", b =>
{
b.HasOne("MaBibli.Shared.Entites.Auteur", "Auteur")
.WithMany("Livres")
.HasForeignKey("AuteurId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
.WithMany("Auteurs")
.HasForeignKey("LivreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Auteur");
b.Navigation("Livre");
});
modelBuilder.Entity("MaBibli.Shared.Entites.Pret", b =>
{
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
.WithMany("Prets")
.HasForeignKey("LivreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Livre");
});
modelBuilder.Entity("MaBibli.Shared.Entites.StatutLecture", b =>
{
b.HasOne("MaBibli.Shared.Entites.Livre", "Livre")
.WithMany("Statuts")
.HasForeignKey("LivreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Livre");
});
modelBuilder.Entity("MaBibli.Shared.Entites.Auteur", b =>
{
b.Navigation("Livres");
});
modelBuilder.Entity("MaBibli.Shared.Entites.Livre", b =>
{
b.Navigation("Auteurs");
b.Navigation("Prets");
b.Navigation("Statuts");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,56 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MaBibli.Api.Data.Migrations
{
/// <inheritdoc />
public partial class ListeDEnvies : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "LivresSouhaites",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Utilisateur = table.Column<string>(type: "TEXT", nullable: false),
Titre = table.Column<string>(type: "TEXT", nullable: false),
TitreNormalise = table.Column<string>(type: "TEXT", nullable: false),
Auteur = table.Column<string>(type: "TEXT", nullable: true),
AuteurNormalise = table.Column<string>(type: "TEXT", nullable: false, defaultValue: ""),
Editeur = table.Column<string>(type: "TEXT", nullable: true),
Annee = table.Column<string>(type: "TEXT", nullable: true),
Isbn = table.Column<string>(type: "TEXT", nullable: true),
CoverUrl = table.Column<string>(type: "TEXT", nullable: true),
Note = table.Column<string>(type: "TEXT", nullable: true),
DateAjout = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_LivresSouhaites", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_LivresSouhaites_Utilisateur",
table: "LivresSouhaites",
column: "Utilisateur");
migrationBuilder.CreateIndex(
name: "IX_LivresSouhaites_Utilisateur_Oeuvre",
table: "LivresSouhaites",
columns: new[] { "Utilisateur", "TitreNormalise", "AuteurNormalise" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "LivresSouhaites");
}
}
}
@@ -104,6 +104,61 @@ namespace MaBibli.Api.Data.Migrations
b.ToTable("LivreAuteurs");
});
modelBuilder.Entity("MaBibli.Shared.Entites.LivreSouhaite", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Annee")
.HasColumnType("TEXT");
b.Property<string>("Auteur")
.HasColumnType("TEXT");
b.Property<string>("AuteurNormalise")
.IsRequired()
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue("");
b.Property<string>("CoverUrl")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAjout")
.HasColumnType("TEXT");
b.Property<string>("Editeur")
.HasColumnType("TEXT");
b.Property<string>("Isbn")
.HasColumnType("TEXT");
b.Property<string>("Note")
.HasColumnType("TEXT");
b.Property<string>("Titre")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("TitreNormalise")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Utilisateur")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "Utilisateur" }, "IX_LivresSouhaites_Utilisateur");
b.HasIndex(new[] { "Utilisateur", "TitreNormalise", "AuteurNormalise" }, "IX_LivresSouhaites_Utilisateur_Oeuvre")
.IsUnique();
b.ToTable("LivresSouhaites");
});
modelBuilder.Entity("MaBibli.Shared.Entites.Pret", b =>
{
b.Property<int>("Id")
+162
View File
@@ -0,0 +1,162 @@
using System.Text;
using MaBibli.Api.Services.Catalogue;
using MaBibli.Api.Services.Identite;
using MaBibli.Api.Services.Souhaits;
using MaBibli.Shared.Dtos;
namespace MaBibli.Api.Endpoints;
public static class SouhaitsEndpoints
{
/// <summary>
/// La liste d'envies et son export.
/// </summary>
/// <remarks>
/// <b>Aucun point d'entrée n'accepte de nom d'utilisateur</b>, et il ne doit jamais y en
/// avoir : la liste rendue est toujours celle de l'appelant, identifié par les en-têtes
/// SSOwat. C'est la même règle que pour les statuts de lecture, et elle est ici encore plus
/// nette — le sens de la fonctionnalité est de préparer un cadeau sans que l'autre le voie.
/// </remarks>
public static IEndpointRouteBuilder MapSouhaitsEndpoints(this IEndpointRouteBuilder routes)
{
var groupe = routes.MapGroup("/api/souhaits").WithTags("Liste d'envies");
groupe.MapGet("/", async (
IServiceSouhaits service,
IFournisseurUtilisateur utilisateurs,
CancellationToken ct) =>
Results.Ok(await service.ListerAsync(utilisateurs.Obtenir().Identifiant, ct)))
.WithName("ListerSouhaits")
.WithSummary("La liste d'envies de l'appelant, groupée par auteur.")
.WithDescription(
"Personnelle : deux membres du foyer obtiennent deux listes différentes. "
+ "Sans identité, la liste est vide.")
.Produces<IReadOnlyList<SouhaitDto>>();
groupe.MapPost("/", async (
EnregistrementSouhait saisie,
IServiceSouhaits service,
IFournisseurUtilisateur utilisateurs,
CancellationToken ct) =>
{
var resultat = await service.AjouterAsync(
saisie, utilisateurs.Obtenir().Identifiant, ct);
return resultat.Erreur is not null
? Results.BadRequest(new { erreur = resultat.Erreur })
: Results.Created($"/api/souhaits/{resultat.Souhait!.Id}", resultat.Souhait);
})
.WithName("AjouterSouhait")
.WithSummary("Ajoute un livre à la liste d'envies de l'appelant.")
.Produces<SouhaitDto>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest);
groupe.MapDelete("/{id:int}", async (
int id,
IServiceSouhaits service,
IFournisseurUtilisateur utilisateurs,
CancellationToken ct) =>
{
var supprime = await service.SupprimerAsync(
id, utilisateurs.Obtenir().Identifiant, ct);
return supprime ? Results.NoContent() : Results.NotFound();
})
.WithName("SupprimerSouhait")
.WithSummary("Retire un livre de la liste d'envies de l'appelant.")
.WithDescription(
"404 si l'envie n'existe pas OU appartient à quelqu'un d'autre : les deux cas "
+ "sont volontairement indiscernables.")
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound);
// Deux formats parce que les deux usages décrits dans IDEES.md diffèrent : le .txt
// s'emporte en librairie et se lit tel quel, le .csv s'ouvre dans un tableur pour se
// répartir des achats. Ce sont deux fichiers texte : aucune dépendance ni pour les
// produire, ni pour les lire.
groupe.MapGet("/export.txt", (
IServiceSouhaits service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) =>
ExporterAsync(FormatExportSouhaits.Texte, service, utilisateurs, ct))
.WithName("ExporterSouhaitsTexte")
.WithSummary("Liste d'envies en texte lisible, groupée par auteur.")
.ExcludeFromDescription();
groupe.MapGet("/export.csv", (
IServiceSouhaits service, IFournisseurUtilisateur utilisateurs, CancellationToken ct) =>
ExporterAsync(FormatExportSouhaits.Csv, service, utilisateurs, ct))
.WithName("ExporterSouhaitsCsv")
.WithSummary("Liste d'envies en CSV, ouvrable dans un tableur.")
.ExcludeFromDescription();
return routes;
}
/// <summary>
/// Produit le fichier d'export et le renvoie en <b>téléchargement</b>.
/// </summary>
/// <remarks>
/// Le nom de fichier passé à <c>Results.File</c> pose un <c>Content-Disposition:
/// attachment</c> : le navigateur enregistre au lieu d'afficher, y compris sur mobile où un
/// .txt s'ouvrirait sinon dans l'onglet. C'est ce qui rend l'export réellement
/// « emportable ».
/// <para>
/// Le fichier est produit ici plutôt que côté client : le client Blazor n'a pas la liste
/// complète sous la main, et un export généré par le serveur reste correct même si l'écran
/// affiche une vue filtrée.
/// </para>
/// </remarks>
private static async Task<IResult> ExporterAsync(
FormatExportSouhaits format,
IServiceSouhaits service,
IFournisseurUtilisateur utilisateurs,
CancellationToken ct)
{
var utilisateur = utilisateurs.Obtenir();
var souhaits = await service.ListerAsync(utilisateur.Identifiant, ct);
var maintenant = DateTime.Now;
var contenu = ExportSouhaits.Produire(
format, souhaits, utilisateur.Affichage, maintenant);
// Le BOM n'est mis QUE sur le CSV, où il est indispensable : sans lui, Excel lit le
// fichier en codage hérité et affiche « Émile Zola ». Le .txt s'en passe — il est fait
// pour être lu tel quel, et certains lecteurs simples affichent le BOM comme un
// caractère parasite en tête de la première ligne.
var corps = Encoding.UTF8.GetBytes(contenu);
var octets = format == FormatExportSouhaits.Csv
? [.. ExportSouhaits.Bom, .. corps]
: corps;
return Results.File(
octets,
ExportSouhaits.TypeMime(format),
ExportSouhaits.NomFichier(format, maintenant));
}
/// <summary>Bibliographie d'un auteur du catalogue, d'après la BnF.</summary>
public static IEndpointRouteBuilder MapBibliographieEndpoints(this IEndpointRouteBuilder routes)
{
routes.MapGet("/api/auteurs/{id:int}/bibliographie", async (
int id,
IServiceBibliographie service,
IFournisseurUtilisateur utilisateurs,
CancellationToken ct) =>
{
var bibliographie = await service.ObtenirAsync(
id, utilisateurs.Obtenir().Identifiant, ct);
return bibliographie is null ? Results.NotFound() : Results.Ok(bibliographie);
})
.WithName("BibliographieAuteur")
.WithTags("Auteurs")
.WithSummary("Ce que la BnF connaît de cet auteur, confronté à l'étagère.")
.WithDescription(
"« Possédé » vaut pour tout le foyer, « souhaité » seulement pour l'appelant. "
+ "Le rapprochement se fait par titre normalisé : il peut manquer un livre "
+ "possédé sous un autre titre, d'où le fait qu'aucune ligne ne soit masquée.")
.Produces<BibliographieDto>()
.Produces(StatusCodes.Status404NotFound);
return routes;
}
}
+5
View File
@@ -4,6 +4,7 @@ using MaBibli.Api.Services.Catalogue;
using MaBibli.Api.Services.Identite;
using MaBibli.Api.Services.Isbn;
using MaBibli.Api.Services.Prets;
using MaBibli.Api.Services.Souhaits;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
@@ -16,6 +17,8 @@ builder.Services.AddDbContext<MaBibliDbContext>(options =>
builder.Services.AddScoped<IServiceAuteurs, ServiceAuteurs>();
builder.Services.AddScoped<IServiceCatalogue, ServiceCatalogue>();
builder.Services.AddScoped<IServicePrets, ServicePrets>();
builder.Services.AddScoped<IServiceSouhaits, ServiceSouhaits>();
builder.Services.AddScoped<IServiceBibliographie, ServiceBibliographie>();
// Identité : lue dans les en-têtes injectés par SSOwat, avec repli sur un utilisateur simulé
// configuré en développement (il n'y a pas de portail YunoHost en local).
@@ -73,6 +76,8 @@ app.MapIsbnEndpoints();
app.MapLivresEndpoints();
app.MapAuteursEndpoints();
app.MapPretsEndpoints();
app.MapSouhaitsEndpoints();
app.MapBibliographieEndpoints();
app.MapIdentiteEndpoints();
app.MapFallbackToFile("index.html");
@@ -0,0 +1,208 @@
using MaBibli.Api.Data;
using MaBibli.Api.Services.Isbn;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Textes;
using Microsoft.EntityFrameworkCore;
namespace MaBibli.Api.Services.Catalogue;
public interface IServiceBibliographie
{
/// <summary>
/// Bibliographie d'un auteur du catalogue, confrontée à ce que l'on possède et à ce que
/// l'appelant souhaite déjà. <c>null</c> si l'auteur n'existe pas.
/// </summary>
Task<BibliographieDto?> ObtenirAsync(int auteurId, string? utilisateur, CancellationToken ct = default);
}
/// <summary>
/// Bibliographie d'un auteur : ce que la BnF connaît de lui, mis en regard de l'étagère.
/// </summary>
/// <remarks>
/// Trois sources se rencontrent ici, et elles n'ont pas la même portée :
/// <list type="bullet">
/// <item>la <b>BnF</b>, source externe, ni possédée ni personnelle ;</item>
/// <item>le <b>catalogue</b>, commun au foyer — « possédé » veut dire possédé par tout le
/// monde ;</item>
/// <item>la <b>liste d'envies</b>, personnelle — « déjà souhaité » ne vaut que pour l'appelant.</item>
/// </list>
/// Deux membres du foyer voient donc la même colonne « possédé » et deux colonnes « souhaité »
/// différentes. C'est voulu, et c'est ce qui permet d'offrir un livre à l'autre.
/// </remarks>
public sealed class ServiceBibliographie(
MaBibliDbContext db, IBnfClient bnf, IServiceAuteurs auteurs) : IServiceBibliographie
{
public async Task<BibliographieDto?> ObtenirAsync(
int auteurId, string? utilisateur, CancellationToken ct = default)
{
var auteur = await auteurs.ObtenirAsync(auteurId, ct);
if (auteur is null)
{
return null;
}
var (resultat, avertissement) = await bnf.RechercherParAuteurAsync(auteur.Nom, ct);
// Ce que l'on possède DE CET AUTEUR. On ne compare pas à tout le catalogue : deux auteurs
// peuvent avoir écrit deux livres homonymes, et griser l'un à cause de l'autre serait un
// faux positif — bien plus gênant qu'un oubli, puisqu'il détourne d'un achat justifié.
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);
}
// L'ISBN ne sert qu'en confirmation : il ne rapproche que la même édition exacte,
// alors que le titre rapproche l'œuvre. Il rattrape en revanche le cas où le titre
// saisi à la main diverge de celui de la BnF.
if (!string.IsNullOrWhiteSpace(livre.Isbn))
{
parIsbn.TryAdd(livre.Isbn, livre.LivreId);
}
}
var souhaits = await SouhaitsAsync(utilisateur, ct);
var oeuvres = Regrouper(resultat)
.Select(o => Confronter(o, parTitre, parIsbn, souhaits))
.OrderByDescending(o => AnneeTriable(o.Annee))
.ThenBy(o => o.Titre, StringComparer.CurrentCultureIgnoreCase)
.ToList();
return new BibliographieDto
{
Auteur = auteur,
Oeuvres = oeuvres,
NoticesAnnoncees = resultat.NombreAnnonce,
NoticesLues = resultat.NombreLu,
NoticesEcartees = resultat.EcarteesAutreAuteur + resultat.EcarteesTypeNonLivre,
Avertissement = avertissement,
};
}
/// <summary>Clés d'œuvre déjà souhaitées par l'appelant, avec l'identifiant de l'envie.</summary>
private async Task<Dictionary<string, int>> SouhaitsAsync(string? utilisateur, CancellationToken ct)
{
if (utilisateur is null)
{
return [];
}
var lignes = await db.LivresSouhaites
.AsNoTracking()
.Where(s => s.Utilisateur == utilisateur)
.Select(s => new { s.Id, s.TitreNormalise })
.ToListAsync(ct);
var index = new Dictionary<string, int>();
foreach (var ligne in lignes)
{
index.TryAdd(ligne.TitreNormalise, ligne.Id);
}
return index;
}
/// <summary>
/// Réunit les rééditions sous une seule œuvre.
/// </summary>
/// <remarks>
/// C'est le cœur de l'utilisabilité de l'écran : sans regroupement, Bernard Werber occupe
/// 183 lignes dont « Les fourmis » onze fois. Après regroupement, 51 lignes.
/// <para>
/// Le représentant retenu est la notice la <b>plus ancienne</b> : c'est l'édition originale,
/// donc l'année qui situe l'œuvre dans la carrière de l'auteur, et non celle du dernier
/// retirage en poche. Le titre et l'éditeur suivent la même notice pour rester cohérents,
/// mais l'ISBN est pris sur <b>n'importe quelle</b> notice qui en porte un : les éditions
/// anciennes n'en ont souvent aucun, et un ISBN de réédition reste utile pour commander.
/// </para>
/// </remarks>
public static IEnumerable<OeuvreBibliographie> Regrouper(ResultatBibliographie resultat) =>
resultat.Notices
.Where(n => n.CleTitre.Length > 0)
.GroupBy(n => n.CleTitre)
.Select(groupe =>
{
var ordonnees = groupe
.OrderBy(n => AnneeTriable(n.Annee) ?? int.MaxValue)
.ToList();
var reference = ordonnees[0];
return new OeuvreBibliographie
{
Titre = reference.Titre,
Annee = reference.Annee,
Editeur = reference.Editeur,
Isbn = ordonnees.Select(n => n.Isbn).FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)),
NombreEditions = groupe.Count(),
};
});
private static OeuvreBibliographie Confronter(
OeuvreBibliographie oeuvre,
IReadOnlyDictionary<string, int> parTitre,
IReadOnlyDictionary<string, int> parIsbn,
IReadOnlyDictionary<string, int> souhaits)
{
var cle = CleOeuvre.Cle(oeuvre.Titre);
int? livreId = parTitre.TryGetValue(cle, out var idTitre) ? idTitre : null;
if (livreId is null
&& !string.IsNullOrWhiteSpace(oeuvre.Isbn)
&& parIsbn.TryGetValue(oeuvre.Isbn, out var idIsbn))
{
livreId = idIsbn;
}
int? souhaitId = souhaits.TryGetValue(cle, out var idSouhait) ? idSouhait : null;
return oeuvre with
{
Possede = livreId is not null,
LivreId = livreId,
Souhaite = souhaitId is not null,
SouhaitId = souhaitId,
};
}
/// <summary>
/// Année exploitable pour le tri, extraite d'une date BnF.
/// </summary>
/// <remarks>
/// <c>dc:date</c> n'est pas un nombre : on rencontre « 2012 », mais aussi « [DL 2012] »,
/// « cop. 1991 » ou « 19..? ». On prend la première suite de quatre chiffres, et
/// <c>null</c> s'il n'y en a pas — une date illisible ne doit pas devenir l'an zéro et
/// remonter en tête de liste.
/// </remarks>
public static int? AnneeTriable(string? date)
{
if (string.IsNullOrWhiteSpace(date))
{
return null;
}
for (var i = 0; i + 4 <= date.Length; i++)
{
var tranche = date.AsSpan(i, 4);
if (int.TryParse(tranche, out var annee) && annee is >= 1000 and <= 2999)
{
return annee;
}
}
return null;
}
}
@@ -0,0 +1,209 @@
using System.Xml.Linq;
using MaBibli.Shared.Textes;
namespace MaBibli.Api.Services.Isbn;
/// <summary>Une notice BnF réduite à ce qui sert à bâtir une bibliographie.</summary>
public sealed record NoticeAuteur
{
public required string Titre { get; init; }
/// <summary>Clé d'œuvre du titre : c'est elle qui réunit les rééditions.</summary>
public required string CleTitre { get; init; }
public string? Annee { get; init; }
public string? Editeur { get; init; }
public string? Isbn { get; init; }
/// <summary>Auteurs nettoyés de la notice, tous rôles confondus.</summary>
public IReadOnlyList<string> Auteurs { get; init; } = [];
}
/// <summary>Résultat de la lecture d'une réponse SRU « par auteur ».</summary>
public sealed record ResultatBibliographie
{
public IReadOnlyList<NoticeAuteur> Notices { get; init; } = [];
/// <summary>Nombre de notices annoncé par la BnF, tous types confondus.</summary>
public int NombreAnnonce { get; init; }
/// <summary>Notices lues dans cette réponse, avant tout filtrage.</summary>
public int NombreLu { get; init; }
/// <summary>Notices écartées parce qu'elles ne décrivent pas un livre (disque, jeu, film…).</summary>
public int EcarteesTypeNonLivre { get; init; }
/// <summary>Notices écartées parce qu'aucun de leurs auteurs n'est celui demandé.</summary>
public int EcarteesAutreAuteur { get; init; }
}
/// <summary>
/// Lecture d'une réponse SRU de la BnF interrogée <b>par auteur</b>
/// (<c>bib.author all "…"</c>, <c>recordSchema=dublincore</c>).
/// </summary>
/// <remarks>
/// Fonction pure, testée sur des réponses réelles enregistrées dans <c>Fixtures/</c>.
/// <para>
/// <b>Deux filtres, mesurés sur des réponses réelles le 2026-08-18, sont indispensables.</b>
/// Sans eux l'écran de bibliographie ment à l'utilisateur.
/// </para>
/// <list type="number">
/// <item>
/// <b>Le type de document.</b> L'index auteur ne distingue pas un livre d'un livre audio, d'un
/// jeu vidéo ou d'un spectacle. Sur 200 notices de Bernard Werber : 180 imprimés, mais aussi
/// 13 enregistrements sonores, 3 images animées, 2 jeux multimédias et 1 spectacle. Une liste
/// d'envies de livres n'a pas à proposer d'acheter un jeu.
/// </item>
/// <item>
/// <b>L'auteur réel de la notice.</b> C'est le filtre <b>critique</b>, et il n'est pas
/// théorique. <c>bib.author all "Émile Zola"</c> rapproche les mots « émile » et « zola » sur
/// l'<i>ensemble</i> des auteurs d'une notice : un livre signé d'un « Émile » quelconque et d'un
/// « Zola » quelconque remonte donc aussi. Mesuré sur 200 notices d'« Émile Zola » : <b>67 sont
/// écartées</b>, dont toute l'œuvre de sa fille Denise Le Blond-Zola (« Émile Zola raconté par
/// sa fille », « Zola et Cézanne »), qui parle de lui sans qu'il l'ait écrite. Les proposer à
/// l'achat comme du Zola serait faux.
/// </item>
/// </list>
/// <para>
/// Ce second filtre a un <b>coût assumé</b> : il écarte aussi les adaptations dont l'auteur n'est
/// pas le scénariste — les bandes dessinées tirées de Werber sont signées Corbeyran, et
/// disparaissent (17 notices sur 200). C'est le bon compromis : afficher l'œuvre d'un tiers sous
/// le nom de l'auteur est une erreur visible et gênante, en omettre une adaptation ne l'est pas.
/// </para>
/// </remarks>
public static class BnfBibliographieParser
{
private static readonly XNamespace Srw = "http://www.loc.gov/zing/srw/";
private static readonly XNamespace Dc = "http://purl.org/dc/elements/1.1/";
private static readonly XNamespace Xml = "http://www.w3.org/XML/1998/namespace";
/// <summary>
/// Types <c>dc:type</c> (en français) considérés comme des livres.
/// </summary>
/// <remarks>
/// Liste <b>positive</b> et non liste d'exclusion : les types inconnus sont écartés. Une
/// bibliographie tronquée vaut mieux qu'une bibliographie qui propose un DVD.
/// </remarks>
private static readonly HashSet<string> TypesLivre =
[
NormalisationTexte.Normaliser("texte imprimé"),
NormalisationTexte.Normaliser("ressource électronique"),
];
/// <summary>Préfixe des <c>dc:identifier</c> qui portent un ISBN : « ISBN 2226086366 ».</summary>
private const string PrefixeIsbn = "ISBN ";
/// <summary>
/// Convertit une réponse SRU en notices exploitables.
/// </summary>
/// <param name="xml">Corps de la réponse SRU.</param>
/// <param name="auteurDemande">
/// Nom de l'auteur dont on veut la bibliographie. Chaque notice doit compter cet auteur
/// parmi les siens, au sens de <see cref="RapprochementAuteurs"/> — la même règle que celle
/// qui regroupe les auteurs du catalogue, donc tolérante à la casse, aux accents, à l'ordre
/// « Nom, Prénom » et aux initiales.
/// </param>
public static ResultatBibliographie Parser(string xml, string auteurDemande)
{
var doc = XDocument.Parse(xml);
var annonce = doc.Descendants(Srw + "numberOfRecords").FirstOrDefault()?.Value;
var notices = new List<NoticeAuteur>();
int lues = 0, horsType = 0, autreAuteur = 0;
foreach (var record in doc.Descendants(Srw + "record"))
{
var data = record.Element(Srw + "recordData");
if (data is null)
{
continue;
}
lues++;
if (!EstUnLivre(data))
{
horsType++;
continue;
}
var auteurs = Tous(data, "creator")
.Select(NettoyageIsbd.Auteur)
.Where(a => !string.IsNullOrWhiteSpace(a))
.Select(a => a!)
.Distinct()
.ToList();
if (!auteurs.Any(a => RapprochementAuteurs.SontLeMemeAuteur(a, auteurDemande)))
{
autreAuteur++;
continue;
}
var titre = NettoyageIsbd.Titre(Premier(data, "title"));
titre = NettoyageIsbd.RetirerAuteurEnFin(titre, auteurs);
if (titre is null)
{
continue;
}
notices.Add(new NoticeAuteur
{
Titre = titre,
CleTitre = CleOeuvre.Cle(titre),
Annee = Premier(data, "date"),
Editeur = NettoyageIsbd.Editeur(Premier(data, "publisher")),
Isbn = Isbn(data),
Auteurs = auteurs,
});
}
return new ResultatBibliographie
{
Notices = notices,
NombreAnnonce = int.TryParse(annonce, out var n) ? n : lues,
NombreLu = lues,
EcarteesTypeNonLivre = horsType,
EcarteesAutreAuteur = autreAuteur,
};
}
/// <summary>
/// La notice décrit-elle un livre ?
/// </summary>
/// <remarks>
/// Seuls les <c>dc:type</c> en français sont consultés : les variantes anglaises sont plus
/// grossières (« text » couvre aussi bien un imprimé qu'un enregistrement transcrit).
/// </remarks>
private static bool EstUnLivre(XElement data) =>
data.Descendants(Dc + "type")
.Where(t => (string?)t.Attribute(Xml + "lang") == "fre")
.Select(t => NormalisationTexte.Normaliser(t.Value))
.Any(TypesLivre.Contains);
/// <summary>
/// ISBN de la notice, extrait des <c>dc:identifier</c>.
/// </summary>
/// <remarks>
/// Le Dublin Core BnF mélange dans ce champ l'ark du catalogue et, quand il existe, l'ISBN
/// préfixé du mot « ISBN ». Mesuré : 84 notices sur 100 en portent un, presque toujours sous
/// sa forme à 10 chiffres — la BnF indexant l'ISBN tel qu'imprimé (voir CLAUDE.md).
/// </remarks>
private static string? Isbn(XElement data) =>
Tous(data, "identifier")
.Where(v => v.StartsWith(PrefixeIsbn, StringComparison.OrdinalIgnoreCase))
.Select(v => v[PrefixeIsbn.Length..].Trim())
.FirstOrDefault(v => v.Length > 0);
private static string? Premier(XElement data, string nomLocal) =>
Tous(data, nomLocal).FirstOrDefault();
private static IEnumerable<string> Tous(XElement data, string nomLocal) =>
data.Descendants(Dc + nomLocal)
.Select(e => e.Value?.Trim())
.Where(v => !string.IsNullOrEmpty(v))
.Select(v => v!);
}
+106
View File
@@ -11,6 +11,16 @@ public interface IBnfClient
/// </summary>
Task<(IReadOnlyList<CandidatLivre> Candidats, string? Avertissement)> RechercherAsync(
string isbn, string? urlCouverture, CancellationToken ct = default);
/// <summary>
/// Interroge le SRU <b>par auteur</b> pour bâtir une bibliographie.
/// </summary>
/// <remarks>
/// Renvoie un résultat vide et un <c>Avertissement</c> si la BnF est injoignable : comme
/// pour le lookup ISBN, une source indisponible ne doit pas faire échouer l'écran.
/// </remarks>
Task<(ResultatBibliographie Resultat, string? Avertissement)> RechercherParAuteurAsync(
string auteur, CancellationToken ct = default);
}
/// <summary>
@@ -59,4 +69,100 @@ public sealed class BnfClient(HttpClient http, ILogger<BnfClient> logger) : IBnf
return ([], $"Réponse BnF illisible pour {isbn}.");
}
}
/// <summary>
/// Notices lues par page, et nombre de pages : au plus <b>200 notices</b>.
/// </summary>
/// <remarks>
/// Ce plafond est un arbitrage, pas une limite de l'API. Mesuré le 2026-08-18 : une requête
/// coûte ~0,85 s, et les deux pages sont donc lancées <b>en parallèle</b>. Aller au-delà
/// serait long pour un gain nul dans la plupart des cas — Bernard Werber tient en 203
/// notices, Amélie Nothomb en 277 — et resterait de toute façon insuffisant pour un classique
/// très réédité : Émile Zola en compte 2692. Comme le SRU classe par pertinence, les deux
/// premières pages contiennent l'essentiel de l'œuvre ; le reste est de la réédition.
/// <para>
/// L'interface <b>doit</b> signaler que la liste est un extrait quand la BnF en annonce plus
/// (<c>BibliographieDto.Tronquee</c>) : une bibliographie partielle présentée comme complète
/// ferait croire à tort qu'un livre n'existe pas.
/// </para>
/// </remarks>
public const int NoticesParPage = 100;
public const int PagesMaximum = 2;
public async Task<(ResultatBibliographie Resultat, string? Avertissement)> RechercherParAuteurAsync(
string auteur, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(auteur))
{
return (new ResultatBibliographie(), null);
}
try
{
// Les pages sont lancées ensemble : à ~0,85 s l'unité, les enchaîner doublerait
// l'attente de l'écran pour rien.
var pages = await Task.WhenAll(
Enumerable.Range(0, PagesMaximum)
.Select(i => LirePageAsync(auteur, (i * NoticesParPage) + 1, ct)));
var lues = pages.Where(p => p is not null).Select(p => p!).ToList();
if (lues.Count == 0)
{
return (new ResultatBibliographie(), $"La BnF n'a pas répondu pour « {auteur} ».");
}
return (Fusionner(lues), null);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
logger.LogWarning(ex, "BnF injoignable pour l'auteur {Auteur}", auteur);
return (new ResultatBibliographie(), $"BnF injoignable ({ex.GetType().Name}) pour « {auteur} ».");
}
catch (System.Xml.XmlException ex)
{
logger.LogWarning(ex, "Réponse BnF illisible pour l'auteur {Auteur}", auteur);
return (new ResultatBibliographie(), $"Réponse BnF illisible pour « {auteur} ».");
}
}
private async Task<ResultatBibliographie?> LirePageAsync(
string auteur, int premiereNotice, CancellationToken ct)
{
// bib.author : index auteur du catalogue général, vérifié le 2026-08-18.
// « all » exige que tous les mots soient présents — mais sur l'ENSEMBLE des auteurs de la
// notice, d'où le post-filtre de BnfBibliographieParser.
var url = "api/SRU"
+ "?version=1.2"
+ "&operation=searchRetrieve"
+ $"&query={Uri.EscapeDataString($"bib.author all \"{auteur}\"")}"
+ "&recordSchema=dublincore"
+ $"&maximumRecords={NoticesParPage}"
+ $"&startRecord={premiereNotice}";
using var reponse = await http.GetAsync(url, ct);
if (!reponse.IsSuccessStatusCode)
{
logger.LogWarning(
"BnF a répondu {Code} pour l'auteur {Auteur} (notice {Debut})",
(int)reponse.StatusCode, auteur, premiereNotice);
return null;
}
var xml = await reponse.Content.ReadAsStringAsync(ct);
return BnfBibliographieParser.Parser(xml, auteur);
}
/// <summary>Recolle les pages en un seul résultat, en additionnant les compteurs.</summary>
private static ResultatBibliographie Fusionner(IReadOnlyList<ResultatBibliographie> pages) =>
new()
{
Notices = pages.SelectMany(p => p.Notices).ToList(),
// Le total annoncé est le même sur chaque page : on le prend, on ne l'additionne pas.
NombreAnnonce = pages.Max(p => p.NombreAnnonce),
NombreLu = pages.Sum(p => p.NombreLu),
EcarteesTypeNonLivre = pages.Sum(p => p.EcarteesTypeNonLivre),
EcarteesAutreAuteur = pages.Sum(p => p.EcarteesAutreAuteur),
};
}
@@ -0,0 +1,177 @@
using System.Globalization;
using System.Text;
using MaBibli.Shared.Dtos;
namespace MaBibli.Api.Services.Souhaits;
/// <summary>
/// Sérialisation de la liste d'envies en fichier emportable.
/// </summary>
/// <remarks>
/// <b>Fonctions pures</b> : elles prennent une liste et rendent une chaîne, sans base ni HTTP.
/// C'est ce qui permet de vérifier le contenu exact du fichier en test plutôt que de se fier à
/// une inspection visuelle.
/// <para>
/// <b>Aucune dépendance, ni côté production ni côté lecture.</b> IDEES.md demandait un format
/// « lisible partout » : ni PDF, ni tableur binaire, ni JSON. Les deux sorties sont du texte.
/// </para>
/// </remarks>
public static class ExportSouhaits
{
/// <summary>
/// Séparateur du CSV : le <b>point-virgule</b>, pas la virgule.
/// </summary>
/// <remarks>
/// Le CSV n'a qu'un seul travail — s'ouvrir proprement dans un tableur. Or Excel en locale
/// française attend le point-virgule comme séparateur de liste : un fichier à virgules y
/// atterrit intégralement dans la colonne A, ce qui rend l'export inutilisable pour l'usage
/// visé (se répartir des achats avant un anniversaire). La collection étant francophone,
/// c'est ce cas-là qu'il faut servir. Les titres contenant un point-virgule restent corrects :
/// ils sont entre guillemets, conformément à RFC 4180.
/// </remarks>
public const char SeparateurCsv = ';';
/// <summary>
/// Marque d'ordre des octets, <b>indispensable</b> en tête du CSV.
/// </summary>
/// <remarks>
/// Sans elle, Excel lit un CSV en codage hérité et affiche « Émile Zola ». Sur une liste de
/// livres français, à peu près chaque ligne serait touchée. Le BOM ne gêne aucun autre
/// lecteur (LibreOffice, tableurs en ligne, éditeurs de texte).
/// </remarks>
public static readonly byte[] Bom = Encoding.UTF8.GetPreamble();
public static string NomFichier(FormatExportSouhaits format, DateTime maintenant) =>
$"liste-envies-{maintenant:yyyy-MM-dd}.{(format == FormatExportSouhaits.Csv ? "csv" : "txt")}";
public static string TypeMime(FormatExportSouhaits format) =>
format == FormatExportSouhaits.Csv ? "text/csv; charset=utf-8" : "text/plain; charset=utf-8";
public static string Produire(
FormatExportSouhaits format, IReadOnlyList<SouhaitDto> souhaits, string? proprietaire, DateTime maintenant) =>
format switch
{
FormatExportSouhaits.Csv => Csv(souhaits),
_ => Texte(souhaits, proprietaire, maintenant),
};
/// <summary>
/// Liste lisible telle quelle, groupée par auteur — la version qu'on emporte en librairie.
/// </summary>
/// <remarks>
/// Le groupement par auteur n'est pas cosmétique : c'est ainsi qu'une librairie est rangée.
/// Une liste à plat obligerait à parcourir tout le fichier pour chaque rayon.
/// </remarks>
public static string Texte(IReadOnlyList<SouhaitDto> souhaits, string? proprietaire, DateTime maintenant)
{
var sortie = new StringBuilder();
sortie.Append("Liste d'envies");
if (!string.IsNullOrWhiteSpace(proprietaire))
{
sortie.Append(" de ").Append(proprietaire);
}
sortie.Append(" — ").Append(maintenant.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture)).Append('\n');
sortie.Append(new string('=', 40)).Append('\n');
if (souhaits.Count == 0)
{
sortie.Append("\n(aucun livre dans la liste)\n");
return sortie.ToString();
}
sortie.Append('\n').Append(souhaits.Count).Append(souhaits.Count > 1 ? " livres" : " livre").Append('\n');
// « Auteur inconnu » est rejeté en fin de liste : ce sont les entrées les moins
// exploitables en rayon, elles ne doivent pas ouvrir le fichier.
var groupes = souhaits
.GroupBy(s => string.IsNullOrWhiteSpace(s.Auteur) ? null : s.Auteur!.Trim())
.OrderBy(g => g.Key is null)
.ThenBy(g => g.Key, StringComparer.CurrentCultureIgnoreCase);
foreach (var groupe in groupes)
{
sortie.Append('\n').Append(groupe.Key ?? "Auteur non précisé").Append('\n');
foreach (var souhait in groupe.OrderBy(s => s.Titre, StringComparer.CurrentCultureIgnoreCase))
{
sortie.Append(" - ").Append(souhait.Titre);
// Éditeur, année et ISBN sur la même ligne, entre parenthèses : c'est ce qu'on
// épelle à un libraire, et ça reste lisible sur un écran de téléphone.
var precisions = new[] { souhait.Editeur, souhait.Annee, Isbn(souhait.Isbn) }
.Where(p => !string.IsNullOrWhiteSpace(p))
.ToList();
if (precisions.Count > 0)
{
sortie.Append(" (").Append(string.Join(", ", precisions)).Append(')');
}
sortie.Append('\n');
if (!string.IsNullOrWhiteSpace(souhait.Note))
{
sortie.Append(" ").Append(souhait.Note.Trim()).Append('\n');
}
}
}
return sortie.ToString();
}
/// <summary>Tableau ouvrable dans un tableur, une envie par ligne.</summary>
public static string Csv(IReadOnlyList<SouhaitDto> souhaits)
{
var sortie = new StringBuilder();
Ligne(sortie, "Titre", "Auteur", "Éditeur", "Année", "ISBN", "Note", "Ajouté le");
foreach (var souhait in souhaits)
{
Ligne(
sortie,
souhait.Titre,
souhait.Auteur,
souhait.Editeur,
souhait.Annee,
souhait.Isbn,
souhait.Note,
souhait.DateAjout.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
return sortie.ToString();
}
private static string? Isbn(string? isbn) =>
string.IsNullOrWhiteSpace(isbn) ? null : $"ISBN {isbn}";
private static void Ligne(StringBuilder sortie, params string?[] champs)
{
sortie.AppendJoin(SeparateurCsv, champs.Select(Echapper));
// CRLF : RFC 4180 le prescrit, et c'est ce qu'attendent les tableurs sous Windows.
sortie.Append("\r\n");
}
/// <summary>
/// Échappement RFC 4180 : guillemets doublés, champ encadré dès qu'il contient un
/// séparateur, un guillemet ou un saut de ligne.
/// </summary>
/// <remarks>
/// Une note libre peut contenir n'importe quoi : sans échappement, un simple « offert ; à
/// rendre » décalerait toutes les colonnes suivantes.
/// </remarks>
private static string Echapper(string? champ)
{
var valeur = champ ?? string.Empty;
var doitEncadrer = valeur.Contains(SeparateurCsv)
|| valeur.Contains('"')
|| valeur.Contains('\n')
|| valeur.Contains('\r');
return doitEncadrer ? $"\"{valeur.Replace("\"", "\"\"")}\"" : valeur;
}
}
@@ -0,0 +1,170 @@
using MaBibli.Api.Data;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;
using MaBibli.Shared.Isbn;
using MaBibli.Shared.Textes;
using Microsoft.EntityFrameworkCore;
namespace MaBibli.Api.Services.Souhaits;
/// <summary>Issue d'un ajout d'envie : soit la ligne créée, soit un message pour l'utilisateur.</summary>
public readonly record struct ResultatSouhait(SouhaitDto? Souhait, string? Erreur)
{
public static ResultatSouhait Ok(SouhaitDto souhait) => new(souhait, null);
public static ResultatSouhait Invalide(string message) => new(null, message);
}
public interface IServiceSouhaits
{
Task<IReadOnlyList<SouhaitDto>> ListerAsync(string? utilisateur, CancellationToken ct = default);
Task<ResultatSouhait> AjouterAsync(
EnregistrementSouhait saisie, string? utilisateur, CancellationToken ct = default);
Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default);
}
/// <summary>
/// La liste d'envies, <b>personnelle</b>.
/// </summary>
/// <remarks>
/// ⚠️ <b>Toute méthode filtre sur <paramref name="utilisateur"/>, sans exception.</b> C'est la
/// différence de fond avec <c>ServiceCatalogue</c>, dont les lectures ne filtrent jamais sur
/// <c>AjoutePar</c> : le catalogue est commun, la liste d'envies ne l'est pas. Une envie qui
/// fuiterait vers un autre membre du foyer gâcherait exactement ce que la fonctionnalité sert
/// à préparer.
/// <para>
/// Sans identité (ni en-tête SSOwat ni utilisateur simulé), il n'y a pas de liste : on ne rend
/// rien et on n'écrit rien, plutôt que de rattacher des envies à un propriétaire inventé.
/// </para>
/// </remarks>
public sealed class ServiceSouhaits(MaBibliDbContext db) : IServiceSouhaits
{
public async Task<IReadOnlyList<SouhaitDto>> ListerAsync(
string? utilisateur, CancellationToken ct = default)
{
if (utilisateur is null)
{
return [];
}
var souhaits = await db.LivresSouhaites
.AsNoTracking()
.Where(s => s.Utilisateur == utilisateur)
.OrderBy(s => s.AuteurNormalise == string.Empty)
.ThenBy(s => s.AuteurNormalise)
.ThenBy(s => s.TitreNormalise)
.ToListAsync(ct);
return souhaits.Select(Projeter).ToList();
}
public async Task<ResultatSouhait> AjouterAsync(
EnregistrementSouhait saisie, string? utilisateur, CancellationToken ct = default)
{
if (utilisateur is null)
{
return ResultatSouhait.Invalide(
"Impossible d'ajouter une envie sans savoir à qui elle appartient.");
}
if (string.IsNullOrWhiteSpace(saisie.Titre))
{
return ResultatSouhait.Invalide("Le titre est obligatoire.");
}
// L'ISBN reste facultatif — on souhaite souvent une œuvre sans avoir choisi son édition —
// mais s'il est saisi il doit tenir debout, comme pour un livre du catalogue.
string? isbn = null;
if (!string.IsNullOrWhiteSpace(saisie.Isbn))
{
var normalise = IsbnUtils.Normaliser(saisie.Isbn);
if (!IsbnUtils.EstValide(normalise))
{
return ResultatSouhait.Invalide(
$"« {saisie.Isbn} » n'est pas un ISBN valide. Laissez le champ vide si vous ne l'avez pas.");
}
isbn = normalise;
}
var souhait = new LivreSouhaite
{
Utilisateur = utilisateur,
Titre = saisie.Titre,
Auteur = saisie.Auteur,
Editeur = Vide(saisie.Editeur),
Annee = Vide(saisie.Annee),
Isbn = isbn,
CoverUrl = Vide(saisie.CoverUrl),
Note = Vide(saisie.Note),
DateAjout = DateTime.UtcNow,
};
souhait.RecalculerFormes();
// Le doublon est refusé avec un message plutôt que laissé à l'index unique : l'écran de
// bibliographie rend le double clic facile, et « UNIQUE constraint failed » ne veut rien
// dire pour l'utilisateur.
var deja = await db.LivresSouhaites.AnyAsync(
s => s.Utilisateur == utilisateur
&& s.TitreNormalise == souhait.TitreNormalise
&& s.AuteurNormalise == souhait.AuteurNormalise,
ct);
if (deja)
{
return ResultatSouhait.Invalide($"« {souhait.Titre} » est déjà dans votre liste d'envies.");
}
db.LivresSouhaites.Add(souhait);
await db.SaveChangesAsync(ct);
return ResultatSouhait.Ok(Projeter(souhait));
}
public async Task<bool> SupprimerAsync(int id, string? utilisateur, CancellationToken ct = default)
{
if (utilisateur is null)
{
return false;
}
// Le filtre sur l'utilisateur fait partie de la CLÉ de recherche, pas d'une vérification
// ultérieure : ainsi personne ne peut supprimer l'envie d'un autre en devinant son
// identifiant. Une envie inexistante et une envie appartenant à un autre sont
// indiscernables de l'extérieur, ce qui est bien le comportement voulu.
var souhait = await db.LivresSouhaites
.FirstOrDefaultAsync(s => s.Id == id && s.Utilisateur == utilisateur, ct);
if (souhait is null)
{
return false;
}
db.LivresSouhaites.Remove(souhait);
await db.SaveChangesAsync(ct);
return true;
}
private static string? Vide(string? valeur) =>
string.IsNullOrWhiteSpace(valeur) ? null : valeur.Trim();
/// <summary>
/// Projection vers le DTO. <b>Sans le champ <c>Utilisateur</c></b> : il ne sort jamais de
/// l'API, pour qu'aucun client ne puisse croire qu'il désigne de qui il parle.
/// </summary>
internal static SouhaitDto Projeter(LivreSouhaite souhait) => new()
{
Id = souhait.Id,
Titre = souhait.Titre,
Auteur = souhait.Auteur,
Editeur = souhait.Editeur,
Annee = souhait.Annee,
Isbn = souhait.Isbn,
CoverUrl = souhait.CoverUrl,
Note = souhait.Note,
DateAjout = souhait.DateAjout,
};
}
+5
View File
@@ -70,6 +70,10 @@ else
<li class="carte-auteur-ligne" @key="auteur.Id">
<a class="lien-auteur" href="@($"?auteur={auteur.Id}")">@auteur.Nom</a>
<span class="etiquette">@Livres(auteur.NombreDeLivres)</span>
@* Le declencheur decrit dans IDEES.md : depuis un auteur qu'on a deja, voir
tout ce qu'il a ecrit et marquer ce qui manque. *@
<a class="bouton bouton-compact bouton-discret"
href="@($"auteurs/{auteur.Id}/bibliographie")">Bibliographie</a>
</li>
}
</ul>
@@ -77,6 +81,7 @@ else
<nav class="actions-flottantes">
<a class="bouton bouton-discret" href="">Retour au catalogue</a>
<a class="bouton bouton-discret" href="souhaits">Ma liste d'envies</a>
</nav>
@code {
+245
View File
@@ -0,0 +1,245 @@
@page "/auteurs/{AuteurId:int}/bibliographie"
@inject ServiceLivresApi Api
<PageTitle>MaBibli — bibliographie</PageTitle>
@if (_bibliographie is null && _erreur is null)
{
<h1 class="titre-page">Bibliographie</h1>
<p class="message-discret">Interrogation de la BnF…</p>
}
else if (_erreur is not null)
{
<h1 class="titre-page">Bibliographie</h1>
<p class="message-erreur" role="alert">@_erreur</p>
}
else if (_bibliographie is { } biblio)
{
<h1 class="titre-page">@biblio.Auteur.Nom</h1>
@if (biblio.Avertissement is not null)
{
<p class="message-avertissement" role="alert">@biblio.Avertissement</p>
}
@if (biblio.Oeuvres.Count == 0)
{
<p class="message-discret">
La BnF ne connaît aucun livre de cet auteur. C'est le cas attendu pour les auteurs
étrangers : le dépôt légal français ne couvre que l'édition francophone.
</p>
}
else
{
@* Ces compteurs disent à l'utilisateur CE QU'IL REGARDE. Sur un auteur très réédité,
la liste n'est qu'un extrait : le taire ferait croire qu'un livre absent n'existe pas. *@
<p class="compteur">
@biblio.Oeuvres.Count œuvre@(biblio.Oeuvres.Count > 1 ? "s" : "")
— @_possedees déjà chez vous
</p>
@* Deux phrases distinctes plutôt qu'une seule à trous : enchaîner des fragments
conditionnels dans une même phrase produit une ponctuation bancale dès qu'un des
fragments manque (« exister. .34 notices ont été écartées »). *@
<p class="message-discret">
@if (biblio.Tronquee)
{
<text>
D'après le catalogue de la BnF, à partir de @biblio.NoticesLues notices
sur les <strong>@biblio.NoticesAnnoncees</strong> qu'elle annonce :
<strong>cette liste est un extrait</strong>, classé par pertinence.
Un livre absent d'ici peut très bien exister.
</text>
}
else
{
<text>
D'après le catalogue de la BnF, sur
@biblio.NoticesLues notice@(biblio.NoticesLues > 1 ? "s" : "").
</text>
}
</p>
@if (biblio.NoticesEcartees > 0)
{
<p class="message-discret">
@biblio.NoticesEcartees notice@(biblio.NoticesEcartees > 1 ? "s ont" : " a")
été écartée@(biblio.NoticesEcartees > 1 ? "s" : "") : autre support (livre audio,
jeu) ou ouvrage signé de quelqu'un d'autre.
</p>
}
@if (_message is not null)
{
<p class="message-erreur" role="alert">@_message</p>
}
@* Filtre sur une seule question : « qu'est-ce qui me manque ? ». Les livres possédés ne
sont jamais masqués par défaut — le grisage est l'information principale de l'écran. *@
<div class="filtres">
<div class="segments" role="group" aria-label="Filtrer la bibliographie">
<button type="button" class="segment @(_seulementManquants ? null : "segment-actif")"
@onclick="() => _seulementManquants = false">Tout</button>
<button type="button" class="segment @(_seulementManquants ? "segment-actif" : null)"
@onclick="() => _seulementManquants = true">Ce qui me manque</button>
</div>
</div>
<ul class="liste-bibliographie">
@foreach (var oeuvre in Affichees(biblio))
{
@* La classe « oeuvre-possedee » grise la ligne : le livre est déjà sur l'étagère. *@
<li class="carte-oeuvre @(oeuvre.Possede ? "oeuvre-possedee" : null)" @key="oeuvre.Titre">
<div class="carte-corps">
<p class="carte-titre">
@if (oeuvre is { Possede: true, LivreId: { } livreId })
{
<a class="lien-auteur" href="@($"livres/{livreId}")">@oeuvre.Titre</a>
}
else
{
@oeuvre.Titre
}
</p>
<p class="carte-details">
@if (!string.IsNullOrWhiteSpace(oeuvre.Annee))
{
<span>@oeuvre.Annee</span>
}
@if (!string.IsNullOrWhiteSpace(oeuvre.Editeur))
{
<span>@oeuvre.Editeur</span>
}
@if (oeuvre.NombreEditions > 1)
{
<span class="etiquette">@oeuvre.NombreEditions éditions</span>
}
@if (oeuvre.Possede)
{
<span class="etiquette etiquette-possede">Déjà chez vous</span>
}
else if (oeuvre.Souhaite)
{
<span class="etiquette etiquette-souhaite">Dans vos envies</span>
}
</p>
</div>
@if (oeuvre.ADecouvrir)
{
<button type="button" class="bouton bouton-compact bouton-principal"
disabled="@_enCours" @onclick="() => SouhaiterAsync(oeuvre)"
aria-label="@($"Ajouter « {oeuvre.Titre} » à ma liste d'envies")">
Je le veux
</button>
}
</li>
}
</ul>
@* Le rapprochement est fait par titre, et il rate des choses (voir CleOeuvre). Le dire
franchement vaut mieux que laisser l'utilisateur découvrir seul qu'un livre qu'il
possède n'est pas grisé. *@
<p class="message-discret">
Le rapprochement avec votre bibliothèque se fait <strong>par titre</strong>. Un livre
que vous possédez sous un titre différent (traduction, intégrale, édition retitrée)
n'apparaîtra donc pas comme possédé. Rien n'est masqué : à vous de trancher.
</p>
}
}
<nav class="actions-flottantes">
<a class="bouton bouton-discret" href="auteurs">Retour aux auteurs</a>
@if (_bibliographie is { } b)
{
<a class="bouton bouton-discret" href="@($"?auteur={b.Auteur.Id}")">Ses livres chez vous</a>
}
<a class="bouton bouton-discret" href="souhaits">Ma liste d'envies</a>
</nav>
@code {
[Parameter]
public int AuteurId { get; set; }
private BibliographieDto? _bibliographie;
private bool _seulementManquants;
private bool _enCours;
private string? _erreur;
private string? _message;
private int _possedees;
protected override async Task OnParametersSetAsync() => await ChargerAsync();
private IEnumerable<OeuvreBibliographie> Affichees(BibliographieDto biblio) =>
_seulementManquants ? biblio.Oeuvres.Where(o => o.ADecouvrir) : biblio.Oeuvres;
private async Task ChargerAsync()
{
_erreur = null;
try
{
_bibliographie = await Api.ObtenirBibliographieAsync(AuteurId);
if (_bibliographie is null)
{
_erreur = "Cet auteur n'existe pas dans votre bibliothèque.";
return;
}
_possedees = _bibliographie.Oeuvres.Count(o => o.Possede);
}
catch (Exception ex)
{
_erreur = $"Impossible d'obtenir la bibliographie : {ex.Message}";
}
}
/// <summary>
/// Ajoute une œuvre à la liste d'envies, en emportant ce que la BnF en sait.
/// </summary>
/// <remarks>
/// L'auteur enregistré est celui de l'écran, pas celui de la notice : c'est lui que
/// l'utilisateur a en tête, et c'est sous ce nom qu'il retrouvera l'envie dans l'export.
/// <para>
/// Après l'ajout, la bibliographie est <b>relue</b> plutôt que corrigée en mémoire : c'est
/// le serveur qui sait rapprocher l'envie de l'œuvre, et refaire ce calcul ici le
/// dupliquerait — avec le risque qu'il diverge.
/// </para>
/// </remarks>
private async Task SouhaiterAsync(OeuvreBibliographie oeuvre)
{
if (_bibliographie is null)
{
return;
}
_enCours = true;
_message = null;
var resultat = await Api.AjouterSouhaitAsync(new EnregistrementSouhait
{
Titre = oeuvre.Titre,
Auteur = _bibliographie.Auteur.Nom,
Editeur = oeuvre.Editeur,
Annee = oeuvre.Annee,
// L'ISBN de la BnF est celui d'UNE édition relevée, souvent à 10 chiffres et parfois
// épuisée. Il est repris tel quel : il aide à commander, sans engager le choix.
Isbn = oeuvre.Isbn,
});
_enCours = false;
if (!resultat.EstOk)
{
_message = resultat.Erreur;
return;
}
await ChargerAsync();
}
}
+1
View File
@@ -126,6 +126,7 @@ else if (_livres is not null)
<a class="bouton bouton-discret" href="ajout/manuel">Saisie manuelle</a>
<a class="bouton bouton-discret" href="prets">Prêts</a>
<a class="bouton bouton-discret" href="auteurs">Auteurs</a>
<a class="bouton bouton-discret" href="souhaits">Envies</a>
</nav>
@code {
+241
View File
@@ -0,0 +1,241 @@
@page "/souhaits"
@inject ServiceLivresApi Api
<PageTitle>MaBibli — liste d'envies</PageTitle>
<h1 class="titre-page">Liste d'envies</h1>
@* Le rappel n'est pas décoratif : la bibliothèque est commune, cette liste ne l'est pas.
Sans ce mot, on pourrait croire que le foyer entier voit ce qu'on note ici. *@
<p class="message-discret">
Les livres que vous aimeriez acquérir. <strong>Cette liste est la vôtre</strong> :
les autres membres du foyer ont la leur, et ne voient pas celle-ci.
Ces livres ne sont pas dans le catalogue et ne comptent pas dans la bibliothèque.
</p>
@if (_erreur is not null)
{
<p class="message-erreur" role="alert">@_erreur</p>
}
@if (_souhaits is null)
{
<p class="message-discret">Chargement…</p>
}
else
{
<section class="bloc-souhait-ajout">
<h2 class="titre-section">Ajouter une envie</h2>
@* Deux façons d'alimenter la liste : l'ISBN quand on a le livre en main (en librairie,
chez quelqu'un), la saisie libre quand on n'a qu'un titre. La bibliographie d'un
auteur, elle, alimente la liste depuis l'écran des auteurs. *@
<div class="champ">
<label class="champ-libelle" for="isbn-souhait">Pré-remplir depuis un ISBN</label>
<div class="ligne-isbn">
<input id="isbn-souhait" class="champ-saisie" type="text" inputmode="numeric"
placeholder="978…" @bind="_isbn" @bind:event="oninput" />
<button type="button" class="bouton bouton-discret"
disabled="@(_recherche || string.IsNullOrWhiteSpace(_isbn))"
@onclick="ChercherIsbnAsync">
@(_recherche ? "Recherche…" : "Chercher")
</button>
</div>
<p class="champ-aide">Facultatif : les champs ci-dessous restent modifiables.</p>
</div>
<div class="champ">
<label class="champ-libelle" for="titre-souhait">Titre <span class="obligatoire">*</span></label>
<input id="titre-souhait" class="champ-saisie" type="text" @bind="_saisie.Titre" @bind:event="oninput" />
</div>
<div class="champ">
<label class="champ-libelle" for="auteur-souhait">Auteur</label>
<input id="auteur-souhait" class="champ-saisie" type="text" @bind="_saisie.Auteur" @bind:event="oninput" />
</div>
<div class="champ">
<label class="champ-libelle" for="note-souhait">Note</label>
<input id="note-souhait" class="champ-saisie" type="text" @bind="_saisie.Note" @bind:event="oninput"
placeholder="« demandé à Noël », « en poche seulement »…" />
</div>
<div class="actions-formulaire">
<button type="button" class="bouton bouton-principal"
disabled="@(_ajout || string.IsNullOrWhiteSpace(_saisie.Titre))"
@onclick="AjouterAsync">
Ajouter à ma liste
</button>
</div>
</section>
@if (_souhaits.Count == 0)
{
<p class="message-discret">
Votre liste est vide. Ajoutez un livre ci-dessus, ou ouvrez la bibliographie d'un
auteur depuis <a href="auteurs">la liste des auteurs</a>.
</p>
}
else
{
<p class="compteur">@_souhaits.Count livre@(_souhaits.Count > 1 ? "s" : "") souhaité@(_souhaits.Count > 1 ? "s" : "")</p>
@* Un seul bloc d'export, en haut de liste : c'est l'action qu'on vient chercher quand on
part en librairie, elle ne doit pas être au bout d'un défilement. *@
<div class="actions-export">
<a class="bouton bouton-discret" href="@ServiceLivresApi.UrlExportSouhaits(FormatExportSouhaits.Texte)" download>
Exporter en texte
</a>
<a class="bouton bouton-discret" href="@ServiceLivresApi.UrlExportSouhaits(FormatExportSouhaits.Csv)" download>
Exporter en CSV
</a>
</div>
<ul class="liste-souhaits">
@foreach (var souhait in _souhaits)
{
<li class="carte-souhait" @key="souhait.Id">
<Couverture Url="@souhait.CoverUrl" Titre="@souhait.Titre"
Classe="couverture-petite" Agrandissable="true" />
<div class="carte-corps">
<p class="carte-titre">@souhait.Titre</p>
@if (!string.IsNullOrWhiteSpace(souhait.Auteur))
{
<p class="carte-auteur">@souhait.Auteur</p>
}
<p class="carte-details">
@if (!string.IsNullOrWhiteSpace(souhait.Editeur))
{
<span>@souhait.Editeur</span>
}
@if (!string.IsNullOrWhiteSpace(souhait.Annee))
{
<span>@souhait.Annee</span>
}
@if (!string.IsNullOrWhiteSpace(souhait.Isbn))
{
<span class="etiquette">ISBN @souhait.Isbn</span>
}
</p>
@if (!string.IsNullOrWhiteSpace(souhait.Note))
{
<p class="souhait-note">@souhait.Note</p>
}
</div>
<button type="button" class="bouton bouton-compact bouton-danger"
disabled="@_enCours" @onclick="() => SupprimerAsync(souhait)"
aria-label="@($"Retirer « {souhait.Titre} » de la liste d'envies")">
Retirer
</button>
</li>
}
</ul>
}
}
<nav class="actions-flottantes">
<a class="bouton bouton-discret" href="">Retour au catalogue</a>
<a class="bouton bouton-discret" href="auteurs">Auteurs</a>
</nav>
@code {
private IReadOnlyList<SouhaitDto>? _souhaits;
private EnregistrementSouhait _saisie = new();
private string _isbn = string.Empty;
private bool _ajout;
private bool _recherche;
private bool _enCours;
private string? _erreur;
protected override Task OnInitializedAsync() => ChargerAsync();
private async Task ChargerAsync()
{
try
{
_souhaits = await Api.ListerSouhaitsAsync();
}
catch (Exception ex)
{
_erreur = $"Impossible de charger la liste d'envies : {ex.Message}";
}
}
/// <summary>
/// Pré-remplit le formulaire depuis la cascade ISBN existante (BnF → OpenLibrary).
/// </summary>
/// <remarks>
/// On retient le premier candidat sans demander de choisir : contrairement à l'ajout au
/// catalogue, une envie ne fige pas d'édition — l'utilisateur achètera celle qu'il trouvera.
/// Tous les champs restent modifiables.
/// </remarks>
private async Task ChercherIsbnAsync()
{
_recherche = true;
_erreur = null;
try
{
var resultat = await Api.ChercherIsbnAsync(_isbn.Trim());
var candidat = resultat?.Candidats.FirstOrDefault();
if (candidat is null)
{
_erreur = $"Aucune notice trouvée pour « {_isbn.Trim()} ». Complétez à la main.";
return;
}
_saisie.Titre = candidat.Titre;
_saisie.Auteur = string.Join(", ", candidat.Auteurs);
_saisie.Editeur = candidat.Editeur;
_saisie.Annee = candidat.Annee;
_saisie.Isbn = _isbn.Trim();
_saisie.CoverUrl = candidat.CoverUrl;
}
catch (Exception ex)
{
_erreur = $"La recherche a échoué : {ex.Message}";
}
finally
{
_recherche = false;
}
}
private async Task AjouterAsync()
{
_ajout = true;
_erreur = null;
var resultat = await Api.AjouterSouhaitAsync(_saisie);
_ajout = false;
if (!resultat.EstOk)
{
_erreur = resultat.Erreur;
return;
}
_saisie = new EnregistrementSouhait();
_isbn = string.Empty;
await ChargerAsync();
}
private async Task SupprimerAsync(SouhaitDto souhait)
{
_enCours = true;
_erreur = await Api.SupprimerSouhaitAsync(souhait.Id);
_enCours = false;
if (_erreur is null)
{
await ChargerAsync();
}
}
}
@@ -231,6 +231,63 @@ public sealed class ServiceLivresApi(HttpClient http)
};
}
// ─────────────────────────────────────────────────────────────────────────
// Liste d'envies et bibliographie par auteur.
// Bloc ajouté en fin de classe pour rester bien délimité.
// ─────────────────────────────────────────────────────────────────────────
/// <summary>
/// La liste d'envies <b>de l'utilisateur courant</b>.
/// </summary>
/// <remarks>
/// Comme pour le statut de lecture, le client ne transmet aucune identité : le serveur sait
/// qui appelle. Il n'existe volontairement pas de moyen de demander la liste d'un autre.
/// </remarks>
public async Task<IReadOnlyList<SouhaitDto>> ListerSouhaitsAsync(CancellationToken ct = default) =>
await http.GetFromJsonAsync<List<SouhaitDto>>("api/souhaits", Json, ct) ?? [];
public async Task<ResultatApi<SouhaitDto>> AjouterSouhaitAsync(
EnregistrementSouhait saisie, CancellationToken ct = default)
{
var reponse = await http.PostAsJsonAsync("api/souhaits", saisie, Json, ct);
return await InterpreterAsync<SouhaitDto>(reponse, ct);
}
public async Task<string?> SupprimerSouhaitAsync(int id, CancellationToken ct = default)
{
var reponse = await http.DeleteAsync($"api/souhaits/{id}", ct);
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
}
/// <summary>
/// Adresse de téléchargement d'un export.
/// </summary>
/// <remarks>
/// Le fichier n'est pas récupéré par ce service : un simple lien suffit, le serveur pose un
/// <c>Content-Disposition: attachment</c> et le navigateur enregistre. Passer par
/// <see cref="HttpClient"/> obligerait à fabriquer une URL d'objet en JavaScript pour un
/// résultat identique.
/// <para>
/// ⚠️ Le lien <b>doit</b> porter l'attribut <c>download</c> : sans lui, le routeur Blazor
/// intercepte le clic comme une navigation interne et affiche sa page « introuvable » au
/// lieu de laisser le navigateur télécharger.
/// </para>
/// </remarks>
public static string UrlExportSouhaits(FormatExportSouhaits format) =>
format == FormatExportSouhaits.Csv ? "api/souhaits/export.csv" : "api/souhaits/export.txt";
/// <summary>
/// Bibliographie d'un auteur d'après la BnF. <c>null</c> si l'auteur n'existe pas.
/// </summary>
public async Task<BibliographieDto?> ObtenirBibliographieAsync(
int auteurId, CancellationToken ct = default)
{
var reponse = await http.GetAsync($"api/auteurs/{auteurId}/bibliographie", ct);
return reponse.StatusCode == HttpStatusCode.NotFound
? null
: await LireAsync<BibliographieDto>(reponse, ct);
}
private sealed record ErreurApi(string? Erreur);
}
+96
View File
@@ -738,3 +738,99 @@ body {
margin: 0;
font-size: 0.95rem;
}
/* ─────────────────────────────────────────────────────────────────────────────
Liste d'envies et bibliographie par auteur.
Mobile d'abord : une colonne, l'action à droite du texte, rien qui déborde.
───────────────────────────────────────────────────────────────────────────── */
.bloc-souhait-ajout {
background: var(--mb-fond-doux);
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 1rem;
}
/* Le champ ISBN et son bouton sur une ligne : le bouton ne doit pas se rétrécir
au point de devenir intouchable au pouce quand le champ est large. */
.ligne-isbn {
display: flex;
gap: 0.5rem;
}
.ligne-isbn .champ-saisie {
flex: 1;
min-width: 0;
}
.ligne-isbn .bouton {
flex-shrink: 0;
}
.actions-export {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.liste-souhaits,
.liste-bibliographie {
list-style: none;
margin: 0;
padding: 0;
}
.carte-souhait,
.carte-oeuvre {
display: flex;
gap: 0.75rem;
align-items: flex-start;
padding: 0.75rem 0;
border-bottom: 1px solid var(--mb-bord);
}
/* Le corps prend la place restante et « min-width: 0 » l'autorise à rétrécir :
sans lui, un titre long pousse le bouton hors de l'écran sur mobile. */
.carte-souhait .carte-corps,
.carte-oeuvre .carte-corps {
flex: 1;
min-width: 0;
}
.carte-souhait .bouton,
.carte-oeuvre .bouton {
flex-shrink: 0;
align-self: center;
}
.souhait-note {
margin: 0.25rem 0 0;
color: var(--mb-texte-doux);
font-size: 0.9rem;
font-style: italic;
}
/* Le grisage des livres déjà possédés : c'est l'information principale de l'écran
de bibliographie. L'opacité seule suffirait visuellement, mais elle ne dit rien
à un lecteur d'écran — d'où l'étiquette « Déjà chez vous » qui l'accompagne
toujours dans le balisage. */
.oeuvre-possedee {
opacity: 0.55;
}
.oeuvre-possedee .carte-titre {
font-weight: 400;
}
.etiquette-possede {
background: #e3ece3;
color: #2f5d34;
font-weight: 600;
}
.etiquette-souhaite {
background: #e6e6f5;
color: #3b3b7a;
font-weight: 600;
}
+137
View File
@@ -0,0 +1,137 @@
using System.Text.Json.Serialization;
namespace MaBibli.Shared.Dtos;
/// <summary>
/// Une envie telle qu'exposée par l'API.
/// </summary>
/// <remarks>
/// Il n'y a <b>aucun champ « utilisateur »</b>, et il ne doit jamais y en avoir : l'API ne rend
/// que les envies de l'appelant, qu'elle identifie par les en-têtes SSOwat. Exposer le
/// propriétaire laisserait croire qu'on peut demander celles d'un autre.
/// </remarks>
public record SouhaitDto
{
public required int Id { get; init; }
public required string Titre { get; init; }
public string? Auteur { get; init; }
public string? Editeur { get; init; }
public string? Annee { get; init; }
public string? Isbn { get; init; }
public string? CoverUrl { get; init; }
public string? Note { get; init; }
public required DateTime DateAjout { get; init; }
}
/// <summary>Charge utile d'ajout d'une envie.</summary>
/// <remarks>
/// Comme <see cref="EnregistrementLivre"/>, elle ne porte pas d'identité : le propriétaire est
/// déterminé par le serveur.
/// </remarks>
public record EnregistrementSouhait
{
public string Titre { get; set; } = string.Empty;
public string? Auteur { get; set; }
public string? Editeur { get; set; }
public string? Annee { get; set; }
public string? Isbn { get; set; }
public string? CoverUrl { get; set; }
public string? Note { get; set; }
}
/// <summary>Formats d'export de la liste d'envies.</summary>
/// <remarks>
/// Les deux usages décrits dans IDEES.md ne demandent pas la même chose : « l'emporter en
/// librairie » veut un texte qui se lit tel quel sur un téléphone, « la partager avant un
/// anniversaire » finit souvent dans un tableur pour se répartir les achats. Ni l'un ni l'autre
/// n'exige de dépendance : ce sont deux fichiers texte produits par du <c>string</c>.
/// </remarks>
public enum FormatExportSouhaits
{
/// <summary>Liste lisible telle quelle, groupée par auteur.</summary>
Texte = 0,
/// <summary>Tableau ouvrable dans un tableur.</summary>
Csv = 1,
}
/// <summary>Une œuvre de la bibliographie d'un auteur, confrontée à ce que l'on possède déjà.</summary>
public record OeuvreBibliographie
{
public required string Titre { get; init; }
public string? Annee { get; init; }
public string? Editeur { get; init; }
/// <summary>ISBN d'une des éditions relevées, quand la notice en portait un.</summary>
public string? Isbn { get; init; }
/// <summary>
/// Nombre de notices BnF réunies sous cette œuvre — c'est-à-dire le nombre de rééditions
/// relevées. Sert à repérer les œuvres majeures d'un auteur très réédité.
/// </summary>
public int NombreEditions { get; init; }
/// <summary>
/// Vrai si un livre du catalogue porte le même titre d'œuvre.
/// <b>Rapprochement par titre, donc faillible</b> — voir <c>CleOeuvre</c>.
/// </summary>
public bool Possede { get; init; }
/// <summary>Le livre possédé, s'il y en a un : de quoi ouvrir sa fiche.</summary>
public int? LivreId { get; init; }
/// <summary>Vrai si l'appelant a déjà cette œuvre dans sa liste d'envies.</summary>
public bool Souhaite { get; init; }
public int? SouhaitId { get; init; }
/// <summary>Ni possédé, ni déjà souhaité : ce qui reste à découvrir.</summary>
[JsonIgnore]
public bool ADecouvrir => !Possede && !Souhaite;
}
/// <summary>
/// Bibliographie d'un auteur du catalogue, telle que la BnF la connaît.
/// </summary>
/// <remarks>
/// Les compteurs ne sont pas décoratifs : ils disent à l'utilisateur ce qu'il regarde. Sur un
/// auteur très réédité (Émile Zola : 2692 notices), on ne montre qu'un <b>extrait classé par
/// pertinence</b>, et le taire ferait passer une liste partielle pour une bibliographie complète.
/// </remarks>
public record BibliographieDto
{
public required AuteurDto Auteur { get; init; }
public IReadOnlyList<OeuvreBibliographie> Oeuvres { get; init; } = [];
/// <summary>Nombre total de notices que la BnF déclare pour cet auteur.</summary>
public int NoticesAnnoncees { get; init; }
/// <summary>Nombre de notices réellement lues (plafonné).</summary>
public int NoticesLues { get; init; }
/// <summary>Notices écartées parce qu'aucun de leurs auteurs n'est celui demandé.</summary>
public int NoticesEcartees { get; init; }
/// <summary>Vrai si la BnF en annonce plus qu'on n'en a lu : la liste est un extrait.</summary>
public bool Tronquee => NoticesAnnoncees > NoticesLues;
/// <summary>Message à afficher quand la source n'a pas pu être interrogée normalement.</summary>
public string? Avertissement { get; init; }
}
+85
View File
@@ -0,0 +1,85 @@
using MaBibli.Shared.Textes;
namespace MaBibli.Shared.Entites;
/// <summary>
/// Un livre que l'on aimerait acquérir. <b>Personnel</b> : chacun sa liste.
/// </summary>
/// <remarks>
/// <b>Pourquoi une entité séparée plutôt qu'un statut de plus sur <see cref="Livre"/> ?</b>
/// Un livre souhaité n'est pas un livre possédé. S'il était une ligne de <c>Livres</c>, il
/// entrerait mécaniquement dans le catalogue, dans les compteurs, dans les prêts et dans le
/// cache hors-ligne — et il faudrait ajouter « et qui n'est pas souhaité » à <b>chaque</b>
/// lecture du catalogue. Un invariant qu'il faut réécrire à chaque requête finit toujours par
/// être oublié quelque part ; ici l'inventaire et l'envie ne se croisent tout simplement pas,
/// parce qu'ils ne vivent pas dans la même table.
/// <para>
/// C'est aussi la seule forme qui accepte une envie <b>sans ISBN ni édition arrêtée</b> : on
/// souhaite une œuvre (« le dernier Werber »), on possède un exemplaire. Un souhait n'a donc
/// ni <see cref="Format"/> ni couverture obligatoire, et ne peut pas être prêté.
/// </para>
/// <para>
/// La portée personnelle suit <see cref="StatutLecture"/>, dont la documentation annonçait déjà
/// ce voisinage : <see cref="Utilisateur"/> <b>est</b> une frontière, contrairement à
/// <see cref="Livre.AjoutePar"/>. On ne lit jamais la liste d'envies de quelqu'un d'autre —
/// c'est précisément ce qui permet d'offrir un livre sans vendre la mèche.
/// </para>
/// </remarks>
public class LivreSouhaite
{
public int Id { get; set; }
/// <summary>
/// <c>YNH_USER</c> du propriétaire de l'envie. <b>Frontière</b> : toute lecture filtre dessus.
/// </summary>
public string Utilisateur { get; set; } = string.Empty;
public string Titre { get; set; } = string.Empty;
/// <summary>
/// Titre mis à plat, servant à la fois au dédoublonnage et au rapprochement avec le
/// catalogue (« ai-je déjà ce livre ? »). Recalculé à chaque écriture.
/// </summary>
public string TitreNormalise { get; set; } = string.Empty;
/// <summary>
/// Auteur en <b>texte libre</b>, volontairement pas une clé étrangère vers <see cref="Auteur"/>.
/// </summary>
/// <remarks>
/// La table <c>Auteurs</c> décrit qui est <i>dans la bibliothèque</i> : y insérer les auteurs
/// souhaités les ferait apparaître sur l'écran des auteurs avec « 0 livre », et exposerait la
/// liste d'envies d'une personne à tout le foyer — l'inverse de ce qu'on veut. Le texte libre
/// survit en outre à la disparition de l'auteur du catalogue.
/// </remarks>
public string? Auteur { get; set; }
/// <summary>
/// Auteur mis à plat. <b>Jamais <c>null</c></b> (chaîne vide si l'auteur est inconnu) :
/// SQLite considère deux <c>NULL</c> comme distincts, et l'index unique laisserait alors
/// passer des doublons d'envies sans auteur.
/// </summary>
public string AuteurNormalise { get; set; } = string.Empty;
public string? Editeur { get; set; }
/// <summary>Année de publication, telle que la source la donne. Texte : la BnF écrit « 2012 », mais aussi « [DL 2012] ».</summary>
public string? Annee { get; set; }
/// <summary>ISBN d'une édition repérée, quand il y en a une. Facultatif : on souhaite une œuvre, pas forcément une édition.</summary>
public string? Isbn { get; set; }
public string? CoverUrl { get; set; }
/// <summary>Note libre : « demandé à Noël », « en poche seulement », « chez Untel ».</summary>
public string? Note { get; set; }
public DateTime DateAjout { get; set; }
public void RecalculerFormes()
{
Titre = Titre.Trim();
TitreNormalise = CleOeuvre.Cle(Titre);
Auteur = string.IsNullOrWhiteSpace(Auteur) ? null : Auteur.Trim();
AuteurNormalise = RapprochementAuteurs.Cle(Auteur);
}
}
+74
View File
@@ -0,0 +1,74 @@
namespace MaBibli.Shared.Textes;
/// <summary>
/// Clé d'identification d'une <b>œuvre</b> à partir d'un titre, indépendamment de l'édition.
/// </summary>
/// <remarks>
/// C'est le rapprochement le moins mauvais dont on dispose, et il faut être clair sur ses limites.
/// <para>
/// <b>Pourquoi pas l'ISBN ?</b> Parce qu'il désigne une <i>édition</i>, pas une œuvre : le poche
/// de 1998, le grand format de 1991 et la réédition de 2015 portent trois ISBN différents pour le
/// même roman. Mesuré sur la bibliographie BnF de Bernard Werber : 183 notices de livres pour
/// 51 œuvres, chacune revenant en moyenne trois fois sous trois ISBN. Comparer les ISBN
/// répondrait donc « vous ne l'avez pas » à propos d'un livre posé sur l'étagère, ce qui est le
/// pire résultat possible pour une liste d'envies.
/// </para>
/// <para>
/// <b>Ce que la clé fait.</b> Elle coupe la mention de responsabilité ISBD (« <c> / </c> ») puis
/// le sous-titre (« <c> : </c> »), et met le reste à plat. C'est ce qui réunit
/// « Les thanatonautes », « Les thanatonautes : roman » et
/// « Les thanatonautes / Bernard Werber ».
/// </para>
/// <para>
/// <b>Ce qu'elle rate</b>, et qu'il ne faut pas prétendre autrement :
/// <list type="bullet">
/// <item>Deux œuvres distinctes partageant un titre principal sont confondues (« Œuvres »,
/// « Nouvelles »). Le risque est borné : la comparaison n'a lieu qu'<b>à auteur donné</b>.</item>
/// <item>Un tome n'est pas rapproché de son recueil : « Troisième humanité » et
/// « Troisième humanité. Tome 1 » restent deux lignes. Le point n'est pas coupé, faute de savoir
/// distinguer « Tome 1 » d'un titre qui contient légitimement un point.</item>
/// <item>Un titre retraduit ou changé au fil des rééditions n'est pas rapproché du tout.</item>
/// <item>Une œuvre possédée à l'intérieur d'une intégrale n'est pas repérée : l'intégrale a son
/// propre titre.</item>
/// </list>
/// Conséquence assumée : l'écran de bibliographie peut afficher comme « à découvrir » un livre
/// déjà présent sous un autre titre. Il ne fait donc que <b>griser</b> ce qu'il reconnaît, sans
/// jamais masquer une ligne — un faux négatif se voit et se corrige d'un coup d'œil, alors qu'une
/// ligne masquée à tort serait invisible.
/// </para>
/// </remarks>
public static class CleOeuvre
{
/// <summary>Sépare le titre de la mention de responsabilité : « Germinal / Émile Zola ».</summary>
private const string SeparateurResponsabilite = " / ";
/// <summary>Sépare le titre de son sous-titre : « Les thanatonautes : roman ».</summary>
private const string SeparateurSousTitre = " : ";
/// <summary>
/// Clé comparable d'un titre : « Les thanatonautes : roman » → <c>les thanatonautes</c>.
/// </summary>
public static string Cle(string? titre)
{
var s = titre?.Trim();
if (string.IsNullOrEmpty(s))
{
return string.Empty;
}
s = Tronquer(s, SeparateurResponsabilite);
var sansSousTitre = Tronquer(s, SeparateurSousTitre);
// Un titre qui n'est QUE son sous-titre (« : roman ») ne doit pas se réduire à rien :
// mieux vaut alors garder la chaîne entière que produire une clé vide, qui se
// confondrait avec toutes les autres clés vides.
var retenu = NormalisationTexte.Normaliser(sansSousTitre);
return retenu.Length > 0 ? retenu : NormalisationTexte.Normaliser(s);
}
private static string Tronquer(string s, string separateur)
{
var i = s.IndexOf(separateur, StringComparison.Ordinal);
return i >= 0 ? s[..i].Trim() : s;
}
}
@@ -0,0 +1,175 @@
using MaBibli.Api.Services.Catalogue;
using MaBibli.Api.Services.Isbn;
namespace MaBibli.Tests;
/// <summary>
/// Lecture des réponses SRU « par auteur », sur des réponses <b>réelles</b> enregistrées le
/// 2026-08-18 (25 notices chacune). Aucun accès réseau.
/// </summary>
/// <remarks>
/// Les deux fixtures sont choisies pour ce qu'elles ont de gênant :
/// <list type="bullet">
/// <item><c>werber</c> : bibliographie propre mais très redondante (rééditions en série) ;</item>
/// <item><c>zola</c> : bibliographie <b>polluée</b> — l'index auteur de la BnF y mêle les
/// ouvrages de sa fille Denise Le Blond-Zola, qui parlent de lui sans qu'il les ait écrits.</item>
/// </list>
/// </remarks>
public class BnfBibliographieParserTests
{
private const string Werber = "bnf-auteur-werber.xml";
private const string Zola = "bnf-auteur-zola.xml";
[Fact]
public void Lit_le_nombre_de_notices_annonce_par_la_BnF()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
// La BnF en annonce bien plus qu'elle n'en renvoie : c'est ce qui permet à l'écran de
// dire honnêtement qu'il ne montre qu'un extrait.
Assert.True(
resultat.NombreAnnonce > resultat.NombreLu,
$"annoncé {resultat.NombreAnnonce}, lu {resultat.NombreLu}");
}
[Fact]
public void Retient_les_notices_de_l_auteur_demande()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
Assert.NotEmpty(resultat.Notices);
Assert.All(
resultat.Notices,
n => Assert.Contains(n.Auteurs, a => a.Contains("Werber", StringComparison.OrdinalIgnoreCase)));
}
/// <summary>
/// Le filtre critique : <c>bib.author all "Émile Zola"</c> rapproche « émile » et « zola »
/// sur l'ensemble des auteurs d'une notice, et remonte donc l'œuvre de sa fille.
/// </summary>
[Fact]
public void Ecarte_les_ouvrages_ecrits_par_quelqu_un_d_autre()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Zola), "Émile Zola");
Assert.True(
resultat.EcarteesAutreAuteur > 0,
"la fixture Zola doit contenir des notices d'un autre auteur, sinon elle ne teste rien");
// Denise Le Blond-Zola a écrit sur son père ; ses livres ne sont pas de lui.
Assert.DoesNotContain(
resultat.Notices,
n => n.Auteurs.Any(a => a.Contains("Le Blond", StringComparison.OrdinalIgnoreCase)));
Assert.All(
resultat.Notices,
n => Assert.Contains(
n.Auteurs,
a => MaBibli.Shared.Textes.RapprochementAuteurs.SontLeMemeAuteur(a, "Émile Zola")));
}
[Fact]
public void Tolere_la_forme_bibliographique_du_nom()
{
// La notice porte « Werber, Bernard (1961-....). Auteur du texte » : c'est le nettoyage
// ISBD puis le rapprochement d'auteurs qui doivent le ramener à « Bernard Werber ».
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "werber, bernard");
Assert.NotEmpty(resultat.Notices);
}
[Fact]
public void Extrait_l_ISBN_du_champ_identifier()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
var avecIsbn = resultat.Notices.Where(n => !string.IsNullOrWhiteSpace(n.Isbn)).ToList();
Assert.NotEmpty(avecIsbn);
// Le préfixe « ISBN » du dc:identifier ne doit pas rester collé au numéro.
Assert.All(avecIsbn, n => Assert.DoesNotContain("ISBN", n.Isbn!, StringComparison.OrdinalIgnoreCase));
Assert.All(avecIsbn, n => Assert.True(n.Isbn!.Length is 10 or 13, n.Isbn));
}
[Fact]
public void Coupe_la_mention_de_responsabilite_du_titre()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
// « Troisième humanité / Bernard Werber » → « Troisième humanité ».
Assert.All(resultat.Notices, n => Assert.DoesNotContain(" / ", n.Titre));
}
/// <summary>
/// Le regroupement est ce qui rend l'écran utilisable : sans lui, « Les fourmis » occupe
/// autant de lignes qu'il y a eu de retirages.
/// </summary>
[Fact]
public void Regroupe_les_reeditions_sous_une_seule_oeuvre()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
var oeuvres = ServiceBibliographie.Regrouper(resultat).ToList();
Assert.True(
oeuvres.Count < resultat.Notices.Count,
$"{resultat.Notices.Count} notices devraient donner moins de {resultat.Notices.Count} œuvres");
// Aucune œuvre en double après regroupement.
var cles = oeuvres.Select(o => MaBibli.Shared.Textes.CleOeuvre.Cle(o.Titre)).ToList();
Assert.Equal(cles.Count, cles.Distinct().Count());
// Au moins une œuvre a bien réuni plusieurs notices, sinon le test ne prouve rien.
Assert.Contains(oeuvres, o => o.NombreEditions > 1);
}
[Fact]
public void Retient_l_edition_la_plus_ancienne_comme_representant()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
foreach (var oeuvre in ServiceBibliographie.Regrouper(resultat).Where(o => o.NombreEditions > 1))
{
var cle = MaBibli.Shared.Textes.CleOeuvre.Cle(oeuvre.Titre);
var anneesDuGroupe = resultat.Notices
.Where(n => n.CleTitre == cle)
.Select(n => ServiceBibliographie.AnneeTriable(n.Annee))
.Where(a => a is not null)
.Select(a => a!.Value)
.ToList();
if (anneesDuGroupe.Count == 0)
{
continue;
}
Assert.Equal(anneesDuGroupe.Min(), ServiceBibliographie.AnneeTriable(oeuvre.Annee));
}
}
[Fact]
public void Une_reponse_vide_ne_fait_pas_echouer_la_lecture()
{
const string vide = """
<srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/">
<srw:numberOfRecords>0</srw:numberOfRecords>
</srw:searchRetrieveResponse>
""";
var resultat = BnfBibliographieParser.Parser(vide, "Personne");
Assert.Empty(resultat.Notices);
Assert.Equal(0, resultat.NombreAnnonce);
}
[Theory]
[InlineData("2012", 2012)]
[InlineData("[DL 2012]", 2012)]
[InlineData("cop. 1991", 1991)]
[InlineData("19..?", null)]
[InlineData("", null)]
[InlineData(null, null)]
public void Extrait_une_annee_triable_des_dates_BnF(string? date, int? attendu) =>
Assert.Equal(attendu, ServiceBibliographie.AnneeTriable(date));
}
+82
View File
@@ -0,0 +1,82 @@
using MaBibli.Shared.Textes;
namespace MaBibli.Tests;
/// <summary>
/// Clé d'œuvre : ce qu'elle réunit, et surtout <b>ce qu'elle rate</b>.
/// </summary>
/// <remarks>
/// Les cas d'échec sont testés au même titre que les réussites : ils documentent la limite du
/// rapprochement, que l'interface annonce à l'utilisateur. Si l'un d'eux se met à passer un
/// jour, c'est que la règle a changé — et le message affiché doit changer avec.
/// </remarks>
public class CleOeuvreTests
{
[Theory]
// Les trois formes rencontrées pour un même roman de Werber.
[InlineData("Les thanatonautes", "Les thanatonautes : roman")]
[InlineData("Les thanatonautes", "Les thanatonautes / Bernard Werber")]
[InlineData("Les thanatonautes : roman", "Les thanatonautes / Bernard Werber")]
// Casse et accents, comme partout ailleurs dans le projet.
[InlineData("Germinal", "GERMINAL")]
public void Reunit_les_variantes_d_un_meme_titre(string a, string b) =>
Assert.Equal(CleOeuvre.Cle(a), CleOeuvre.Cle(b));
[Theory]
[InlineData("Germinal", "La bête humaine")]
// Un tome n'est pas rapproché de l'œuvre entière : le point n'est pas coupé.
[InlineData("Troisième humanité", "Troisième humanité. Tome 1")]
public void Distingue_deux_oeuvres_differentes(string a, string b) =>
Assert.NotEqual(CleOeuvre.Cle(a), CleOeuvre.Cle(b));
/// <summary>
/// Limite assumée : deux œuvres distinctes partageant leur titre principal se confondent.
/// Le risque est borné, la comparaison n'ayant lieu qu'à auteur donné.
/// </summary>
[Fact]
public void Confond_deux_oeuvres_partageant_le_titre_principal() =>
Assert.Equal(
CleOeuvre.Cle("Nouvelles : premier recueil"),
CleOeuvre.Cle("Nouvelles : second recueil"));
/// <summary>
/// Limite <b>héritée</b> de <see cref="NormalisationTexte"/>, pas propre à la clé d'œuvre :
/// la ligature « œ » n'est pas ramenée à « oe ».
/// </summary>
/// <remarks>
/// <c>NormalisationTexte</c> décompose en NFD, qui sépare les accents mais laisse les
/// ligatures intactes — seul NFKD les défait. Conséquence concrète : <i>L'Œuvre</i> de Zola,
/// que la BnF écrit avec la ligature, n'est pas rapproché d'un « L'oeuvre » saisi au clavier.
/// <para>
/// Le défaut ne vient pas d'ici et ne se corrige pas ici : la même fonction alimente la
/// recherche du catalogue et la clé unique des auteurs, et en changer déplacerait ces deux
/// invariants. Consigné dans IDEES.md ; ce test existe pour que la limite soit visible plutôt
/// que découverte en usage.
/// </para>
/// </remarks>
[Fact]
public void Ne_reduit_pas_la_ligature_oe() =>
Assert.NotEqual(CleOeuvre.Cle("L'Œuvre"), CleOeuvre.Cle("L'oeuvre"));
/// <summary>
/// Limite assumée : un titre retraduit n'est pas rapproché du tout.
/// </summary>
[Fact]
public void Ne_rapproche_pas_un_titre_retraduit() =>
Assert.NotEqual(CleOeuvre.Cle("Le Seigneur des anneaux"), CleOeuvre.Cle("The Lord of the Rings"));
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Rend_une_cle_vide_pour_une_saisie_vide(string? titre) =>
Assert.Equal(string.Empty, CleOeuvre.Cle(titre));
/// <summary>
/// Un titre réduit à son seul sous-titre ne doit pas produire une clé vide : elle se
/// confondrait avec toutes les autres clés vides.
/// </summary>
[Fact]
public void Ne_vide_pas_un_titre_qui_commence_par_le_separateur() =>
Assert.NotEqual(string.Empty, CleOeuvre.Cle(": roman"));
}
+185
View File
@@ -0,0 +1,185 @@
using System.Text;
using MaBibli.Api.Services.Souhaits;
using MaBibli.Shared.Dtos;
namespace MaBibli.Tests;
/// <summary>
/// Contenu <b>exact</b> des fichiers d'export. Fonctions pures : rien à simuler.
/// </summary>
public class ExportSouhaitsTests
{
private static readonly DateTime Jour = new(2026, 8, 18, 10, 0, 0, DateTimeKind.Utc);
private static SouhaitDto Souhait(
int id, string titre, string? auteur = null, string? editeur = null,
string? annee = null, string? isbn = null, string? note = null) =>
new()
{
Id = id,
Titre = titre,
Auteur = auteur,
Editeur = editeur,
Annee = annee,
Isbn = isbn,
Note = note,
DateAjout = Jour,
};
[Fact]
public void Le_texte_groupe_les_livres_par_auteur()
{
var texte = ExportSouhaits.Texte(
[
Souhait(1, "La prophétie des abeilles", "Bernard Werber"),
Souhait(2, "Germinal", "Émile Zola"),
Souhait(3, "Les fourmis", "Bernard Werber"),
],
"Mathieu",
Jour);
Assert.Contains("Liste d'envies de Mathieu — 18/08/2026", texte);
Assert.Contains("3 livres", texte);
// Un seul en-tête par auteur, et ses deux titres regroupés dessous.
var lignes = texte.Split('\n');
Assert.Single(lignes, l => l == "Bernard Werber");
var debutWerber = Array.IndexOf(lignes, "Bernard Werber");
Assert.Equal(" - La prophétie des abeilles", lignes[debutWerber + 1]);
Assert.Equal(" - Les fourmis", lignes[debutWerber + 2]);
// Werber avant Zola : les auteurs sont classés alphabétiquement.
Assert.True(debutWerber < Array.IndexOf(lignes, "Émile Zola"));
}
[Fact]
public void Le_texte_range_les_livres_sans_auteur_en_dernier()
{
var texte = ExportSouhaits.Texte(
[Souhait(1, "Titre orphelin"), Souhait(2, "Germinal", "Émile Zola")],
null,
Jour);
Assert.True(texte.IndexOf("Émile Zola", StringComparison.Ordinal)
< texte.IndexOf("Auteur non précisé", StringComparison.Ordinal));
}
[Fact]
public void Le_texte_porte_les_precisions_utiles_en_librairie()
{
var texte = ExportSouhaits.Texte(
[Souhait(1, "Germinal", "Émile Zola", "Le Livre de poche", "1885", "2253004227", "en poche seulement")],
null,
Jour);
Assert.Contains(" - Germinal (Le Livre de poche, 1885, ISBN 2253004227)", texte);
Assert.Contains(" en poche seulement", texte);
}
[Fact]
public void Le_texte_le_dit_quand_la_liste_est_vide()
{
var texte = ExportSouhaits.Texte([], "Mathieu", Jour);
Assert.Contains("(aucun livre dans la liste)", texte);
}
[Fact]
public void Le_csv_porte_un_en_tete_et_une_ligne_par_envie()
{
var csv = ExportSouhaits.Csv([Souhait(1, "Germinal", "Émile Zola", "Le Livre de poche", "1885")]);
var lignes = csv.Split("\r\n", StringSplitOptions.RemoveEmptyEntries);
Assert.Equal("Titre;Auteur;Éditeur;Année;ISBN;Note;Ajouté le", lignes[0]);
Assert.Equal("Germinal;Émile Zola;Le Livre de poche;1885;;;2026-08-18", lignes[1]);
}
/// <summary>
/// Sans échappement, une note contenant le séparateur décalerait toutes les colonnes
/// suivantes — et une note est du texte parfaitement libre.
/// </summary>
[Fact]
public void Le_csv_echappe_le_separateur_et_les_guillemets()
{
var csv = ExportSouhaits.Csv(
[Souhait(1, "Titre; piégé", note: "il a dit \"oui\"")]);
var ligne = csv.Split("\r\n")[1];
Assert.StartsWith("\"Titre; piégé\";", ligne);
Assert.Contains("\"il a dit \"\"oui\"\"\"", ligne);
// Sept colonnes une fois l'échappement respecté, pas huit.
Assert.Equal(7, ColonnesCsv(ligne).Count);
}
[Fact]
public void Le_csv_echappe_les_sauts_de_ligne()
{
var csv = ExportSouhaits.Csv([Souhait(1, "Titre", note: "ligne 1\nligne 2")]);
Assert.Contains("\"ligne 1\nligne 2\"", csv);
}
/// <summary>
/// Sans BOM, Excel affiche « Émile Zola » — sur un fonds français, à peu près chaque ligne.
/// </summary>
[Fact]
public void Le_bom_precede_le_csv()
{
var octets = ExportSouhaits.Bom
.Concat(Encoding.UTF8.GetBytes(ExportSouhaits.Csv([Souhait(1, "Émile")])))
.ToArray();
Assert.Equal([0xEF, 0xBB, 0xBF], octets[..3]);
}
[Theory]
[InlineData(FormatExportSouhaits.Texte, "liste-envies-2026-08-18.txt", "text/plain; charset=utf-8")]
[InlineData(FormatExportSouhaits.Csv, "liste-envies-2026-08-18.csv", "text/csv; charset=utf-8")]
public void Le_fichier_est_date_et_typé(FormatExportSouhaits format, string nom, string mime)
{
Assert.Equal(nom, ExportSouhaits.NomFichier(format, Jour));
Assert.Equal(mime, ExportSouhaits.TypeMime(format));
}
/// <summary>Découpe une ligne CSV en respectant les guillemets (lecteur minimal, pour le test).</summary>
private static List<string> ColonnesCsv(string ligne)
{
var colonnes = new List<string>();
var courante = new StringBuilder();
var dansGuillemets = false;
for (var i = 0; i < ligne.Length; i++)
{
var c = ligne[i];
if (c == '"')
{
if (dansGuillemets && i + 1 < ligne.Length && ligne[i + 1] == '"')
{
courante.Append('"');
i++;
continue;
}
dansGuillemets = !dansGuillemets;
continue;
}
if (c == ExportSouhaits.SeparateurCsv && !dansGuillemets)
{
colonnes.Add(courante.ToString());
courante.Clear();
continue;
}
courante.Append(c);
}
colonnes.Add(courante.ToString());
return colonnes;
}
}
@@ -0,0 +1,361 @@
<?xml version="1.0" encoding="UTF-8"?><srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/" xmlns="http://catalogue.bnf.fr/namespaces/InterXMarc" xmlns:ixm="http://catalogue.bnf.fr/namespaces/InterXMarc" xmlns:mn="http://catalogue.bnf.fr/namespaces/motsnotices" xmlns:sd="http://www.loc.gov/zing/srw/diagnostic/">
<srw:version>1.2</srw:version>
<srw:echoedSearchRetrieveRequest>
<srw:version>1.2</srw:version>
<srw:query>bib.author all "Bernard Werber"</srw:query>
</srw:echoedSearchRetrieveRequest>
<srw:numberOfRecords>203</srw:numberOfRecords>
<srw:records>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43683323s</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>2012</dc:date> <dc:format>23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb43683323s</srw:recordIdentifier>
<srw:recordPosition>1</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20131002</ixm:attr>
<ixm:attr name="LastModificationDate">20131002</ixm:attr>
<mn:score>12.24525</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43745025g</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éd. France loisirs (Paris)</dc:publisher> <dc:date>2013</dc:date> <dc:format>23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb43745025g</srw:recordIdentifier>
<srw:recordPosition>2</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20140116</ixm:attr>
<ixm:attr name="LastModificationDate">20140116</ixm:attr>
<mn:score>12.24525</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb42777895t</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>A. Michel (Paris)</dc:publisher> <dc:date>2012</dc:date> <dc:format>23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb42777895t</srw:recordIdentifier>
<srw:recordPosition>3</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20121024</ixm:attr>
<ixm:attr name="LastModificationDate">20121024</ixm:attr>
<mn:score>12.24525</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43683332r</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>2012</dc:date> <dc:format>23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb43683332r</srw:recordIdentifier>
<srw:recordPosition>4</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20131002</ixm:attr>
<ixm:attr name="LastModificationDate">20131002</ixm:attr>
<mn:score>12.2451105</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb39269846w</dc:identifier> <dc:title>Nous, les dieux / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>A. Michel (Paris)</dc:publisher> <dc:date>2004</dc:date> <dc:format>24 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb39269846w</srw:recordIdentifier>
<srw:recordPosition>5</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20041103</ixm:attr>
<ixm:attr name="LastModificationDate">20071205</ixm:attr>
<mn:score>12.2451105</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb40039497k</dc:identifier> <dc:title>Nous, les dieux / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éd. France loisirs (Paris)</dc:publisher> <dc:date>2005</dc:date> <dc:format>21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb40039497k</srw:recordIdentifier>
<srw:recordPosition>6</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20050922</ixm:attr>
<ixm:attr name="LastModificationDate">20081030</ixm:attr>
<mn:score>12.2451105</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb47615775q</dc:identifier> <dc:title>L'encyclopédie du savoir relatif et absolu / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Albin Michel (Paris)</dc:publisher> <dc:date>2018</dc:date> <dc:format>24 et 23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb47615775q</srw:recordIdentifier>
<srw:recordPosition>7</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20241211</ixm:attr>
<ixm:attr name="LastModificationDate">20241211</ixm:attr>
<mn:score>12.244974</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb39900329q</dc:identifier> <dc:title>Nous, les dieux : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>2004</dc:date> <dc:format>25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb39900329q</srw:recordIdentifier>
<srw:recordPosition>8</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20050106</ixm:attr>
<ixm:attr name="LastModificationDate">20071205</ixm:attr>
<mn:score>12.244974</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb444898718</dc:identifier> <dc:title>Troisième humanité / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Livre de poche (Paris)</dc:publisher> <dc:date>2015</dc:date> <dc:description>Collection : Le livre de poche</dc:description> <dc:format>18 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb444898718</srw:recordIdentifier>
<srw:recordPosition>9</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20160118</ixm:attr>
<ixm:attr name="LastModificationDate">20160118</ixm:attr>
<mn:score>12.244841</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb409564505</dc:identifier> <dc:title>Nous, les dieux / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Librairie générale française (Paris)</dc:publisher> <dc:date>2006</dc:date> <dc:description>Collection : Le livre de poche</dc:description> <dc:format>18 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb409564505</srw:recordIdentifier>
<srw:recordPosition>10</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20061215</ixm:attr>
<ixm:attr name="LastModificationDate">20061215</ixm:attr>
<mn:score>12.244579</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35551566h</dc:identifier> <dc:title>Le jour des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>1992</dc:date> <dc:format>463 p. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35551566h</srw:recordIdentifier>
<srw:recordPosition>11</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19930209</ixm:attr>
<ixm:attr name="LastModificationDate">19930223</ixm:attr>
<mn:score>12.244083</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb357335589</dc:identifier> <dc:title>Les thanatonautes / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>France loisirs (Paris)</dc:publisher> <dc:date>1994</dc:date> <dc:identifier>ISBN 2724280946</dc:identifier> <dc:format>450 p. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb357335589</srw:recordIdentifier>
<srw:recordPosition>12</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19950130</ixm:attr>
<ixm:attr name="LastModificationDate">19950209</ixm:attr>
<mn:score>12.243847</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35720318k</dc:identifier> <dc:title>Les thanatonautes : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>1994</dc:date> <dc:format>450 p. : couv. ill. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35720318k</srw:recordIdentifier>
<srw:recordPosition>13</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19941114</ixm:attr>
<ixm:attr name="LastModificationDate">19941201</ixm:attr>
<mn:score>12.243847</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43699999b</dc:identifier> <dc:title>Les fourmis / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Livre de poche jeunesse (Paris)</dc:publisher> <dc:date>2013</dc:date> <dc:description>Collection : Le livre de poche. Jeunesse. Fictions</dc:description> <dc:format>18 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb43699999b</srw:recordIdentifier>
<srw:recordPosition>14</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20131025</ixm:attr>
<ixm:attr name="LastModificationDate">20210730</ixm:attr>
<mn:score>12.243847</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35828640p</dc:identifier> <dc:title>La révolution des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>1996</dc:date> <dc:format>533 p. : couv. ill. en coul. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35828640p</srw:recordIdentifier>
<srw:recordPosition>15</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19960806</ixm:attr>
<ixm:attr name="LastModificationDate">19960906</ixm:attr>
<mn:score>12.243619</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb36193471k</dc:identifier> <dc:title>Le livre du voyage / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>le Grand livre du mois (Paris)</dc:publisher> <dc:date>1997</dc:date> <dc:format>127 p. : couv. ill. en coul. ; 22 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb36193471k</srw:recordIdentifier>
<srw:recordPosition>16</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19980108</ixm:attr>
<ixm:attr name="LastModificationDate">19980206</ixm:attr>
<mn:score>12.243619</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb358187920</dc:identifier> <dc:title>La révolution des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>A. Michel (Paris)</dc:publisher> <dc:date>1996</dc:date> <dc:identifier>ISBN 2226086366</dc:identifier> <dc:format>533 p. : couv. ill. en coul. ; 24 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb358187920</srw:recordIdentifier>
<srw:recordPosition>17</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19960620</ixm:attr>
<ixm:attr name="LastModificationDate">19960712</ixm:attr>
<mn:score>12.243619</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb361686335</dc:identifier> <dc:title>La révolution des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>France loisirs (Paris)</dc:publisher> <dc:date>1997</dc:date> <dc:identifier>ISBN 2744104000</dc:identifier> <dc:format>533 p. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb361686335</srw:recordIdentifier>
<srw:recordPosition>18</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970611</ixm:attr>
<ixm:attr name="LastModificationDate">19970711</ixm:attr>
<mn:score>12.243619</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35492416k</dc:identifier> <dc:title>Les fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>France loisirs (Paris)</dc:publisher> <dc:date>1991</dc:date> <dc:identifier>ISBN 2724265696</dc:identifier> <dc:format>351 p. ; 25 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35492416k</srw:recordIdentifier>
<srw:recordPosition>19</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19920127</ixm:attr>
<ixm:attr name="LastModificationDate">20160128</ixm:attr>
<mn:score>12.243619</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35540108s</dc:identifier> <dc:title>Le jour des fourmis : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>A. Michel (Paris)</dc:publisher> <dc:date>1992</dc:date> <dc:identifier>ISBN 2226061185</dc:identifier> <dc:format>463 p. : couv. ill. en coul. ; 24 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35540108s</srw:recordIdentifier>
<srw:recordPosition>20</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19921204</ixm:attr>
<ixm:attr name="LastModificationDate">19921222</ixm:attr>
<mn:score>12.243619</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb370268608</dc:identifier> <dc:title>Le livre du voyage / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>France loisirs (Paris)</dc:publisher> <dc:date>1998</dc:date> <dc:identifier>ISBN 2744115509</dc:identifier> <dc:format>127 p. : jaquette ill. ; 22 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb370268608</srw:recordIdentifier>
<srw:recordPosition>21</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19980921</ixm:attr>
<ixm:attr name="LastModificationDate">19990402</ixm:attr>
<mn:score>12.243397</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb42606801s</dc:identifier> <dc:title>Le rire du Cyclope / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éd. France loisirs (Paris)</dc:publisher> <dc:date>2011</dc:date> <dc:identifier>ISBN 9782298045680</dc:identifier> <dc:description>Code à barres commercial : EAN 9782298045680</dc:description> <dc:format>1 vol. (816 p.) : jaquette ill. en coul. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb42606801s</srw:recordIdentifier>
<srw:recordPosition>22</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20120221</ixm:attr>
<ixm:attr name="LastModificationDate">20120329</ixm:attr>
<mn:score>12.243397</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb45372631j</dc:identifier> <dc:title>Demain les chats : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éditions France loisirs (Paris)</dc:publisher> <dc:date>2017</dc:date> <dc:identifier>ISBN 9782298122015</dc:identifier> <dc:description>Code à barres commercial : EAN 9782298122015</dc:description> <dc:format>1 vol. (335 p.) ; 20 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb45372631j</srw:recordIdentifier>
<srw:recordPosition>23</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20171018</ixm:attr>
<ixm:attr name="LastModificationDate">20171207</ixm:attr>
<mn:score>12.243397</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35807212p</dc:identifier> <dc:title>Les thanatonautes : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Librairie générale française (Paris)</dc:publisher> <dc:date>1996</dc:date> <dc:description>Collection : Le livre de poche ; 13922</dc:description> <dc:identifier>ISBN 225313922X</dc:identifier> <dc:format>502 p. : couv. ill. ; 18 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35807212p</srw:recordIdentifier>
<srw:recordPosition>24</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19960417</ixm:attr>
<ixm:attr name="LastModificationDate">19960517</ixm:attr>
<mn:score>12.243397</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb45159572s</dc:identifier> <dc:title>Demain les chats : roman / Bernard Werber</dc:title> <dc:creator>Werber, Bernard (1961-....). Auteur du texte</dc:creator> <dc:publisher>Éditions France loisirs (Paris)</dc:publisher> <dc:date>2016</dc:date> <dc:identifier>ISBN 9782298119732</dc:identifier> <dc:description>Code à barres commercial : EAN 9782298119732</dc:description> <dc:format>1 vol. (307 p.) ; 23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb45159572s</srw:recordIdentifier>
<srw:recordPosition>25</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20161117</ixm:attr>
<ixm:attr name="LastModificationDate">20161222</ixm:attr>
<mn:score>12.243397</mn:score>
</srw:extraRecordData>
</srw:record>
</srw:records>
<srw:nextRecordPosition>26</srw:nextRecordPosition>
</srw:searchRetrieveResponse>
+361
View File
@@ -0,0 +1,361 @@
<?xml version="1.0" encoding="UTF-8"?><srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/" xmlns="http://catalogue.bnf.fr/namespaces/InterXMarc" xmlns:ixm="http://catalogue.bnf.fr/namespaces/InterXMarc" xmlns:mn="http://catalogue.bnf.fr/namespaces/motsnotices" xmlns:sd="http://www.loc.gov/zing/srw/diagnostic/">
<srw:version>1.2</srw:version>
<srw:echoedSearchRetrieveRequest>
<srw:version>1.2</srw:version>
<srw:query>bib.author all "Emile Zola"</srw:query>
</srw:echoedSearchRetrieveRequest>
<srw:numberOfRecords>2692</srw:numberOfRecords>
<srw:records>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb32359518t</dc:identifier> <dc:title>Zola und die Tiere... / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>I. Graetz-Verlag (Berlin-Charlottenburg)</dc:publisher> <dc:date>1933</dc:date> <dc:description>Collection : Entr'act-Bücherei ; 3</dc:description> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:format>In-8°, 40 p., pl.</dc:format> <dc:language>ger</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb32359518t</srw:recordIdentifier>
<srw:recordPosition>1</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970701</ixm:attr>
<ixm:attr name="LastModificationDate">20191219</ixm:attr>
<mn:score>11.365902</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb370961796</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>B. Grasset (Paris)</dc:publisher> <dc:date>2000</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:identifier>ISBN 2246172632</dc:identifier> <dc:format>280 p. : couv. ill. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb370961796</srw:recordIdentifier>
<srw:recordPosition>2</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20000322</ixm:attr>
<ixm:attr name="LastModificationDate">20190914</ixm:attr>
<mn:score>11.36502</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb38668184t</dc:identifier> <dc:title>Zola et Cézanne d'après une correspondance retrouvée</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:date>1931</dc:date> <dc:format>Pag. 39-58 ; In-8 °</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb38668184t</srw:recordIdentifier>
<srw:recordPosition>3</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20041209</ixm:attr>
<ixm:attr name="LastModificationDate">20041209</ixm:attr>
<mn:score>11.364924</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb33073483k</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>Cercle du bibliophile (Levallois-Perret)</dc:publisher> <dc:date>1966</dc:date> <dc:description>Collection : Cercle du bibliophile</dc:description> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:format>1 vol. (290 p.) : pl., fac-sim. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb33073483k</srw:recordIdentifier>
<srw:recordPosition>4</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970701</ixm:attr>
<ixm:attr name="LastModificationDate">20130418</ixm:attr>
<mn:score>11.364755</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb34870862h</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>B. Grasset (Paris)</dc:publisher> <dc:date>1986</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:identifier>ISBN 2246172624</dc:identifier> <dc:format>289 p. : couv. ill. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb34870862h</srw:recordIdentifier>
<srw:recordPosition>5</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19871004</ixm:attr>
<ixm:attr name="LastModificationDate">20190915</ixm:attr>
<mn:score>11.36381</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb32359517g</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>Fasquelle, éditeur (Paris)</dc:publisher> <dc:date>1931</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:format>1 vol. (266 p. et portrait) ; in-8</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb32359517g</srw:recordIdentifier>
<srw:recordPosition>6</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970701</ixm:attr>
<ixm:attr name="LastModificationDate">20170503</ixm:attr>
<mn:score>11.363017</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb386685362</dc:identifier> <dc:title>Paul Alexis, ami des peintres, bohême et critique d'art</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>Le mercure de France (Paris)</dc:publisher> <dc:date>1939</dc:date> <dc:subject xml:lang="fre">Alexis, Paul (1847-1901)</dc:subject> <dc:format>1 pièce (paginé 293-303) ; in-8</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb386685362</srw:recordIdentifier>
<srw:recordPosition>7</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20041209</ixm:attr>
<ixm:attr name="LastModificationDate">20070301</ixm:attr>
<mn:score>11.362901</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb45696031s</dc:identifier> <dc:title>Émile Zola raconté par sa fille / Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>Bernard Grasset (Paris)</dc:publisher> <dc:date>2019</dc:date> <dc:description>Collection : Les cahiers rouges</dc:description> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:identifier>ISBN 9782246820376</dc:identifier> <dc:description>Code à barres commercial : EAN 9782246820376</dc:description> <dc:format>1 vol. (299 p.) ; 19 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb45696031s</srw:recordIdentifier>
<srw:recordPosition>8</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20190326</ixm:attr>
<ixm:attr name="LastModificationDate">20190914</ixm:attr>
<mn:score>11.361762</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb34430876p</dc:identifier> <dc:title>Bulletin de la Société littéraire des amis d'Émile Zola</dc:title> <dc:creator>Société littéraire des amis d'Émile Zola. Auteur du texte</dc:creator> <dc:publisher>[s.n.] (Paris)</dc:publisher> <dc:date>1922-1938</dc:date> <dc:identifier>ISSN 11536284</dc:identifier> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Critique et interprétation</dc:subject> <dc:subject xml:lang="fre">Naturalisme (mouvement littéraire) -- Critique et interprétation</dc:subject> <dc:language>fre</dc:language> <dc:type xml:lang="fre">publication en série imprimée</dc:type> <dc:type xml:lang="eng">printed serial</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb34430876p</srw:recordIdentifier>
<srw:recordPosition>9</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19900919</ixm:attr>
<ixm:attr name="LastModificationDate">20210317</ixm:attr>
<mn:score>11.290642</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb343484129</dc:identifier> <dc:title>Les Cahiers naturalistes (Paris)</dc:title> <dc:title>Les Cahiers naturalistes : bulletin officiel de la Société littéraire des amis d'Emile Zola</dc:title> <dc:creator>Société littéraire des amis d'Émile Zola. Auteur du texte / Autrice du texte</dc:creator> <dc:publisher>Grasset-Fasquelle (Paris)</dc:publisher> <dc:date>1955</dc:date> <dc:identifier>ISSN 00080365</dc:identifier> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Critique et interprétation</dc:subject> <dc:subject xml:lang="fre">Naturalisme (mouvement littéraire) -- Critique et interprétation</dc:subject> <dc:subject xml:lang="fre">Dreyfus, Affaire (1894-1906)</dc:subject> <dc:language>fre</dc:language> <dc:type xml:lang="fre">publication en série imprimée</dc:type> <dc:type xml:lang="eng">printed serial</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb343484129</srw:recordIdentifier>
<srw:recordPosition>10</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19860728</ixm:attr>
<ixm:attr name="LastModificationDate">20190919</ixm:attr>
<mn:score>11.28979</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb323595164</dc:identifier> <dc:title>Émile Zola / par Denise Le Blond-Zola</dc:title> <dc:creator>Le Blond-Zola, Denise (1889-1942). Auteur du texte</dc:creator> <dc:publisher>F. Bernouard (Paris)</dc:publisher> <dc:date>1927</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902)</dc:subject> <dc:format>In-8°, 38 p., couv. en coul.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb323595164</srw:recordIdentifier>
<srw:recordPosition>11</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970701</ixm:attr>
<ixm:attr name="LastModificationDate">20130418</ixm:attr>
<mn:score>11.288839</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb320764010</dc:identifier> <dc:title>Formes mortelles du botulisme / Dr François Émile-Zola,...</dc:title> <dc:creator>Émile-Zola, François (1917-1989). Auteur du texte</dc:creator> <dc:date>1946</dc:date> <dc:format>92 p. ; In-8° (245 x 160)</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb320764010</srw:recordIdentifier>
<srw:recordPosition>12</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970701</ixm:attr>
<ixm:attr name="LastModificationDate">20180606</ixm:attr>
<mn:score>11.266872</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35887568s</dc:identifier> <dc:title>Réponse à la pl ̤. de la R ̤. L. les Jacobins sur les recrutement, discipline et régénération maç ̤. ; (et) Rapport sur l'amnistie générale... / L ̤. Emile Zola n° 382</dc:title> <dc:creator>Loge Émile Zola (Paris). Auteur du texte</dc:creator> <dc:publisher>l'Emancipatrice (Paris)</dc:publisher> <dc:date>1927</dc:date> <dc:format>12 p. ; 27 cm</dc:format> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35887568s</srw:recordIdentifier>
<srw:recordPosition>13</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19901220</ixm:attr>
<ixm:attr name="LastModificationDate">19901220</ixm:attr>
<mn:score>11.172066</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35311116p</dc:identifier> <dc:title>La Question de l'imprimerie mac ̤. : rapport présenté... / à la L ̤. Emile Zola... ; [par le] F ̤. Frédéric Tizorin,..</dc:title> <dc:contributor>Tizorin, Frédéric. Rédacteur</dc:contributor> <dc:creator>Loge Émile Zola (Paris). Auteur du texte</dc:creator> <dc:publisher>l'Emancipatrice (Paris)</dc:publisher> <dc:date>1910</dc:date> <dc:format>15 p. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35311116p</srw:recordIdentifier>
<srw:recordPosition>14</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19910212</ixm:attr>
<ixm:attr name="LastModificationDate">20151202</ixm:attr>
<mn:score>11.106373</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb30082301p</dc:identifier> <dc:title>Lycée de Rennes. Le centenaire de Michelet, discours prononcé dans la salle des fêtes du lycée, le 13 juillet 1898 / par M. Benaerts,...</dc:title> <dc:creator>Benaerts, Louis (1868-1941). Auteur du texte</dc:creator> <dc:contributor>Lycée et collège Émile Zola (Rennes). Éditeur scientifique</dc:contributor> <dc:publisher>impr. des Arts et manufactures (Rennes)</dc:publisher> <dc:date>1898</dc:date> <dc:subject xml:lang="fre">Michelet, Jules (1798-1874) -- Anniversaires</dc:subject> <dc:format>In-8° , 35 p.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb30082301p</srw:recordIdentifier>
<srw:recordPosition>15</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19950501</ixm:attr>
<ixm:attr name="LastModificationDate">20180314</ixm:attr>
<mn:score>10.988168</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb33902955k</dc:identifier> <dc:title>Rapport sur la création de cours obligatoires d'instruction et d'éducation pour l'adolescence, par le F @ Henry-Marx,... / G @ L @ D @ F @ L @ Emile-Zola</dc:title> <dc:creator>Marx, Henry. Auteur du texte</dc:creator> <dc:creator>Loge Émile Zola (Paris). Auteur du texte</dc:creator> <dc:publisher>Impr. L'Emancipatrice (Paris)</dc:publisher> <dc:date>1912</dc:date> <dc:format>In-8°, 11 p.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb33902955k</srw:recordIdentifier>
<srw:recordPosition>16</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970701</ixm:attr>
<ixm:attr name="LastModificationDate">20200513</ixm:attr>
<mn:score>10.98049</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb412345498</dc:identifier> <dc:title>Mes étés à Brienne / Brigitte Émile-Zola ; préface d'Alain Pagès...</dc:title> <dc:creator>Émile-Zola, Brigitte (1944-....). Auteur du texte</dc:creator> <dc:contributor>Pagès, Alain (1950-....). Préfacier</dc:contributor> <dc:publisher>Éd. du Frisson esthétique (agneaux)</dc:publisher> <dc:date>2008</dc:date> <dc:description>Collection : Collection Sources</dc:description> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Famille</dc:subject> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Résidences et lieux familiers -- Brienne-le-Château (Aube, France)</dc:subject> <dc:identifier>ISBN 9782917440001</dc:identifier> <dc:description>Code à barres commercial : EAN 9782917440001</dc:description> <dc:format>1 vol. (189 p.) : ill., couv. ill. ; 23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb412345498</srw:recordIdentifier>
<srw:recordPosition>17</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20080404</ixm:attr>
<ixm:attr name="LastModificationDate">20230110</ixm:attr>
<mn:score>10.976976</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb31241888c</dc:identifier> <dc:title>Université de France. Collège royal de Rennes. Discours prononcé à la distribution solennelle des prix, le 13 août 1845. [Signé : Antonin Rondelet]</dc:title> <dc:creator>Rondelet, Antonin (1823-1893). Auteur du texte</dc:creator> <dc:contributor>Lycée et collège Émile Zola (Rennes). Éditeur scientifique</dc:contributor> <dc:publisher>impr. de L. Boitel (Lyon)</dc:publisher> <dc:date>1845</dc:date> <dc:format>In-8° , 15 p.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb31241888c</srw:recordIdentifier>
<srw:recordPosition>18</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970701</ixm:attr>
<ixm:attr name="LastModificationDate">19970701</ixm:attr>
<mn:score>10.89491</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb452689930</dc:identifier> <dc:title>Contes à Ninon et Nouveaux contes à Ninon / Émile Zola... ; précédés d'une Vie de l'auteur par Denise Le Blond-Zola</dc:title> <dc:creator>Zola, Émile (1840-1902). Auteur du texte</dc:creator> <dc:contributor>Le Blond-Zola, Denise (1889-1942). Préfacier</dc:contributor> <dc:publisher>typogr. François Bernouard (Paris)</dc:publisher> <dc:date>1927</dc:date> <dc:format>1 vol. ; in-8</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb452689930</srw:recordIdentifier>
<srw:recordPosition>19</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20170511</ixm:attr>
<ixm:attr name="LastModificationDate">20170511</ixm:attr>
<mn:score>10.85401</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb34978585f</dc:identifier> <dc:title>Zola et l'affiche : exposition du 5 octobre 1986 au 29 mars 1987, Maison d'Émile Zola...</dc:title> <dc:contributor>Musée Émile Zola (Médan, Yvelines). Collaborateur</dc:contributor> <dc:publisher>Association du Musée Émile Zola (Médan)</dc:publisher> <dc:date>1986</dc:date> <dc:subject xml:lang="fre">Zola, Émile (1840-1902) -- Illustrations</dc:subject> <dc:subject xml:lang="fre">Affiches de cinéma -- France</dc:subject> <dc:subject xml:lang="fre">Roman français -- Affiches</dc:subject> <dc:description>[Exposition. Médan. 1986/1987]</dc:description> <dc:format>19 f.</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb34978585f</srw:recordIdentifier>
<srw:recordPosition>20</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19881216</ixm:attr>
<ixm:attr name="LastModificationDate">20010220</ixm:attr>
<mn:score>10.828315</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb346101384</dc:identifier> <dc:title>"Amoco Cadiz" / Émile Zola</dc:title> <dc:creator>Zola, Émile (auteur prétendu). Auteur du texte</dc:creator> <dc:publisher>Baudouin (Paris)</dc:publisher> <dc:date>1978</dc:date> <dc:format>Non paginé ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb346101384</srw:recordIdentifier>
<srw:recordPosition>21</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19860717</ixm:attr>
<ixm:attr name="LastModificationDate">20100925</ixm:attr>
<mn:score>10.777203</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb43908217n</dc:identifier> <dc:title>Lettres à Alexandrine : 1876-1901 / Émile Zola ; édition établie, présentée et annotée par Brigitte Émile-Zola et Alain Pagès ; avec la collaboration de Céline Grenaud-Tostain, Sophie Guermès, Jean-Sébastien Macke... [et al.]</dc:title> <dc:creator>Zola, Émile (1840-1902). Auteur du texte</dc:creator> <dc:contributor>Émile-Zola, Brigitte (1944-....). Éditeur scientifique</dc:contributor> <dc:contributor>Pagès, Alain (1950-....). Éditeur scientifique</dc:contributor> <dc:contributor>Zola, Alexandrine (1839-1925). Destinataire de lettres</dc:contributor> <dc:publisher>Gallimard (Paris)</dc:publisher> <dc:date>2014</dc:date> <dc:identifier>ISBN 9782070139217</dc:identifier> <dc:description>Code à barres commercial : EAN 9782070139217</dc:description> <dc:format>1 vol. (813 p.) ; 23 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb43908217n</srw:recordIdentifier>
<srw:recordPosition>22</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">20141003</ixm:attr>
<ixm:attr name="LastModificationDate">20250612</ixm:attr>
<mn:score>10.722156</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb332677407</dc:identifier> <dc:title>Catalogue des ouvrages légués par M. Saint-Albin Reynaud,...</dc:title> <dc:contributor>Médiathèque centrale d'agglomération Émile Zola (Montpellier). Éditeur scientifique</dc:contributor> <dc:publisher>imp. de Grollier (Montpellier)</dc:publisher> <dc:date>1881</dc:date> <dc:description>[Collection. Saint-Albin Reynaud]</dc:description> <dc:format>In-8°</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb332677407</srw:recordIdentifier>
<srw:recordPosition>23</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19970701</ixm:attr>
<ixm:attr name="LastModificationDate">19970701</ixm:attr>
<mn:score>10.693977</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb349977019</dc:identifier> <dc:title>Les Fables de La Fontaine : [exposition], Musée Fabre, 2 mars-15 avril 1984, Ville de Montpellier, Bibliothèque municipale</dc:title> <dc:creator>Médiathèque centrale d'agglomération Émile Zola (Montpellier). Auteur du texte</dc:creator> <dc:publisher>Bibliothèque municipale (Montpellier)</dc:publisher> <dc:date>1984</dc:date> <dc:subject xml:lang="fre">La Fontaine, Jean de (1621-1695). Fables</dc:subject> <dc:description>[Exposition. Montpellier. 1984]</dc:description> <dc:format>[50] p. : ill., couv. ill. ; 21 cm</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb349977019</srw:recordIdentifier>
<srw:recordPosition>24</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19890214</ixm:attr>
<ixm:attr name="LastModificationDate">20190912</ixm:attr>
<mn:score>10.693712</mn:score>
</srw:extraRecordData>
</srw:record>
<srw:record>
<srw:recordSchema>dc</srw:recordSchema>
<srw:recordPacking>xml</srw:recordPacking>
<srw:recordData>
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai_dc/ http://www.openarchives.org/OAI/2.0/oai_dc.xsd"> <dc:identifier>http://catalogue.bnf.fr/ark:/12148/cb35749705x</dc:identifier> <dc:title>Rabelais à la Bibliothèque municipale : [coffret de 16 cartes postales] / Bibliothèque municipale</dc:title> <dc:creator>Médiathèque centrale d'agglomération Émile Zola (Montpellier). Auteur du texte</dc:creator> <dc:publisher>Bibliothèque municipale (Montpellier)</dc:publisher> <dc:date>1994</dc:date> <dc:subject xml:lang="fre">Cavalier, Calixte (1820-1888) -- Bibliothèque</dc:subject> <dc:subject xml:lang="fre">Rabelais, François (1494?-1553)</dc:subject> <dc:format>16 cartes postales dans un coffret : ill. en noir et en coul. ; 16 cm + avec une notice introductive</dc:format> <dc:language>fre</dc:language> <dc:type xml:lang="fre">texte imprimé</dc:type> <dc:type xml:lang="eng">printed text</dc:type> <dc:type xml:lang="eng">text</dc:type> <dc:rights xml:lang="fre">Catalogue en ligne de la Bibliothèque nationale de France</dc:rights> <dc:rights xml:lang="eng">French National Library online Catalog</dc:rights>
</oai_dc:dc> </srw:recordData>
<srw:recordIdentifier>ark:/12148/cb35749705x</srw:recordIdentifier>
<srw:recordPosition>25</srw:recordPosition>
<srw:extraRecordData>
<ixm:attr name="CreationDate">19950524</ixm:attr>
<ixm:attr name="LastModificationDate">20190919</ixm:attr>
<mn:score>10.693217</mn:score>
</srw:extraRecordData>
</srw:record>
</srw:records>
<srw:nextRecordPosition>26</srw:nextRecordPosition>
</srw:searchRetrieveResponse>
+16
View File
@@ -24,6 +24,14 @@ public class IsbnLookupServiceTests
IsbnInterroges.Add(isbn);
return Task.FromResult((Reponse(isbn), Avertissement));
}
/// <summary>
/// Ces tests ne portent que sur la cascade ISBN : la recherche par auteur n'y intervient
/// pas, mais l'interface l'exige. Elle est couverte par BnfBibliographieParserTests.
/// </summary>
public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
string auteur, CancellationToken ct = default) =>
Task.FromResult((new ResultatBibliographie(), (string?)null));
}
private sealed class OpenLibraryFake : IOpenLibraryClient
@@ -181,5 +189,13 @@ public class IsbnLookupServiceTests
DerniereCouverture = urlCouverture;
return Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));
}
/// <summary>
/// Ces tests ne portent que sur la cascade ISBN : la recherche par auteur n'y intervient
/// pas, mais l'interface l'exige. Elle est couverte par BnfBibliographieParserTests.
/// </summary>
public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
string auteur, CancellationToken ct = default) =>
Task.FromResult((new ResultatBibliographie(), (string?)null));
}
}
+338
View File
@@ -0,0 +1,338 @@
using MaBibli.Api.Data;
using MaBibli.Api.Services.Catalogue;
using MaBibli.Api.Services.Isbn;
using MaBibli.Api.Services.Souhaits;
using MaBibli.Shared.Catalogue;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace MaBibli.Tests;
/// <summary>
/// Liste d'envies, sur une base SQLite <b>en mémoire</b> : l'unicité « une œuvre par personne »
/// n'existe qu'en base, elle ne se vérifierait pas sur un faux dépôt.
/// </summary>
/// <remarks>
/// L'invariant central testé ici est la <b>portée personnelle</b> : deux utilisateurs ne doivent
/// jamais se voir. C'est l'exact contraire du catalogue, commun au foyer — et une régression y
/// serait discrète, puisque tout continuerait de « marcher » avec un seul compte.
/// </remarks>
public class ServiceSouhaitsTests : IDisposable
{
private readonly SqliteConnection _connexion;
private readonly MaBibliDbContext _db;
private readonly ServiceSouhaits _souhaits;
private readonly ServiceCatalogue _catalogue;
private const string Mathieu = "mathieu";
private const string Camille = "camille";
public ServiceSouhaitsTests()
{
_connexion = new SqliteConnection("Data Source=:memory:");
_connexion.Open();
var options = new DbContextOptionsBuilder<MaBibliDbContext>()
.UseSqlite(_connexion)
.Options;
_db = new MaBibliDbContext(options);
_db.Database.EnsureCreated();
_souhaits = new ServiceSouhaits(_db);
_catalogue = new ServiceCatalogue(_db, new ServiceAuteurs(_db));
}
private Task<ResultatSouhait> AjouterAsync(
string titre, string? auteur = "Bernard Werber", string utilisateur = Mathieu,
string? isbn = null, string? note = null) =>
_souhaits.AjouterAsync(
new EnregistrementSouhait { Titre = titre, Auteur = auteur, Isbn = isbn, Note = note },
utilisateur);
// ── Portée personnelle ───────────────────────────────────────────────────
[Fact]
public async Task Deux_utilisateurs_ont_deux_listes_distinctes()
{
await AjouterAsync("Les fourmis", utilisateur: Mathieu);
await AjouterAsync("Germinal", "Émile Zola", Camille);
var deMathieu = await _souhaits.ListerAsync(Mathieu);
var deCamille = await _souhaits.ListerAsync(Camille);
Assert.Equal("Les fourmis", Assert.Single(deMathieu).Titre);
Assert.Equal("Germinal", Assert.Single(deCamille).Titre);
}
[Fact]
public async Task Deux_utilisateurs_peuvent_souhaiter_le_meme_livre()
{
var premier = await AjouterAsync("Les fourmis", utilisateur: Mathieu);
var second = await AjouterAsync("Les fourmis", utilisateur: Camille);
// L'unicité porte sur (utilisateur, œuvre) : elle ne doit pas empêcher deux personnes
// d'avoir la même envie — c'est même le cas normal dans un foyer.
Assert.Null(premier.Erreur);
Assert.Null(second.Erreur);
}
[Fact]
public async Task On_ne_supprime_pas_l_envie_d_un_autre()
{
var souhait = await AjouterAsync("Les fourmis", utilisateur: Mathieu);
var supprime = await _souhaits.SupprimerAsync(souhait.Souhait!.Id, Camille);
Assert.False(supprime);
Assert.Single(await _souhaits.ListerAsync(Mathieu));
}
[Fact]
public async Task Sans_identite_il_n_y_a_ni_liste_ni_ecriture()
{
var ajout = await AjouterAsync("Les fourmis", utilisateur: null!);
Assert.NotNull(ajout.Erreur);
Assert.Empty(await _souhaits.ListerAsync(null));
}
// ── Cloisonnement d'avec le catalogue ────────────────────────────────────
/// <summary>
/// L'invariant qui justifie l'entité séparée : une envie n'entre ni dans le catalogue, ni
/// dans les compteurs, ni dans la table des auteurs.
/// </summary>
[Fact]
public async Task Une_envie_n_apparait_pas_dans_le_catalogue()
{
await AjouterAsync("La prophétie des abeilles", "Bernard Werber");
var catalogue = await _catalogue.ListerAsync(new CritereLivres(), Mathieu);
Assert.Empty(catalogue);
}
[Fact]
public async Task Une_envie_ne_cree_pas_de_fiche_auteur()
{
await AjouterAsync("La prophétie des abeilles", "Bernard Werber");
// Sinon « Bernard Werber » apparaîtrait sur l'écran des auteurs avec « 0 livre »,
// et trahirait au passage la liste d'envies de son propriétaire.
Assert.Empty(await new ServiceAuteurs(_db).ListerAsync());
}
[Fact]
public async Task Une_envie_recherchee_dans_le_catalogue_ne_remonte_pas()
{
await _catalogue.CreerAsync(
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
await AjouterAsync("La prophétie des abeilles", "Bernard Werber");
var trouves = await _catalogue.ListerAsync(
new CritereLivres { Recherche = "Werber" }, Mathieu);
Assert.Equal("Les fourmis", Assert.Single(trouves).Titre);
}
// ── Validation et doublons ───────────────────────────────────────────────
[Fact]
public async Task Le_titre_est_obligatoire()
{
var resultat = await AjouterAsync(" ");
Assert.NotNull(resultat.Erreur);
}
[Fact]
public async Task La_meme_oeuvre_ne_s_ajoute_pas_deux_fois()
{
await AjouterAsync("Les thanatonautes");
// Variante d'écriture du même titre : c'est exactement ce que produit un double clic
// sur deux lignes de bibliographie qui n'ont pas été regroupées côté BnF.
var doublon = await AjouterAsync("Les thanatonautes : roman");
Assert.NotNull(doublon.Erreur);
Assert.Single(await _souhaits.ListerAsync(Mathieu));
}
[Fact]
public async Task Deux_auteurs_peuvent_avoir_une_oeuvre_de_meme_titre()
{
var premier = await AjouterAsync("Nouvelles", "Bernard Werber");
var second = await AjouterAsync("Nouvelles", "Émile Zola");
Assert.Null(premier.Erreur);
Assert.Null(second.Erreur);
Assert.Equal(2, (await _souhaits.ListerAsync(Mathieu)).Count);
}
[Fact]
public async Task Un_ISBN_invalide_est_refuse()
{
var resultat = await AjouterAsync("Les fourmis", isbn: "1234567890123");
Assert.NotNull(resultat.Erreur);
}
[Fact]
public async Task L_ISBN_reste_facultatif()
{
var resultat = await AjouterAsync("Les fourmis", isbn: null);
Assert.Null(resultat.Erreur);
Assert.Null(resultat.Souhait!.Isbn);
}
[Fact]
public async Task La_liste_est_ordonnee_par_auteur_puis_titre()
{
await AjouterAsync("Les fourmis", "Bernard Werber");
await AjouterAsync("Sans auteur", null);
await AjouterAsync("Germinal", "Émile Zola");
await AjouterAsync("Demain les chats", "Bernard Werber");
var titres = (await _souhaits.ListerAsync(Mathieu)).Select(s => s.Titre).ToList();
// Werber avant Zola, ses titres classés entre eux, et l'envie sans auteur en dernier.
Assert.Equal(["Demain les chats", "Les fourmis", "Germinal", "Sans auteur"], titres);
}
[Fact]
public async Task Une_envie_supprimee_disparait()
{
var souhait = await AjouterAsync("Les fourmis");
Assert.True(await _souhaits.SupprimerAsync(souhait.Souhait!.Id, Mathieu));
Assert.Empty(await _souhaits.ListerAsync(Mathieu));
}
// ── Bibliographie : confrontation à l'étagère ────────────────────────────
/// <summary>
/// Client BnF simulé : ces tests portent sur le rapprochement, pas sur le réseau.
/// </summary>
private sealed class BnfBibliographieFake(string xml) : IBnfClient
{
public Task<(IReadOnlyList<CandidatLivre>, string?)> RechercherAsync(
string isbn, string? urlCouverture, CancellationToken ct = default) =>
Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));
public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
string auteur, CancellationToken ct = default) =>
Task.FromResult((BnfBibliographieParser.Parser(xml, auteur), (string?)null));
}
private async Task<BibliographieDto> BibliographieWerberAsync(string utilisateur = Mathieu)
{
var auteurs = new ServiceAuteurs(_db);
var service = new ServiceBibliographie(
_db, new BnfBibliographieFake(Fixture.Lire("bnf-auteur-werber.xml")), auteurs);
var werber = (await auteurs.ListerAsync()).Single(a => a.Nom.Contains("Werber"));
return (await service.ObtenirAsync(werber.Id, utilisateur))!;
}
[Fact]
public async Task La_bibliographie_grise_les_livres_deja_possedes()
{
// Titre saisi sans le sous-titre « : roman » que porte la notice BnF : c'est le cas
// courant, et c'est précisément ce que la clé d'œuvre doit rattraper.
await _catalogue.CreerAsync(
new EnregistrementLivre { Titre = "Les thanatonautes", Auteur = "Bernard Werber" }, Mathieu);
var biblio = await BibliographieWerberAsync();
var oeuvre = biblio.Oeuvres.Single(
o => o.Titre.StartsWith("Les thanatonautes", StringComparison.OrdinalIgnoreCase));
Assert.True(oeuvre.Possede);
Assert.NotNull(oeuvre.LivreId);
// Les autres restent à découvrir : posséder un livre n'en grise pas d'autres.
Assert.Contains(biblio.Oeuvres, o => o.ADecouvrir);
}
[Fact]
public async Task La_bibliographie_signale_ce_qui_est_deja_souhaite()
{
await _catalogue.CreerAsync(
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
var avant = await BibliographieWerberAsync();
var cible = avant.Oeuvres.First(o => o.ADecouvrir);
await AjouterAsync(cible.Titre, "Bernard Werber");
var apres = await BibliographieWerberAsync();
var revue = apres.Oeuvres.Single(o => o.Titre == cible.Titre);
Assert.True(revue.Souhaite);
Assert.False(revue.ADecouvrir);
}
/// <summary>
/// « Possédé » vaut pour le foyer, « souhaité » seulement pour l'appelant : c'est ce qui
/// permet d'offrir un livre à quelqu'un sans qu'il le voie venir.
/// </summary>
[Fact]
public async Task Le_souhait_d_un_autre_n_apparait_pas_dans_ma_bibliographie()
{
await _catalogue.CreerAsync(
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
var biblio = await BibliographieWerberAsync(Mathieu);
var cible = biblio.Oeuvres.First(o => o.ADecouvrir);
await AjouterAsync(cible.Titre, "Bernard Werber", Camille);
var vueDeMathieu = await BibliographieWerberAsync(Mathieu);
var vueDeCamille = await BibliographieWerberAsync(Camille);
Assert.False(vueDeMathieu.Oeuvres.Single(o => o.Titre == cible.Titre).Souhaite);
Assert.True(vueDeCamille.Oeuvres.Single(o => o.Titre == cible.Titre).Souhaite);
}
[Fact]
public async Task Un_auteur_inconnu_n_a_pas_de_bibliographie()
{
var service = new ServiceBibliographie(
_db, new BnfBibliographieFake("<x/>"), new ServiceAuteurs(_db));
Assert.Null(await service.ObtenirAsync(4242, Mathieu));
}
/// <summary>
/// Le livre d'un homonyme ne doit pas griser l'œuvre d'un autre : la comparaison se fait
/// à auteur donné, pas sur tout le catalogue.
/// </summary>
[Fact]
public async Task Un_livre_d_un_autre_auteur_ne_grise_rien()
{
await _catalogue.CreerAsync(
new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu);
// Un homonyme parfait, mais signé de quelqu'un d'autre.
await _catalogue.CreerAsync(
new EnregistrementLivre { Titre = "Les thanatonautes", Auteur = "Éric Corbeyran" }, Mathieu);
var biblio = await BibliographieWerberAsync();
Assert.False(
biblio.Oeuvres
.Single(o => o.Titre.StartsWith("Les thanatonautes", StringComparison.OrdinalIgnoreCase))
.Possede);
}
public void Dispose()
{
_db.Dispose();
_connexion.Dispose();
GC.SuppressFinalize(this);
}
}