From 6b4f9610530d46cacacd8e2e5a56e91fb6b02d1a Mon Sep 17 00:00:00 2001 From: eclipse Date: Tue, 19 Aug 2025 22:02:21 +0200 Subject: [PATCH] modified a number of data models, view functions and Jinja templates to reflect changes in database schema --- the_works/models.py | 93 +++++++++---------- the_works/templates/views/ausgabe_detail.html | 14 +-- .../templates/views/veroeffentlichung.html | 13 +-- the_works/templates/views/werk.html | 49 +--------- the_works/views/veroeffentlichung.py | 15 +-- the_works/views/werk.py | 57 ++---------- tmp.md | 36 +++++++ 7 files changed, 103 insertions(+), 174 deletions(-) diff --git a/the_works/models.py b/the_works/models.py index 206895f..80cddbf 100644 --- a/the_works/models.py +++ b/the_works/models.py @@ -1,11 +1,12 @@ import sys -from typing import List, Optional -from sqlalchemy import ForeignKey, types, CheckConstraint -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship +from typing import List, Optional, NewType +from decimal import Decimal +from sqlalchemy import ForeignKey, CheckConstraint, types, String, Numeric +from sqlalchemy.orm import DeclarativeBase, registry, Mapped, mapped_column, relationship from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy from flask import url_for - +# these data models can be used with the generic functions from simple_view SIMPLE_MODELS = [ {"name": "genre", "title": "Genres"}, {"name": "herausgeber", "title": "Herausgeber", "column": "Name"}, @@ -16,8 +17,24 @@ SIMPLE_MODELS = [ {"name": "werksform", "title": "Werksformen"} ] +# define some new data types (strings of specific lengths, numbers of different precision and scale) +str13 = NewType("str13", str) +str10 = NewType("str10", str) +str8 = NewType("str8", str) +price = NewType("price", Decimal) + + """Base class for the_works' data models""" class Base(DeclarativeBase): + # register new data types + registry = registry(type_annotation_map={ + str13: String(13), + str10: String(10), + str8: String(8), + price: Numeric(7, 2) + }) + + """Return the data record as a dict""" def asdict(self) -> dict: d = {} for col in self.__table__.c: @@ -26,10 +43,13 @@ class Base(DeclarativeBase): else: if isinstance(value := self.__getattribute__(col.key), str) and len(value) > 50: d[col.key] = value[:48] + '...' + elif value is None: + d[col.key] = "" else: d[col.key] = value return d + """Display the data record in readable format""" def __repr__(self) -> str: return f"{type(self).__name__}({str(self.asdict())})" @@ -61,7 +81,6 @@ class Pseudonym(Base): ID: Mapped[int] = mapped_column(primary_key=True) Pseudonym: Mapped[str] = mapped_column(CheckConstraint('Pseudonym <> ""', name='PseudonymNotEmptyConstraint'), nullable=False, unique=True) - veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='pseudonym') #TODO DELETE werk: Mapped[List['Werk']] = relationship(back_populates='pseudonym') @@ -91,7 +110,6 @@ class Verlag(Base): ausgabe: Mapped[List['Ausgabe']] = relationship(back_populates='verlag') reihe: Mapped[List['Reihe']] = relationship(back_populates='verlag') - werk: Mapped[List['Werk']] = relationship(back_populates='verlag') #TODO DELETE class Werksform(Base): @@ -101,7 +119,6 @@ class Werksform(Base): Werksform: Mapped[str] = mapped_column(CheckConstraint('Werksform <> ""', name='WerksformNotEmptyConstraint'), nullable=False, unique=True) ausgabe: Mapped[List['Ausgabe']] = relationship(back_populates='werksform') - werk: Mapped[List['Werk']] = relationship(back_populates='werksform') #TODO DELETE # Classes that have more than one column and need their own view functions @@ -134,7 +151,6 @@ class Titelbild(Base): # one-to-many ausgabe: Mapped[List['Ausgabe']] = relationship(back_populates='titelbild') - werk: Mapped[List['Werk']] = relationship(back_populates='titelbild') #DELETE def asdict_with_urls(self): tb = self.asdict() @@ -152,12 +168,12 @@ class Text(Base): Untertitel: Mapped[Optional[str]] # many-to-one - Reihe: Mapped[Optional[int]] = mapped_column(ForeignKey('Reihe.ID')) - reihe: Mapped[Optional["Reihe"]] = relationship(back_populates="text") - Sprache: Mapped[int] = mapped_column(ForeignKey('Sprache.ID'), nullable=False) - sprache: Mapped['Sprache'] = relationship(back_populates='text') Textform: Mapped[int] = mapped_column(ForeignKey('Textform.ID'), nullable=False) textform: Mapped['Textform'] = relationship(back_populates='text') + Sprache: Mapped[int] = mapped_column(ForeignKey('Sprache.ID'), nullable=False) + sprache: Mapped['Sprache'] = relationship(back_populates='text') + Reihe: Mapped[Optional[int]] = mapped_column(ForeignKey('Reihe.ID')) + reihe: Mapped[Optional["Reihe"]] = relationship(back_populates="text") # one-to-many veroeffentlichung: Mapped[List['Veroeffentlichung']] = relationship(back_populates='text') @@ -172,16 +188,12 @@ class Veroeffentlichung(Base): # regular columns ID: Mapped[int] = mapped_column(primary_key=True) - AltTitel: Mapped[Optional[str]] - AltUntertitel: Mapped[Optional[str]] - - # many-to-one - Pseudonym: Mapped[int] = mapped_column(ForeignKey('Pseudonym.ID'), nullable=False) #TODO DELETE - pseudonym: Mapped['Pseudonym'] = relationship(back_populates='veroeffentlichung') #TODO DELETE Text: Mapped[int] = mapped_column(ForeignKey('Text.ID'), nullable=False) text: Mapped['Text'] = relationship(back_populates='veroeffentlichung') Werk: Mapped[int] = mapped_column(ForeignKey('Werk.ID'), nullable=False) werk: Mapped['Werk'] = relationship(back_populates='veroeffentlichung') + AltTitel: Mapped[Optional[str]] + AltUntertitel: Mapped[Optional[str]] class Werk(Base): @@ -191,26 +203,11 @@ class Werk(Base): ID: Mapped[int] = mapped_column(primary_key=True) Titel: Mapped[str] = mapped_column(CheckConstraint('Titel <> ""', name='WerkstitelNotEmptyConstraint'), nullable=False) Untertitel: Mapped[Optional[str]] - Reihennummer: Mapped[Optional[str]] - Erscheinungsdatum: Mapped[Optional[str]] #TODO DELETE - ISBN_13: Mapped[Optional[str]] #TODO DELETE - ISBN_10: Mapped[Optional[str]] #TODO DELETE - ISSN: Mapped[Optional[str]] #TODO DELETE - Preis: Mapped[Optional[str]] #TODO DELETE - Klappentext: Mapped[Optional[str]] #TODO DELETE - Anmerkungen: Mapped[Optional[str]] #TODO DELETE - - # many-to-one - Pseudonym: Mapped[int] = mapped_column(ForeignKey('Pseudonym.ID')) - pseudonym: Mapped['Pseudonym'] = relationship(back_populates='werk') Reihe: Mapped[Optional[int]] = mapped_column(ForeignKey('Reihe.ID')) reihe: Mapped[Optional['Reihe']] = relationship(back_populates='werk') - Titelbild: Mapped[Optional[int]] = mapped_column(ForeignKey('Titelbild.ID')) #TODO DELETE - titelbild: Mapped[Optional['Titelbild']] = relationship(back_populates='werk') #TODO DELETE - Verlag: Mapped[Optional[int]] = mapped_column(ForeignKey('Verlag.ID')) #TODO DELETE - verlag: Mapped[Optional['Verlag']] = relationship(back_populates='werk') #TODO DELETE - Werksform: Mapped[int] = mapped_column(ForeignKey('Werksform.ID'), nullable=False) #TODO DELETE - werksform: Mapped['Werksform'] = relationship(back_populates='werk') #TODO DELETE + Reihennummer: Mapped[Optional[str]] + Pseudonym: Mapped[int] = mapped_column(ForeignKey('Pseudonym.ID')) + pseudonym: Mapped['Pseudonym'] = relationship(back_populates='werk') # one-to-many ausgabe: Mapped[List['Ausgabe']] = relationship(back_populates='werk') @@ -228,23 +225,21 @@ class Ausgabe(Base): # regular columns ID: Mapped[int] = mapped_column(primary_key=True) - Erscheinungsdatum: Mapped[Optional[str]] - ISBN_13: Mapped[Optional[str]] - ISBN_10: Mapped[Optional[str]] - ISSN: Mapped[Optional[str]] - Preis: Mapped[Optional[str]] - Klappentext: Mapped[Optional[str]] - Anmerkungen: Mapped[Optional[str]] - - # many-to-one - Titelbild: Mapped[Optional[int]] = mapped_column(ForeignKey('Titelbild.ID')) - titelbild: Mapped[Optional['Titelbild']] = relationship(back_populates='ausgabe') - Verlag: Mapped[Optional[int]] = mapped_column(ForeignKey('Verlag.ID')) - verlag: Mapped[Optional['Verlag']] = relationship(back_populates='ausgabe') Werk: Mapped[int] = mapped_column(ForeignKey('Werk.ID'), nullable=False) werk: Mapped["Werk"] = relationship(back_populates='ausgabe') Werksform: Mapped[int] = mapped_column(ForeignKey('Werksform.ID'), nullable=False) werksform: Mapped['Werksform'] = relationship(back_populates='ausgabe') + Titelbild: Mapped[Optional[int]] = mapped_column(ForeignKey('Titelbild.ID')) + titelbild: Mapped[Optional['Titelbild']] = relationship(back_populates='ausgabe') + ISBN_13: Mapped[Optional[str13]] + ISBN_10: Mapped[Optional[str10]] + ISSN: Mapped[Optional[str8]] + Verlag: Mapped[Optional[int]] = mapped_column(ForeignKey('Verlag.ID')) + verlag: Mapped[Optional['Verlag']] = relationship(back_populates='ausgabe') + Erscheinungsdatum: Mapped[Optional[str]] + Preis: Mapped[Optional[price]] + Klappentext: Mapped[Optional[str]] + Anmerkungen: Mapped[Optional[str]] # Additional tables for many-to-many-relationships within the DB diff --git a/the_works/templates/views/ausgabe_detail.html b/the_works/templates/views/ausgabe_detail.html index 47ab9a7..8cda0d5 100644 --- a/the_works/templates/views/ausgabe_detail.html +++ b/the_works/templates/views/ausgabe_detail.html @@ -53,7 +53,7 @@ Ausgabe bearbeiten - @@ -97,7 +88,7 @@ window.onload = function () { initDataTable("veroeffentlichung-table"); initCreateButton("veroeffentlichung-table", "Veröffentlichung hinzufügen …"); - initModal("veroeffentlichung-modal", ["form_Text", "form_Werk", "form_AltTitel", "form_AltUntertitel", "form_Pseudonym"], ["Neue Veröffentlichung", "Veröffentlichung bearbeiten"], ["{{ url_for('veroeffentlichung.create') }}", "{{ url_for('veroeffentlichung.update', id=-1) }}"]); + initModal("veroeffentlichung-modal", ["form_Text", "form_Werk", "form_AltTitel", "form_AltUntertitel"], ["Neue Veröffentlichung", "Veröffentlichung bearbeiten"], ["{{ url_for('veroeffentlichung.create') }}", "{{ url_for('veroeffentlichung.update', id=-1) }}"]); } {% endblock script %} \ No newline at end of file diff --git a/the_works/templates/views/werk.html b/the_works/templates/views/werk.html index 19687f6..3ff4820 100644 --- a/the_works/templates/views/werk.html +++ b/the_works/templates/views/werk.html @@ -18,20 +18,11 @@ Titel Untertitel - Werksform Reihe Reihennummer - Verlag - Preis - Erscheinungsdatum - ISBN_13 - ISBN_10 - ISSN Genre(s) Herausgeber:in(nen) - Titelbild - Klappentext - Anmerkungen + Pseudonym Aktionen @@ -40,33 +31,11 @@ {{ werk["Titel"] }} {{ werk["Untertitel"] }} - {{ werk["Werksform"] }} {{ werk["Reihe"] }} {{ werk["Reihennummer"] }} - {{ werk["Verlag"] }} - {{ werk["Preis"] }} - {{ werk["Erscheinungsdatum"] }} - {{ werk["ISBN_13"] }} - {{ werk["ISBN_10"] }} - {{ werk["ISSN"] }} {{ werk["Genre_list"] | join(", ") }} {{ werk["Herausgeber_list"] | join(", ") }} - - {% if werk["Titelbild"] %} -
-
- Titelbild (Thumbnail) - -
-
- {% else %} - ✘ - {% endif %} - - ✔{% else %}>✘{% endif %} - {{ werk["Anmerkungen"] }} + {{ werk["Pseudonym"] }} @@ -75,29 +44,15 @@ - -
-
- -
-
-
- -
-
-
- {% endblock content %} {% block script %} - {% endblock script %} diff --git a/the_works/views/veroeffentlichung.py b/the_works/views/veroeffentlichung.py index e220d94..e1532eb 100644 --- a/the_works/views/veroeffentlichung.py +++ b/the_works/views/veroeffentlichung.py @@ -1,14 +1,14 @@ from flask import Blueprint, render_template, request, redirect, flash, url_for from sqlalchemy import select from the_works.database import db -from the_works.models import Veroeffentlichung, Text, Werk, Werksform, Pseudonym +from the_works.models import Veroeffentlichung, Text, Werk bp = Blueprint("veroeffentlichung", __name__) @bp.route("/veroeffentlichung/") @bp.route("/veroeffentlichung/all/") def all(): - rows = db.session.execute(select(Veroeffentlichung, Text, Werk, Werksform, Pseudonym).join(Veroeffentlichung.text, isouter=True).join(Veroeffentlichung.werk, isouter=True).join(Veroeffentlichung.pseudonym, isouter=True).join(Werk.werksform)) + rows = db.session.execute(select(Veroeffentlichung, Text, Werk).join(Veroeffentlichung.text, isouter=True).join(Veroeffentlichung.werk, isouter=True)) veroeffentlichungen = [] for row in rows: veroeffentlichungen.append({ @@ -17,13 +17,10 @@ def all(): "t_id": row.Veroeffentlichung.Text, "Werk": row.Werk.Titel, "w_id": row.Veroeffentlichung.Werk, - "wf_id": row.Werksform.ID, "AltTitel": row.Veroeffentlichung.AltTitel or "", - "AltUntertitel": row.Veroeffentlichung.AltUntertitel or "", - "Pseudonym": row.Pseudonym.Pseudonym, - "p_id": row.Veroeffentlichung.Pseudonym, + "AltUntertitel": row.Veroeffentlichung.AltUntertitel or "" }) - return render_template("views/veroeffentlichung.html", veroeffentlichungen=veroeffentlichungen, texte=db.session.scalars(select(Text)), werke=db.session.scalars(select(Werk)), pseudonyme=db.session.scalars(select(Pseudonym))) + return render_template("views/veroeffentlichung.html", veroeffentlichungen=veroeffentlichungen, texte=db.session.scalars(select(Text))) @bp.route("/veroeffentlichung/create/", methods=["POST"]) def create(): @@ -31,8 +28,7 @@ def create(): Text = request.form["form_Text"], Werk = request.form["form_Werk"], AltTitel = request.form["form_AltTitel"], - AltUntertitel = request.form["form_AltUntertitel"], - Pseudonym = request.form["form_Pseudonym"], + AltUntertitel = request.form["form_AltUntertitel"] )) db.session.commit() flash("Eintrag erfolgreich hinzugefügt") @@ -45,7 +41,6 @@ def update(id): veroeffentlichung.Werk = request.form["form_Werk"] veroeffentlichung.AltTitel = request.form["form_AltTitel"] veroeffentlichung.AltUntertitel = request.form["form_AltUntertitel"] - veroeffentlichung.Pseudonym = request.form["form_Pseudonym"] db.session.commit() flash("Eintrag erfolgreich geändert") return redirect(url_for("veroeffentlichung.all"), code=303) diff --git a/the_works/views/werk.py b/the_works/views/werk.py index 220e87a..dafa626 100644 --- a/the_works/views/werk.py +++ b/the_works/views/werk.py @@ -1,7 +1,7 @@ from flask import Blueprint, render_template, request, redirect, flash, url_for from sqlalchemy import select from the_works.database import db -from the_works.models import Werk, Reihe, Verlag, Werksform, Genre, Herausgeber, Titelbild +from the_works.models import Werk, Reihe, Pseudonym, Genre, Herausgeber bp = Blueprint("werk", __name__) @@ -10,7 +10,7 @@ bp = Blueprint("werk", __name__) @bp.route("/werk/all/") def all(): # select all rows from table "Werk", ORM style - rows = db.session.execute(select(Werk, Reihe, Verlag, Werksform).join(Werk.reihe, isouter=True).join(Werk.verlag, isouter=True).join(Werk.werksform, isouter=True)) + rows = db.session.execute(select(Werk, Reihe, Pseudonym).join(Werk.reihe, isouter=True).join(Werk.pseudonym, isouter=True)) # condense result into list of dicts werke = [] for row in rows: @@ -18,33 +18,20 @@ def all(): "id": row.Werk.ID, "Titel": row.Werk.Titel, "Untertitel": row.Werk.Untertitel or "", - "Werksform": row.Werksform.Werksform if row.Werksform else "", - "Verlag": row.Verlag.Verlag if row.Verlag else "", "Reihe": row.Reihe.Titel if row.Reihe else "", "Reihennummer": row.Werk.Reihennummer or "", - "Erscheinungsdatum": row.Werk.Erscheinungsdatum or "", - "ISBN_13": row.Werk.ISBN_13 or "", - "ISBN_10": row.Werk.ISBN_10 or "", - "ISSN": row.Werk.ISSN or "", - "Preis": row.Werk.Preis or "", -# "Titelbild": url_for("titelbild.thumbnail", id=row.Werk.Titelbild) if row.Werk.Titelbild else "", - "Titelbild": db.session.get(Titelbild, row.Werk.Titelbild).asdict_with_urls() if row.Werk.Titelbild else "", - "Klappentext": row.Werk.Klappentext or "", - "Anmerkungen": row.Werk.Anmerkungen or "", "Herausgeber_list": [wh.herausgeber.Name for wh in row.Werk.herausgeber], "Genre_list": [wg.genre.Genre for wg in row.Werk.genres], + "Pseudonym": row.Pseudonym.Pseudonym if row.Pseudonym else "" }) return render_template("views/werk.html", werke=werke) @bp.route("/werk/read/") def read(id): - # prepare Titelbilder as dict including URLs for thumbnail and full pic - titelbilder = map(lambda t: t.asdict_with_urls(), db.session.scalars(select(Titelbild))) - # id of zero -> return empty data if id == 0: - return render_template("views/werk_detail.html", werk={"ID": 0, "Erscheinungsdatum": ""}, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), titelbilder=titelbilder) + return render_template("views/werk_detail.html", werk={"ID": 0, "Erscheinungsdatum": ""}, reihen=db.session.scalars(select(Reihe)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), pseudonyme=db.session.scalars(select(Pseudonym))) # all other ids -> read existing entry from DB and return as dict w = db.session.get(Werk, id) @@ -54,7 +41,7 @@ def read(id): werk["Genres"] = w.genre_ids werk["Herausgeber"] = w.herausgeber_ids - return render_template("views/werk_detail.html", werk=werk, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), titelbilder=titelbilder) + return render_template("views/werk_detail.html", werk=werk, reihen=db.session.scalars(select(Reihe)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), pseudonyme=db.session.scalars(select(Pseudonym))) @bp.route("/werk/create/", methods=["POST"]) @@ -62,18 +49,9 @@ def create(): werk = Werk( Titel = request.form["form_Titel"], Untertitel = request.form["form_Untertitel"] or None, - Werksform = request.form["form_Werksform"], - Verlag = request.form["form_Verlag"] or None, Reihe = request.form["form_Reihe"] or None, Reihennummer = request.form["form_Reihennummer"] or None, - Erscheinungsdatum = _get_datum(request.form["form_Erscheinungsjahr"], request.form["form_Erscheinungsmonat"], request.form["form_Erscheinungstag"]), - ISBN_13 = request.form["form_ISBN_13"] or None, - ISBN_10 = request.form["form_ISBN_10"] or None, - ISSN = request.form["form_ISSN"] or None, - Preis = request.form["form_Preis"] or None, - Titelbild = request.form["form_Titelbild"] or None, - Klappentext = request.form["form_Klappentext"] or None, - Anmerkungen = request.form["form_Anmerkungen"] or None + Pseudonym = request.form["form_Pseudonym"], ) for g in request.form.getlist("form_Genre"): werk.genre_ids.append(g) @@ -93,18 +71,9 @@ def update(id): # update values werk.Titel = request.form["form_Titel"] werk.Untertitel = request.form["form_Untertitel"] or None - werk.Werksform = request.form["form_Werksform"] - werk.Verlag = request.form["form_Verlag"] or None werk.Reihe = request.form["form_Reihe"] or None werk.Reihennummer = request.form["form_Reihennummer"] or None - werk.Erscheinungsdatum = _get_datum(request.form["form_Erscheinungsjahr"], request.form["form_Erscheinungsmonat"], request.form["form_Erscheinungstag"]) - werk.ISBN_13 = request.form["form_ISBN_13"] or None - werk.ISBN_10 = request.form["form_ISBN_10"] or None - werk.ISSN = request.form["form_ISSN"] or None - werk.Preis = request.form["form_Preis"] or None - werk.Titelbild = request.form["form_Titelbild"] or None - werk.Klappentext = request.form["form_Klappentext"] or None - werk.Anmerkungen = request.form["form_Anmerkungen"] or None + werk.Pseudonym = request.form["form_Pseudonym"] # update associated values: Genre form_set = set(map(int, request.form.getlist("form_Genre"))) @@ -133,15 +102,3 @@ def delete(id): db.session.commit() flash("Eintrag erfolgreich gelöscht") return redirect(url_for("werk.all")) - - -def _get_datum(jahr, monat, tag): - if tag != "": - return "-".join([jahr, monat.zfill(2), tag.zfill(2)]) - elif monat != "": - return "-".join([jahr, monat.zfill(2)]) - elif jahr != "": - return jahr - else: - return None - diff --git a/tmp.md b/tmp.md index 7a83eb1..562006a 100644 --- a/tmp.md +++ b/tmp.md @@ -57,7 +57,43 @@ Sprache, (Genres) Reihe, R.No, (Genres), (Hrsg) IS?Nx3, Preis, Titel +Stand der neuen DB +- Basisdaten sind vorhanden, Ausnahme: Titelbild +- Text ist auf dem alten Stand +- es fehlt + - Veroeffentlichung + - Titelbild + - Werk + - Ausgabe +- die Python views sind alle upgedated (theoretisch) +- zu ändernde Jinja-Templates + - x veroeffentlichung + - x werk + - werk_detail + - ausgabe_detail + - Darstellung von Preis updaten und validaten + +- nice to have: Darstellung und Validation von IS*N verbessern (auf Basis der jew. Spezifikation) + - https://en.wikipedia.org/wiki/ISBN, https://en.wikipedia.org/wiki/ISSN + - isbnlib (Python package) + - isbnlib-dnb +- nice to have: Metadaten zu meinen Werken direkt aus einer externen DB holen + - zB DNB + - keine Cover o. Klappentexte, Daten sind unsauber (zB Preis) + - Demo-Abruf-Seite: https://dnb-sru-demo.streamlit.app/ + - ist kompliziert; vllt reicht es auch, einmal per Hand alle Werke von Tobias Radloff / Paul Jansen abzurufen und in die DB zu übernehmen? + - https://services.dnb.de/sru/dnb?version=1.1&operation=searchRetrieve&query=atr%3DTobias%20and%20atr%3DRadloff + - buchhandel.de + - keine frei verfügbare API, aber [Standardsuche](https://buchhandel.de/suche) + - openlibrary.org: ein Treffer [Amoralisch](https://openlibrary.org/works/OL26179908W/Philip_Strasser_in_Amoralisch?edition=key%3A/books/OL35329529M) + - Google Books: null Treffer + - VLB hat ne [REST-API](https://vlb.de/hilfe/datenbezug/rest-api), aber die kostet + - booklooker: taugt nicht (ist ein Marktplatz und kein Katalog) + - ISBNdb.com -> kennt 9 Bücher von mir + - kostet, gibt aber free 7-day trial + - [Amazon Product Advertising API](https://webservices.amazon.com/paapi5/documentation/) + - kostet nix