--- name: openlibrary-books-scraper description: Turn search terms, ISBNs or subject headings into structured book records via the Apify Actor arman-bd/openlibrary-books-scraper. Returns one record per Open Library work with title, subtitle, authors and author keys, every ISBN-10 and ISBN-13 across all printings, publishers, first publication year, median page count, subject headings, language codes, cover image URL, edition count, community rating and optionally a per-printing edition list. Use for catalogue enrichment from an ISBN column, reading-list and library apps, publishing market research, or collapsing many printings onto one canonical title. Not for full text, prices, availability, library holdings or purchase links. --- # Open Library Books Scraper Apify Actor `arman-bd/openlibrary-books-scraper`. Give it free-text searches, ISBNs, subject headings, or any mix, and get one dataset record per *work*. A work is the abstract book, so every record already merges what Open Library knows across all of its printings. It runs without credentials. ## When to use it - Enrich a spreadsheet column of ISBNs into full records with covers, subjects and authors. - Back a reading-list, catalogue or library app with title and author search. - Publishing market research: count editions, publishers and languages by subject or year. - Collapse different printings of the same title onto one canonical row before joining two datasets. - Supply a permissively licensed book-metadata layer to a retrieval pipeline. ## When not to use it - Full text, excerpts or contents. Only bibliographic metadata is returned. - Prices, stock, retailer links or library holdings. None of that is in the record. - Author biographies or bibliographies. You get author names and keys, nothing further. - Cover image bytes. Covers carry their own terms, so a URL is returned and never the image itself. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/openlibrary-books-scraper').call({ searchQueries: ['dune'], isbns: ['9780441013593'], subjects: ['science fiction'], includeEditions: true, maxEditionsPerBook: 25, maxResults: 20, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~openlibrary-books-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"isbns":["9780441013593","0-441-01359-0"],"includeEditions":false,"maxResults":5}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/openlibrary-books-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `searchQueries` | string[] | one of the three | `[]` | Free text over title, author and full text. Open Library's own field syntax passes through unchanged, so `author:le guin`, `title:dune` and `publisher:penguin` all work. | | `isbns` | string[] | one of the three | `[]` | ISBN-10 or ISBN-13. Hyphens, spaces and any other punctuation are stripped, and the value is upper-cased so a trailing `x` check digit is handled. Each one resolves to the **parent work**, not to that single printing. | | `subjects` | string[] | one of the three | `[]` | Subject headings such as `science fiction`. Multi-word values are quoted for you before the query is built. | | `includeEditions` | boolean | no | `false` | Attach a per-printing list: publisher, publish date, ISBNs, page count, physical format and languages. Costs one extra request per book saved. | | `maxEditionsPerBook` | integer | no | `10` | Cap on attached editions, 1 to 100. Read only when `includeEditions` is on. Popular titles have hundreds of printings. | | `maxResults` | integer | no | `20` | Cap **per target**, 1 to 1000, not per run. Results arrive 100 rows at a time and are paged automatically until the cap is met. | **At least one of `searchQueries`, `isbns` or `subjects` must be non-empty**, otherwise the run throws before it starts. The cost model is simple: every target is at least one request, plus one more per 100 results, plus one per saved book when `includeEditions` is on. That last term dominates, so editions on with `maxResults: 200` across five targets is a thousand extra requests. Leave editions off unless you genuinely need to separate printings, because the union fields on the work record already carry every ISBN, publisher and language across them. ## Output One record per distinct work. De-duplication is by Open Library work key across every target in the run, so a book matched by both a search and an ISBN is saved once and attributed to whichever target reached it first. | Field | Type | Notes | |---|---|---| | `query` | string | The input value that produced this record: the search string, the cleaned ISBN or the subject. | | `queryType` | string | `search`, `isbn` or `subject`. | | `olKey` | string \| null | Open Library work key, of the form `/works/OL893516W`. The primary key. | | `url` | string \| null | Human-readable page for the work. | | `title` | string \| null | Work title. | | `subtitle` | string \| null | Often null. | | `authors` | string[] | Author names. | | `authorKeys` | string[] | Open Library author keys, positionally aligned with `authors`. | | `isbn10` | string[] | Every 10-character ISBN across every printing of the work. Can run to hundreds of entries. | | `isbn13` | string[] | Same, 13-character. Split purely by cleaned length. | | `publishers` | string[] | Union across all printings, so a single record often lists a dozen. | | `firstPublishYear` | number \| null | Year of the earliest known printing. | | `numberOfPages` | number \| null | **Median** page count across printings, not the count of any one edition. | | `subjects` | string[] | Subject headings, **capped at the first 50**. Popular works carry hundreds of near-duplicates. | | `languages` | string[] | Three-letter MARC language codes across all printings. | | `coverUrl` | string \| null | Large cover image URL, null when no cover is on file. | | `editionCount` | number \| null | How many printings are catalogued. Compare against `editions.length` to see whether the list was truncated. | | `ratingsAverage` | number \| null | Community rating rounded to two decimals. **Null rather than 0 when nobody has rated it.** | | `ratingsCount` | number \| null | Number of ratings. | | `editions` | object[] \| null | Null when `includeEditions` is off. Entry keys: `editionKey`, `title`, `publishDate`, `publishers`, `isbn10`, `isbn13`, `numberOfPages`, `physicalFormat`, `languages`. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record, ISBN and subject lists trimmed: ```json { "query": "dune", "queryType": "search", "olKey": "/works/OL893516W", "url": "https://openlibrary.org/works/OL893516W", "title": "Children of Dune", "subtitle": null, "authors": ["Frank Herbert"], "authorKeys": ["OL79034A"], "isbn10": ["0425071790", "0441015905", "0575074906", "…"], "isbn13": ["9782266027229", "9780441104024", "9780425079027", "…"], "publishers": ["Orion Publishing Group, Limited", "Pocket", "Berkley Publishing"], "firstPublishYear": 1976, "numberOfPages": 504, "subjects": ["Dune (Imaginary place)", "Fiction", "Fiction in English", "Science Fiction"], "languages": ["pol", "chi", "ger", "eng", "spa", "dut", "fre", "por"], "coverUrl": "https://covers.openlibrary.org/b/id/6976407-L.jpg", "editionCount": 77, "ratingsAverage": 3.96, "ratingsCount": 47, "editions": [ { "editionKey": "/books/OL47362500M", "title": "Dzieci Diuny", "publishDate": "Apr 14, 2020", "publishers": ["Rebis"], "isbn10": ["8381880475"], "isbn13": ["9788381880473"], "numberOfPages": 528, "physicalFormat": "hardcover", "languages": ["pol"] } ], "scrapedAt": "2026-08-06T12:00:00.000Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "targetsRequested": 4, "targetsFailed": 1, "failures": [ { "target": "9999999999999", "type": "isbn", "error": "unexpected response shape" } ], "booksSaved": 53, "duplicatesSkipped": 7, "filters": { "searchQueries": ["dune"], "isbns": ["9780441013593"], "subjects": ["science fiction"], "includeEditions": true, "maxEditionsPerBook": 25, "maxResults": 20 }, "finishedAt": "2026-08-06T12:00:11.884Z" } ``` A target is one search string, one ISBN or one subject, and `targetsRequested` counts them all together. `duplicatesSkipped` is the count of results that were dropped because another target had already produced that work, which is why `booksSaved` is usually below `targetsRequested × maxResults`. An empty `failures` with a low `booksSaved` means the queries matched little, not that anything broke: a query with no matches is a normal answer here, never a failure. ## Behaviour to plan around - **Every record is a work, never an edition.** `isbn10`, `isbn13`, `publishers` and `languages` are unions across all printings, which is why one row can carry hundreds of ISBNs. If you need to know which ISBN belongs to which printing, turn on `includeEditions`. - **An ISBN resolves to its parent work.** Passing one printing's ISBN gives you the whole title, including every other printing's identifiers. That is usually what you want for de-duplication and occasionally a surprise. - **`maxResults` is per target.** Five targets at 100 is up to 500 books, minus whatever `duplicatesSkipped` absorbs. - **`numberOfPages` is a median, not a page count.** Do not present it as the length of a specific edition. - **`subjects` is truncated at 50** without any flag saying so. Treat it as a sample of the headings, not the complete set. - **`ratingsAverage` is null when `ratingsCount` is zero**, deliberately, rather than reporting a false `0.0`. Filter on `ratingsCount` before averaging anything. - **A failed editions fetch gives `editions: []`, not a run failure.** An empty array with `includeEditions` on and a non-zero `editionCount` means that one lookup failed, and the book record itself is still complete. - **A query matching nothing is not an error.** It answers normally with zero rows and never appears in `failures`. - **One bad target never aborts the run.** It lands in `RUN_SUMMARY.failures` and the rest continue. The Actor only throws when every target failed. - **Transient failures are retried** three times with linear backoff; not-found is final and is not retried. Requests are spaced about 250 ms apart, so a run with editions on is noticeably slower than one without. ## Recipes **Enrich an ISBN column.** No editions, small cap, one row per distinct work. ```json { "isbns": ["9780441013593", "0-441-01359-0", "9780575074903"], "includeEditions": false, "maxResults": 1 } ``` Join back on `query`, which holds the cleaned ISBN, and expect fewer rows than inputs when two ISBNs share a work. **Separate the printings of a title.** Editions on, cap raised. ```json { "searchQueries": ["title:dune"], "includeEditions": true, "maxEditionsPerBook": 100, "maxResults": 5 } ``` Compare `editions.length` against `editionCount` to see whether 100 was enough. **Subject sweep for market research.** Several subjects, deep cap, no editions. ```json { "subjects": ["science fiction", "cryptography"], "searchQueries": ["author:le guin"], "maxResults": 200 } ``` Group by `firstPublishYear` and `publishers` for a publishing-activity picture, and read `duplicatesSkipped` to gauge how much the targets overlapped. **Search-backed lookup for an app.** One query, a page of results, nothing extra. ```json { "searchQueries": ["the left hand of darkness"], "maxResults": 10 } ``` Rank on `ratingsCount` then `editionCount` to put the canonical edition of a title first.