← back to Paul Conrad Archive

src/conrad/crawlers/internet_archive.py

102 lines

"""Internet Archive — bibliography of Conrad's books (metadata API + advancedsearch; robots permit both).
Books on archive.org are Controlled-Digital-Lending scans: we record metadata only and never borrow/open them.
Name-authority conflations (other 'Paul Conrad's tagged 1924-2010) are detected and excluded."""
from __future__ import annotations

import re

from .base import Blocked, Crawler, Transient

BIBLIOGRAPHY = [  # (title, expected year) — the books named in the spec
    ("When in the Course of Human Events", 1973),
    ("The King and Us", 1974),
    ("Pro and Conrad", 1979),
    ("Drawn and Quartered", 1985),
    ("CONartist: 30 Years with the Los Angeles Times", 1993),
    ("Drawing the Line", 1999),
    ("I, Con", None),
]
ADV = "https://archive.org/advancedsearch.php"
META = "https://archive.org/metadata/"
NOT_OUR_CONRAD = re.compile(r"larryboy|veggie|kinderklapper|gott ist|thriving church|slave bug", re.I)  # slave bug: 1975 novel, no dates on creator


def _norm(t: str) -> str:
    return re.sub(r"[^a-z0-9 ]", "", (t or "").lower())


class InternetArchive(Crawler):
    source_id = "internet_archive"
    name = "Internet Archive (books metadata)"
    repository = "Internet Archive"
    url = "https://archive.org/details/paulconraddrawin0000conr"
    classification = "PUBLIC_API"
    access_notes = "Book scans are lending-library (CDL) items; metadata only, not borrowed."

    def search(self, q: str) -> list[dict]:
        d = self.http.get_json(ADV, params={"q": q, "fl[]": ["identifier", "title", "date", "creator", "mediatype"],
                                            "rows": 100, "output": "json"})
        self.stats["pages"] += 1
        return d.get("response", {}).get("docs", [])

    def crawl(self) -> None:
        cands: dict[str, dict] = {}
        try:
            for doc in self.search('creator:("Conrad, Paul") AND mediatype:texts'):
                cands[doc["identifier"]] = doc
            for title, _ in BIBLIOGRAPHY:
                short = title.split(":")[0]
                for doc in self.search(f'title:("{short}") AND creator:(conrad)'):
                    cands[doc["identifier"]] = doc
        except (Blocked, Transient) as e:
            self.error(ADV, e)
        excluded = []
        for ident, doc in sorted(cands.items()):
            creators = doc.get("creator") or []
            creators = creators if isinstance(creators, list) else [creators]
            ctext = " ".join(creators)
            if NOT_OUR_CONRAD.search(doc.get("title", "")) or not re.search(r"conrad,\s*paul", ctext, re.I) \
                    or re.search(r"1865|funke", ctext, re.I):
                excluded.append(f"{ident} ({doc.get('title', '')[:50]}; {ctext[:60]})")
                continue
            try:
                m = self.http.get_json(META + ident)
            except (Blocked, Transient) as e:
                self.error(META + ident, e)
                continue
            self.stats["pages"] += 1
            md = m.get("metadata", {})
            title = md.get("title") or doc.get("title")
            year = int(str(md.get("date") or doc.get("date") or "0")[:4] or 0) or None
            colls = md.get("collection") or []
            colls = colls if isinstance(colls, list) else [colls]
            access = "controlled_digital_lending" if {"inlibrary", "printdisabled"} & set(colls) else "public"
            bib = next((b for b in BIBLIOGRAPHY if _norm(b[0].split(":")[0])[:14] in _norm(title)), None)
            name = bib[0] if bib else title.strip()
            isbn = md.get("isbn")
            isbn = isbn[0] if isinstance(isbn, list) else isbn
            oclc = md.get("oclc-id")
            oclc = oclc[0] if isinstance(oclc, list) else oclc
            desc = md.get("description")
            desc = " | ".join(desc) if isinstance(desc, list) else desc
            self.conn.execute(
                """INSERT INTO books(title,year,publisher,isbn,oclc,ia_identifier,ia_access,record_url,notes,verified)
                   VALUES (?,?,?,?,?,?,?,?,?,1)
                   ON CONFLICT(title) DO UPDATE SET year=COALESCE(excluded.year,year), publisher=excluded.publisher,
                     isbn=COALESCE(excluded.isbn,isbn), oclc=COALESCE(excluded.oclc,oclc),
                     ia_identifier=excluded.ia_identifier, ia_access=excluded.ia_access,
                     record_url=excluded.record_url, notes=excluded.notes, verified=1""",
                (name, year, md.get("publisher"), isbn, oclc, ident, access, f"https://archive.org/details/{ident}",
                 (f"IA title: {title}. " + (desc or ""))[:800]))
            self.stats["seen"] += 1
            self.stats["added"] += 1
        # bibliography titles not found on IA stay listed, unverified
        for title, year in BIBLIOGRAPHY:
            self.conn.execute("INSERT OR IGNORE INTO books(title,year,notes,verified) VALUES (?,?,?,0)",
                              (title, year, "listed in project brief; not found on Internet Archive"))
        self.conn.commit()
        self.notes.append(f"excluded name-authority conflations: {excluded}")


CRAWLER = InternetArchive