use std::collections::HashMap;
use std::process;
use clap::{Args, Parser, Subcommand};
use open_library_api_rs::{
OpenLibraryClient, Result,
models::{
author::{Author, AuthorWorks},
changes::{ChangesParams, RecentChange},
common::{BooksJsCmd, ChangeKind, CoverKey, ImageSize, VolumeIdType},
covers::CoverMeta,
edition::Edition,
list::{List, ListEditions, ListSeed, ListSeeds, ListSubjects, UserLists},
partner::VolumesResponse,
query::{HistoryEntry, QueryResponse},
reading_log::ReadingLog,
search::{
AuthorDoc, AuthorSearchParams, BookDoc, InsideDoc, ListDoc, SearchParams,
SearchResponse, SubjectDoc, SubjectParams,
},
subject::Subject,
work::{Work, WorkBookshelves, WorkEditions, WorkRatings},
},
};
#[derive(Parser)]
#[command(
name = "olib",
version = env!("CARGO_PKG_VERSION"),
about = "Command-line client for the Open Library API",
long_about = "\
olib — Open Library API command-line client
Covers every public read endpoint: works, editions, authors, search, subjects,
covers, user lists, reading logs, the partner/volumes API, recent changes, and
the generic query and history endpoints.
Results are printed to stdout in human-readable form. Use --json to emit raw
pretty-printed JSON (pipe to jq for further processing). Errors go to stderr;
exit code is 0 on success, 1 on failure.
RATE LIMITS
Anonymous (default): 1 request / second
Identified (--email): 3 requests / second
Covers CDN: 100 requests / 5 minutes / IP
IDENTIFYING YOUR APPLICATION
Supply --email and --rate-limit 3 together to use the 3 req/s tier:
olib --email me@example.com --rate-limit 3 work get OL45804W
The email is appended to the User-Agent header so Open Library can contact
you if your application misbehaves. It is never sent in the request body.",
after_help = "\
EXAMPLES
olib work get OL45804W
olib work editions OL45804W --limit 5
olib edition isbn 9780140328724
olib author get OL23919A
olib search books --q \"rust programming\" --limit 5
olib search books --author tolkien --language eng
olib subject science_fiction --details --limit 10
olib cover url id 5428012 large
olib reading already-read alice
olib changes --date 2024-06-15 --kind edit-book
olib volume isbn 0451450523
olib books ISBN:0451450523 OCLC:45883427
# JSON output for scripting
olib --json work get OL45804W | jq '{title, first_publish_date}'
olib --json search books --q \"dune\" | jq '.docs[] | {key, title}'
olib --json changes --kind add-book --limit 5 | jq '.[].key'",
)]
struct Cli {
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true, default_value = "1", value_name = "N")]
rate_limit: u32,
#[arg(long, global = true, value_name = "EMAIL")]
email: Option<String>,
#[arg(long, global = true, hide = true, value_name = "URL")]
base_url: Option<String>,
#[arg(long, global = true, hide = true, value_name = "URL")]
covers_url: Option<String>,
#[command(subcommand)]
command: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Work(WorkArgs),
Edition(EditionArgs),
Author(AuthorArgs),
Search(SearchArgs),
Subject(SubjectCliArgs),
Cover(CoverArgs),
List(ListArgs),
Reading(ReadingArgs),
Changes(ChangesCliArgs),
Volume(VolumeArgs),
Books(BooksCliArgs),
Query(QueryCliArgs),
History(HistoryArgs),
}
#[derive(Args)]
#[command(
about = "Fetch works, editions lists, ratings, and bookshelf counts",
long_about = "\
A 'work' is the canonical creative entity — the book as an idea, separate from
any particular printing. Each work has a unique Work OLID of the form OL<N>W
(e.g. OL45804W for 'Fantastic Mr. Fox').
Sub-commands:
get Core metadata: title, authors, subjects, description, covers
editions Paginated list of all known printings of the work
ratings Community star-rating average and per-star counts
bookshelves How many users have it on each reading shelf",
after_help = "\
EXAMPLES
olib work get OL45804W
olib --json work get OL45804W | jq '{title, first_publish_date, subjects}'
olib work editions OL45804W
olib work editions OL45804W --limit 5 --offset 0
olib work ratings OL45804W
olib work bookshelves OL45804W",
)]
struct WorkArgs {
#[command(subcommand)]
cmd: WorkCmd,
}
#[derive(Subcommand)]
enum WorkCmd {
#[command(
after_help = "\
FIELDS RETURNED
key, title, subtitle, description, authors, subjects, subject_places,
subject_people, subject_times, covers (IDs), first_publish_date, latest_revision
EXAMPLES
olib work get OL45804W
olib --json work get OL45804W | jq '.subjects'
olib --json work get OL45804W | jq '.authors[].author.key'",
)]
Get {
id: String,
},
#[command(
after_help = "\
EXAMPLES
olib work editions OL45804W
olib work editions OL45804W --limit 5
olib work editions OL45804W --limit 20 --offset 20
olib --json work editions OL45804W | jq '.entries[] | {key, title, publish_date}'",
)]
Editions {
id: String,
#[arg(long, default_value = "20", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
},
#[command(
after_help = "\
EXAMPLES
olib work ratings OL45804W
olib --json work ratings OL45804W | jq '.summary'",
)]
Ratings {
id: String,
},
#[command(
after_help = "\
EXAMPLES
olib work bookshelves OL45804W
olib --json work bookshelves OL45804W | jq '.counts'",
)]
Bookshelves {
id: String,
},
}
#[derive(Args)]
#[command(
about = "Fetch editions by OLID or ISBN",
long_about = "\
An 'edition' is a specific physical or digital printing of a work. Edition OLIDs
have the form OL<N>M (e.g. OL7353617M). ISBNs can be 10 or 13 digits;
hyphens and spaces are stripped automatically.
Sub-commands:
get Fetch by Edition OLID
isbn Fetch by ISBN-10 or ISBN-13",
after_help = "\
EXAMPLES
olib edition get OL7353617M
olib edition isbn 9780140328724
olib edition isbn 0-14-032-872-6
olib --json edition isbn 9780140328724 | jq '{title, publishers, publish_date}'",
)]
struct EditionArgs {
#[command(subcommand)]
cmd: EditionCmd,
}
#[derive(Subcommand)]
enum EditionCmd {
#[command(
after_help = "\
FIELDS RETURNED
key, title, subtitle, publishers, publish_date, number_of_pages, languages,
isbn_13, isbn_10, lccn, oclc_numbers, covers, physical_format, weight
EXAMPLES
olib edition get OL7353617M
olib --json edition get OL7353617M | jq '{title, isbn_13, number_of_pages}'",
)]
Get {
id: String,
},
#[command(
after_help = "\
ISBN VALIDATION
ISBN-10: 10 digits (trailing X allowed); validated with Luhn mod-11 check
ISBN-13: 13 digits starting with 978 or 979; validated with EAN-13 check
Hyphens and spaces are stripped before validation.
EXAMPLES
olib edition isbn 9780140328724
olib edition isbn 0140328726
olib edition isbn 978-0-14-032-872-4
olib --json edition isbn 9780140328724 | jq '{title, publish_date}'",
)]
Isbn {
isbn: String,
},
}
#[derive(Args)]
#[command(
about = "Fetch author records and their attributed works",
long_about = "\
Author OLIDs have the form OL<N>A (e.g. OL23919A for J. K. Rowling).
Sub-commands:
get Core author record: name, dates, bio, photo IDs, Wikipedia link
works Paginated list of works attributed to this author",
after_help = "\
EXAMPLES
olib author get OL23919A
olib author works OL23919A --limit 20
olib --json author get OL23919A | jq '{name, birth_date, wikipedia}'",
)]
struct AuthorArgs {
#[command(subcommand)]
cmd: AuthorCmd,
}
#[derive(Subcommand)]
enum AuthorCmd {
#[command(
after_help = "\
FIELDS RETURNED
key, name, personal_name, alternate_names, birth_date, death_date,
bio, location, photos (IDs), wikipedia, links
EXAMPLES
olib author get OL23919A
olib --json author get OL23919A | jq '{name, birth_date, death_date}'
olib --json author get OL23919A | jq '.photos[0]' # first photo ID",
)]
Get {
id: String,
},
#[command(
after_help = "\
EXAMPLES
olib author works OL23919A
olib author works OL23919A --limit 50
olib author works OL23919A --limit 20 --offset 40
olib --json author works OL23919A | jq '.entries[] | {key, title}'",
)]
Works {
id: String,
#[arg(long, default_value = "20", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
},
}
#[derive(Args)]
#[command(
about = "Search books, authors, subjects, lists, and book interiors",
long_about = "\
Five distinct search endpoints are available:
books Full-text search across works and editions (/search.json)
authors Search author names (/search/authors.json)
subjects Search subject names (/search/subjects.json)
lists Search user-created reading lists (/search/lists.json)
inside Full-text search inside scanned book content (/search/inside.json)
All return a result count (num_found) and a paginated list of documents.",
after_help = "\
EXAMPLES
olib search books --q \"lord of the rings\"
olib search books --author tolkien --language eng --limit 5
olib search books --title Foundation --author Asimov --sort new
olib search books --q \"python\" --subject programming --limit 10
olib search authors --q \"ursula le guin\"
olib search subjects fantasy
olib search lists \"classic sci-fi\" --limit 5
olib search inside \"call me ishmael\" --limit 3
olib --json search books --q dune | jq '.docs[] | {key, title}'",
)]
struct SearchArgs {
#[command(subcommand)]
cmd: SearchCmd,
}
#[derive(Subcommand)]
enum SearchCmd {
#[command(
after_help = "\
SORT VALUES
relevance (default) Solr relevance scoring
new Newest first (by first_publish_year)
old Oldest first
LANGUAGE CODES
Use two-letter ISO 639-1 codes: eng, fre, ger, spa, ita, por, jpn, zho, ...
EXAMPLES
olib search books --q \"lord of the rings\"
olib search books --author tolkien --limit 5
olib search books --title Dune --sort new
olib search books --q python --subject programming --language eng
olib search books --isbn 9780451450524
olib search books --place France --person Napoleon
olib --json search books --q tolkien | jq '.num_found'
olib --json search books --q tolkien | jq '.docs[] | {key, title, author_name}'",
)]
Books {
#[arg(long, value_name = "QUERY")]
q: Option<String>,
#[arg(long, value_name = "TITLE")]
title: Option<String>,
#[arg(long, value_name = "NAME")]
author: Option<String>,
#[arg(long, value_name = "ISBN")]
isbn: Option<String>,
#[arg(long, value_name = "SUBJECT")]
subject: Option<String>,
#[arg(long, value_name = "PLACE")]
place: Option<String>,
#[arg(long, value_name = "PERSON")]
person: Option<String>,
#[arg(long, value_name = "CODE")]
language: Option<String>,
#[arg(long, default_value = "10", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
#[arg(long, value_name = "ORDER")]
sort: Option<String>,
},
#[command(
after_help = "\
EXAMPLES
olib search authors --q tolkien
olib search authors --q \"ursula le guin\" --limit 3
olib --json search authors --q tolkien | jq '.docs[] | {key, name, work_count}'",
)]
Authors {
#[arg(long, value_name = "QUERY")]
q: String,
#[arg(long, default_value = "10", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
},
#[command(
after_help = "\
EXAMPLES
olib search subjects fantasy
olib search subjects \"artificial intelligence\"
olib --json search subjects \"world war\" | jq '.docs[] | {name, work_count}'",
)]
Subjects {
query: String,
},
#[command(
after_help = "\
EXAMPLES
olib search lists tolkien
olib search lists \"best sci-fi\" --limit 5
olib --json search lists tolkien | jq '.docs[] | {name, key}'",
)]
Lists {
query: String,
#[arg(long, default_value = "10", value_name = "N")]
limit: u32,
},
#[command(
after_help = "\
This endpoint searches the text inside books digitized by the Internet Archive.
Results include a short excerpt from the matching passage.
EXAMPLES
olib search inside \"call me ishmael\"
olib search inside \"lembas bread\" --limit 3
olib --json search inside \"recursion\" | jq '.docs[] | {title, author}'",
)]
Inside {
query: String,
#[arg(long, default_value = "10", value_name = "N")]
limit: u32,
},
}
#[derive(Args)]
#[command(
about = "Fetch the subject page for a topic slug",
long_about = "\
Fetches the subject page for a topic slug from /subjects/<slug>.json.
Slugs are lowercase ASCII with underscores (e.g. science_fiction, world_war_2).
The --details flag adds related subjects, top authors, and publisher breakdowns.
SLUG EXAMPLES
love science_fiction world_war_2
python_(programming) history children_s_literature",
after_help = "\
EXAMPLES
olib subject love
olib subject science_fiction --details
olib subject cyberpunk --ebooks
olib subject history --published-in 1900-1950 --limit 10
olib subject mystery --limit 10 --offset 30
olib --json subject fantasy --details | jq '.related_subjects[].name'
olib --json subject fantasy | jq '.works[] | {key, title}'",
)]
struct SubjectCliArgs {
slug: String,
#[arg(long)]
details: bool,
#[arg(long)]
ebooks: bool,
#[arg(long, value_name = "YYYY-YYYY")]
published_in: Option<String>,
#[arg(long, default_value = "20", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
}
#[derive(Args)]
#[command(
about = "Generate cover/photo URLs or fetch cover image metadata",
long_about = "\
Book cover images are served from covers.openlibrary.org. Author photos from
covers.openlibrary.org/a/olid/<OLID>-<SIZE>.jpg.
The 'url' and 'photo' sub-commands construct URLs without any network call.
The 'meta' sub-command fetches JSON metadata (width, height, URL) from the API.
KEY TYPES (for cover url and cover meta)
id Internal numeric cover ID (most stable)
isbn ISBN-10 or ISBN-13
oclc OCLC control number
lccn Library of Congress Control Number
olid Open Library edition OLID
SIZES
small (s) ~56px tall
medium (m) ~128px tall
large (l) ~400px tall",
after_help = "\
EXAMPLES
olib cover url id 5428012 large
olib cover url isbn 9780451450524 medium
olib cover url olid OL7353617M small
olib cover meta id 5428012
olib cover photo OL23919A large
# Download a cover image
curl -L \"$(olib cover url id 5428012 large)\" -o cover.jpg
# Get an author's photo
curl -L \"$(olib cover photo OL23919A medium)\" -o author.jpg",
)]
struct CoverArgs {
#[command(subcommand)]
cmd: CoverCmd,
}
#[derive(Subcommand)]
enum CoverCmd {
#[command(
after_help = "\
EXAMPLES
olib cover url id 5428012 large
olib cover url isbn 9780451450524 medium
olib cover url oclc 45883427 small
olib cover url lccn 2004046975 large
olib cover url olid OL7353617M medium
curl -L \"$(olib cover url id 5428012 large)\" -o cover.jpg",
)]
Url {
key: String,
value: String,
size: String,
},
#[command(
after_help = "\
EXAMPLES
olib cover meta id 5428012
olib --json cover meta id 5428012 | jq '{width, height, url}'",
)]
Meta {
key: String,
value: String,
},
#[command(
after_help = "\
EXAMPLES
olib cover photo OL23919A large
olib cover photo OL23919A medium
curl -L \"$(olib cover photo OL23919A large)\" -o author.jpg",
)]
Photo {
olid: String,
size: String,
},
}
#[derive(Args)]
#[command(
about = "Fetch user-created lists and their contents",
long_about = "\
Open Library users can create public reading lists containing works, editions,
authors, and subject references ('seeds').
All list endpoints are read-only in v0.1.
Sub-commands:
user Index of all public lists for a user
show Metadata for a single list (name, description, seed/edition counts)
editions Editions contained in the list (paginated)
subjects Subject tags derived from the list's works
seeds Raw list items (work keys, edition keys, subject URLs)",
after_help = "\
EXAMPLES
olib list user alice
olib list user alice --limit 10 --offset 10
olib list show alice OL123L
olib list editions alice OL123L --limit 20
olib list subjects alice OL123L
olib list seeds alice OL123L
olib --json list show alice OL123L | jq '{name, seed_count}'",
)]
struct ListArgs {
#[command(subcommand)]
cmd: ListCmd,
}
#[derive(Subcommand)]
enum ListCmd {
#[command(
after_help = "\
EXAMPLES
olib list user alice
olib list user alice --limit 10
olib list user alice --limit 10 --offset 10
olib --json list user alice | jq '.lists[] | {key, name, seed_count}'",
)]
User {
username: String,
#[arg(long, default_value = "20", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
},
#[command(
after_help = "\
EXAMPLES
olib list show alice OL123L
olib --json list show alice OL123L | jq '{name, seed_count, edition_count}'",
)]
Show {
username: String,
list_id: String,
},
#[command(
after_help = "\
EXAMPLES
olib list editions alice OL123L
olib list editions alice OL123L --limit 5
olib --json list editions alice OL123L | jq '.entries[] | {key, title}'",
)]
Editions {
username: String,
list_id: String,
#[arg(long, default_value = "20", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
},
#[command(
after_help = "\
EXAMPLES
olib list subjects alice OL123L
olib --json list subjects alice OL123L | jq '.subjects[] | {name, count}'",
)]
Subjects {
username: String,
list_id: String,
},
#[command(
after_help = "\
EXAMPLES
olib list seeds alice OL123L
olib --json list seeds alice OL123L | jq '.entries'",
)]
Seeds {
username: String,
list_id: String,
},
}
#[derive(Args)]
#[command(
about = "Fetch a user's public reading log shelves",
long_about = "\
Open Library users maintain three public reading shelves:
want-to-read Books the user intends to read
currently-reading Books the user is actively reading
already-read Books the user has finished
Each entry includes the work key, title, author names, and the date logged.",
after_help = "\
EXAMPLES
olib reading want-to-read alice
olib reading currently-reading alice
olib reading already-read alice
olib --json reading already-read alice | jq '.reading_log_entries[] | .work.title'
olib --json reading want-to-read alice | jq '[.reading_log_entries[] | .work.key]'",
)]
struct ReadingArgs {
#[command(subcommand)]
cmd: ReadingCmd,
}
#[derive(Subcommand)]
enum ReadingCmd {
#[command(after_help = "EXAMPLE\n olib reading want-to-read alice")]
WantToRead {
username: String,
},
#[command(after_help = "EXAMPLE\n olib reading currently-reading alice")]
CurrentlyReading {
username: String,
},
#[command(
after_help = "\
EXAMPLES
olib reading already-read alice
olib --json reading already-read alice | jq '.reading_log_entries[] | .work.title'",
)]
AlreadyRead {
username: String,
},
}
#[derive(Args)]
#[command(
about = "Browse the Open Library recent-changes feed",
long_about = "\
Fetches edits from /recentchanges.json (or sub-paths filtered by date / kind).
Without any filters, returns the most recent changes across all types.
All three filters (--date, --kind, --bot) can be combined freely.
KIND VALUES
add-cover A new cover image was added
add-book A new edition or work was created
edit-book An existing edition or work was updated
merge-authors Two author records were merged
update A generic update
revert A previous edit was reverted
new-account A new user account was created
register A registration event
lists A list was created or modified",
after_help = "\
EXAMPLES
olib changes
olib changes --limit 50
olib changes --date 2024-06-15
olib changes --kind edit-book --limit 100
olib changes --date 2024-06-15 --kind add-cover
olib changes --bot false --limit 50
olib --json changes --kind edit-book --limit 5 | jq '.[].key'
olib --json changes --date 2024-01-01 | jq '.[].comment'",
)]
struct ChangesCliArgs {
#[arg(long, value_name = "YYYY-MM-DD")]
date: Option<String>,
#[arg(
long,
value_name = "KIND",
long_help = "Filter by change kind. Values: add-cover | add-book | edit-book | \
merge-authors | update | revert | new-account | register | lists"
)]
kind: Option<String>,
#[arg(long, default_value = "20", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
#[arg(long, value_name = "BOOL")]
bot: Option<bool>,
}
#[derive(Args)]
#[command(
about = "Resolve book identifiers via the Partner / Volumes API",
long_about = "\
The Partner API maps standard book identifiers to borrowing / reading availability
information from the Internet Archive's Open Library.
Response includes:
records Bibliographic data keyed by Open Library path
items Available copies with status (borrowable/readable/limited/unavailable)
Sub-commands:
isbn Look up by ISBN (10 or 13 digits)
lccn Look up by Library of Congress Control Number
oclc Look up by OCLC / WorldCat control number
olid Look up by Open Library edition OLID (OL<N>M)
batch Look up multiple identifiers in a single request",
after_help = "\
EXAMPLES
olib volume isbn 0451450523
olib volume lccn 2004046975
olib volume oclc 45883427
olib volume olid OL7408846M
olib volume batch isbn/0451450523 oclc/45883427
olib --json volume isbn 0451450523 | jq '.items[].status'
olib --json volume isbn 0451450523 | jq '.records | keys'",
)]
struct VolumeArgs {
#[command(subcommand)]
cmd: VolumeCmd,
}
#[derive(Subcommand)]
enum VolumeCmd {
#[command(after_help = "EXAMPLES\n olib volume isbn 0451450523\n olib volume isbn 9780451450524")]
Isbn {
value: String,
},
#[command(after_help = "EXAMPLE\n olib volume lccn 2004046975")]
Lccn {
value: String,
},
#[command(after_help = "EXAMPLE\n olib volume oclc 45883427")]
Oclc {
value: String,
},
#[command(after_help = "EXAMPLE\n olib volume olid OL7408846M")]
Olid {
value: String,
},
#[command(
after_help = "\
EXAMPLES
olib volume batch isbn/0451450523 oclc/45883427
olib volume batch isbn/0451450523 isbn/9780140328724 lccn/2004046975
olib --json volume batch isbn/0451450523 oclc/45883427 | jq '.records | keys'",
)]
Batch {
requests: Vec<String>,
},
}
#[derive(Args)]
#[command(
about = "Look up books by bibliographic key via /api/books",
long_about = "\
The /api/books endpoint accepts one or more bibliographic keys and returns
structured data for each matched book.
BIBKEY FORMAT
<PREFIX>:<VALUE> e.g. ISBN:9780451450524
VALID PREFIXES
ISBN International Standard Book Number (10 or 13 digits)
OCLC OCLC / WorldCat control number
LCCN Library of Congress Control Number
OLID Open Library edition identifier (OL<N>M)
ID Internal Open Library cover / record ID
JSCMD VALUES
data (default) Full bibliographic data bundle per book
details Structured details including subjects, excerpts, and identifiers
viewapi Preview URLs and read-online / borrow links",
after_help = "\
EXAMPLES
olib books ISBN:9780451450524
olib books ISBN:0451450523 OCLC:45883427
olib books ISBN:0451450523 OCLC:45883427 LCCN:2004046975
olib books OLID:OL7408846M ID:5428012
olib books ISBN:0451450523 --jscmd details
olib books ISBN:0451450523 --jscmd viewapi
olib --json books ISBN:0451450523 | jq '.'",
)]
struct BooksCliArgs {
bibkeys: Vec<String>,
#[arg(long, default_value = "data", value_name = "CMD")]
jscmd: String,
}
#[derive(Args)]
#[command(
about = "Query any Open Library object type via /query.json",
long_about = "\
The /query.json endpoint lets you filter any Open Library object type by
field values. It is a low-level building block useful for lookups not covered
by the dedicated endpoints.
OBJECT TYPES (--type)
/type/work Works (books as abstract creative entities)
/type/edition Editions (specific printings)
/type/author Author records
/type/subject Subject records
/type/list User-created lists
/type/user User accounts
FIELD FILTERS (--field)
Pass one or more key=value pairs. All filters are AND-ed.
Field names match the JSON keys in the object's schema.",
after_help = "\
EXAMPLES
olib query --type /type/edition --field isbn=0451450523
olib query --type /type/work --field title=Dune
olib query --type /type/edition --field publishers=Penguin
olib query --type /type/edition --field publish_date=1988 --limit 5
olib --json query --type /type/edition --field isbn=0451450523 | jq '.result'",
)]
struct QueryCliArgs {
#[arg(long, value_name = "TYPE")]
r#type: String,
#[arg(long, value_name = "KEY=VALUE")]
field: Vec<String>,
#[arg(long, default_value = "20", value_name = "N")]
limit: u32,
#[arg(long, default_value = "0", value_name = "N")]
offset: u32,
}
#[derive(Args)]
#[command(
about = "Fetch the full revision history of an Open Library resource",
long_about = "\
Appends ?m=history to any Open Library resource key and returns the full list
of revisions, each with a timestamp, editor, and optional edit comment.
The key is an Open Library path starting with /works/, /books/, or /authors/.",
after_help = "\
EXAMPLES
olib history /works/OL45804W
olib history /books/OL7353617M
olib history /authors/OL23919A
olib --json history /works/OL45804W | jq '.[0] | {revision, timestamp, comment}'
olib --json history /works/OL45804W | jq 'length'",
)]
struct HistoryArgs {
key: String,
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let client = match build_client(&cli) {
Ok(c) => c,
Err(e) => {
eprintln!("error: {e}");
process::exit(1);
}
};
if let Err(e) = dispatch(&cli, &client).await {
eprintln!("error: {e}");
process::exit(1);
}
}
fn build_client(cli: &Cli) -> Result<OpenLibraryClient> {
let mut builder = OpenLibraryClient::builder().rate_limit(cli.rate_limit);
if let Some(email) = &cli.email {
builder = builder.contact_email(email.as_str())?;
}
if let Some(url) = &cli.base_url {
builder = builder.base_url(url.as_str());
}
if let Some(url) = &cli.covers_url {
builder = builder.covers_url(url.as_str());
}
builder.build()
}
async fn dispatch(cli: &Cli, client: &OpenLibraryClient) -> Result<()> {
let json = cli.json;
match &cli.command {
Cmd::Work(a) => handle_work(json, client, &a.cmd).await,
Cmd::Edition(a) => handle_edition(json, client, &a.cmd).await,
Cmd::Author(a) => handle_author(json, client, &a.cmd).await,
Cmd::Search(a) => handle_search(json, client, &a.cmd).await,
Cmd::Subject(a) => handle_subject(json, client, a).await,
Cmd::Cover(a) => handle_cover(json, client, &a.cmd).await,
Cmd::List(a) => handle_list(json, client, &a.cmd).await,
Cmd::Reading(a) => handle_reading(json, client, &a.cmd).await,
Cmd::Changes(a) => handle_changes(json, client, a).await,
Cmd::Volume(a) => handle_volume(json, client, &a.cmd).await,
Cmd::Books(a) => handle_books(json, client, a).await,
Cmd::Query(a) => handle_query(json, client, a).await,
Cmd::History(a) => handle_history(json, client, a).await,
}
}
async fn handle_work(json: bool, client: &OpenLibraryClient, cmd: &WorkCmd) -> Result<()> {
match cmd {
WorkCmd::Get { id } => {
let v = client.get_work(id).await?;
if json { print_json(&v) } else { print_work(&v) }
}
WorkCmd::Editions { id, limit, offset } => {
let v = client.get_work_editions(id, *limit, *offset).await?;
if json { print_json(&v) } else { print_work_editions(&v) }
}
WorkCmd::Ratings { id } => {
let v = client.get_work_ratings(id).await?;
if json { print_json(&v) } else { print_work_ratings(&v) }
}
WorkCmd::Bookshelves { id } => {
let v = client.get_work_bookshelves(id).await?;
if json { print_json(&v) } else { print_work_bookshelves(&v) }
}
}
Ok(())
}
async fn handle_edition(json: bool, client: &OpenLibraryClient, cmd: &EditionCmd) -> Result<()> {
match cmd {
EditionCmd::Get { id } => {
let v = client.get_edition(id).await?;
if json { print_json(&v) } else { print_edition(&v) }
}
EditionCmd::Isbn { isbn } => {
let v = client.get_edition_by_isbn(isbn).await?;
if json { print_json(&v) } else { print_edition(&v) }
}
}
Ok(())
}
async fn handle_author(json: bool, client: &OpenLibraryClient, cmd: &AuthorCmd) -> Result<()> {
match cmd {
AuthorCmd::Get { id } => {
let v = client.get_author(id).await?;
if json { print_json(&v) } else { print_author(&v) }
}
AuthorCmd::Works { id, limit, offset } => {
let v = client.get_author_works(id, *limit, *offset).await?;
if json { print_json(&v) } else { print_author_works(&v) }
}
}
Ok(())
}
async fn handle_search(json: bool, client: &OpenLibraryClient, cmd: &SearchCmd) -> Result<()> {
match cmd {
SearchCmd::Books {
q, title, author, isbn, subject, place, person, language,
limit, offset, sort,
} => {
let params = SearchParams {
q: q.clone(),
title: title.clone(),
author: author.clone(),
isbn: isbn.clone(),
subject: subject.clone(),
place: place.clone(),
person: person.clone(),
language: language.clone(),
limit: Some(*limit),
offset: Some(*offset),
sort: sort.clone(),
..Default::default()
};
let v = client.search(params).await?;
if json { print_json(&v) } else { print_search_books(&v) }
}
SearchCmd::Authors { q, limit, offset } => {
let params = AuthorSearchParams {
q: Some(q.clone()),
limit: Some(*limit),
offset: Some(*offset),
};
let v = client.search_authors(params).await?;
if json { print_json(&v) } else { print_search_authors(&v) }
}
SearchCmd::Subjects { query } => {
let v = client.search_subjects(query).await?;
if json { print_json(&v) } else { print_search_subjects(&v) }
}
SearchCmd::Lists { query, limit } => {
let v = client.search_lists(query, Some(*limit)).await?;
if json { print_json(&v) } else { print_search_lists(&v) }
}
SearchCmd::Inside { query, limit } => {
let v = client.search_inside(query, Some(*limit)).await?;
if json { print_json(&v) } else { print_search_inside(&v) }
}
}
Ok(())
}
async fn handle_subject(json: bool, client: &OpenLibraryClient, a: &SubjectCliArgs) -> Result<()> {
let params = SubjectParams {
details: if a.details { Some(true) } else { None },
ebooks: if a.ebooks { Some(true) } else { None },
published_in: a.published_in.clone(),
limit: Some(a.limit),
offset: Some(a.offset),
};
let v = client.get_subject(&a.slug, params).await?;
if json { print_json(&v) } else { print_subject(&v) }
Ok(())
}
async fn handle_cover(json: bool, client: &OpenLibraryClient, cmd: &CoverCmd) -> Result<()> {
match cmd {
CoverCmd::Url { key, value, size } => {
let ck = parse_cover_key(key)?;
let sz = parse_image_size(size)?;
let url = client.cover_url(ck, value, sz);
println!("{url}");
}
CoverCmd::Meta { key, value } => {
let ck = parse_cover_key(key)?;
let v = client.cover_meta(ck, value).await?;
if json { print_json(&v) } else { print_cover_meta(&v) }
}
CoverCmd::Photo { olid, size } => {
let sz = parse_image_size(size)?;
let url = client.author_photo_url(olid, sz)?;
println!("{url}");
}
}
Ok(())
}
async fn handle_list(json: bool, client: &OpenLibraryClient, cmd: &ListCmd) -> Result<()> {
match cmd {
ListCmd::User { username, limit, offset } => {
let v = client.get_user_lists(username, *limit, *offset).await?;
if json { print_json(&v) } else { print_user_lists(&v) }
}
ListCmd::Show { username, list_id } => {
let v = client.get_list(username, list_id).await?;
if json { print_json(&v) } else { print_list(&v) }
}
ListCmd::Editions { username, list_id, limit, offset } => {
let v = client.get_list_editions(username, list_id, *limit, *offset).await?;
if json { print_json(&v) } else { print_list_editions(&v) }
}
ListCmd::Subjects { username, list_id } => {
let v = client.get_list_subjects(username, list_id).await?;
if json { print_json(&v) } else { print_list_subjects(&v) }
}
ListCmd::Seeds { username, list_id } => {
let v = client.get_list_seeds(username, list_id).await?;
if json { print_json(&v) } else { print_list_seeds(&v) }
}
}
Ok(())
}
async fn handle_reading(json: bool, client: &OpenLibraryClient, cmd: &ReadingCmd) -> Result<()> {
let (v, username) = match cmd {
ReadingCmd::WantToRead { username } => (client.get_want_to_read(username).await?, username),
ReadingCmd::CurrentlyReading { username } => {
(client.get_currently_reading(username).await?, username)
}
ReadingCmd::AlreadyRead { username } => (client.get_already_read(username).await?, username),
};
if json { print_json(&v) } else { print_reading_log(username, &v) }
Ok(())
}
async fn handle_changes(json: bool, client: &OpenLibraryClient, a: &ChangesCliArgs) -> Result<()> {
let params = ChangesParams {
limit: Some(a.limit),
offset: Some(a.offset),
bot: a.bot,
};
let v = match (&a.date, &a.kind) {
(Some(date), Some(kind)) => {
let ck = parse_change_kind(kind)?;
client.get_changes_by_date_and_kind(date, &ck, params).await?
}
(Some(date), None) => client.get_changes_by_date(date, params).await?,
(None, Some(kind)) => {
let ck = parse_change_kind(kind)?;
client.get_changes_by_kind(&ck, params).await?
}
(None, None) => client.get_recent_changes(params).await?,
};
if json { print_json(&v) } else { print_changes(&v) }
Ok(())
}
async fn handle_volume(json: bool, client: &OpenLibraryClient, cmd: &VolumeCmd) -> Result<()> {
let v = match cmd {
VolumeCmd::Isbn { value } => client.read_volume(VolumeIdType::Isbn, value).await?,
VolumeCmd::Lccn { value } => client.read_volume(VolumeIdType::Lccn, value).await?,
VolumeCmd::Oclc { value } => client.read_volume(VolumeIdType::Oclc, value).await?,
VolumeCmd::Olid { value } => client.read_volume(VolumeIdType::Olid, value).await?,
VolumeCmd::Batch { requests } => client.read_volumes_batch(requests).await?,
};
if json { print_json(&v) } else { print_volumes(&v) }
Ok(())
}
async fn handle_books(
json: bool,
client: &OpenLibraryClient,
a: &BooksCliArgs,
) -> Result<()> {
let jscmd = match a.jscmd.as_str() {
"details" => BooksJsCmd::Details,
"viewapi" => BooksJsCmd::ViewApi,
_ => BooksJsCmd::Data,
};
let v = client.get_books(&a.bibkeys, jscmd).await?;
if json {
print_json(&v);
} else {
for (key, entry) in &v {
show("Bibkey", key);
if let Some(u) = &entry.info_url { show("URL", u); }
if let Some(p) = &entry.preview { show("Preview", p); }
if let Some(t) = &entry.thumbnail_url { show("Thumbnail", t); }
println!();
}
}
Ok(())
}
async fn handle_query(json: bool, client: &OpenLibraryClient, a: &QueryCliArgs) -> Result<()> {
let mut fields: HashMap<String, String> = HashMap::new();
for f in &a.field {
if let Some((k, v)) = f.split_once('=') {
fields.insert(k.to_string(), v.to_string());
} else {
eprintln!("warning: ignoring malformed --field value (expected key=value): {f}");
}
}
let v = client.query(&a.r#type, &fields, a.limit, a.offset).await?;
if json {
print_json(&v);
} else {
print_query_results(&v);
}
Ok(())
}
async fn handle_history(json: bool, client: &OpenLibraryClient, a: &HistoryArgs) -> Result<()> {
let v = client.get_resource_history(&a.key).await?;
if json { print_json(&v) } else { print_history(&v) }
Ok(())
}
fn print_json(v: &impl serde::Serialize) {
match serde_json::to_string_pretty(v) {
Ok(s) => println!("{s}"),
Err(e) => eprintln!("error: failed to serialize response: {e}"),
}
}
fn show(label: &str, value: &str) {
println!("{:<22} {}", format!("{label}:"), value);
}
fn print_work(w: &Work) {
show("Key", &w.key);
if let Some(t) = &w.title { show("Title", t); }
if let Some(s) = &w.subtitle { show("Subtitle", s); }
if let Some(d) = &w.description { show("Description", d.as_str()); }
if let Some(a) = &w.authors {
let keys: Vec<_> = a.iter().map(|r| r.author.key.as_str()).collect();
show("Authors", &keys.join(", "));
}
if let Some(s) = &w.subjects { show("Subjects", &s.join(", ")); }
if let Some(p) = &w.subject_places { show("Places", &p.join(", ")); }
if let Some(p) = &w.subject_people { show("People", &p.join(", ")); }
if let Some(t) = &w.subject_times { show("Times", &t.join(", ")); }
if let Some(d) = &w.first_publish_date { show("First Published", d); }
if let Some(c) = &w.covers {
let ids: Vec<_> = c.iter().map(|n| n.to_string()).collect();
show("Covers", &ids.join(", "));
}
if let Some(r) = &w.latest_revision { show("Revision", &r.to_string()); }
}
fn print_work_editions(we: &WorkEditions) {
let count = we.size.unwrap_or(we.entries.len() as u64);
println!("Editions ({count} total):");
for (i, e) in we.entries.iter().enumerate() {
let title = e.title.as_deref().unwrap_or("(no title)");
let year = e.publish_date.as_deref().unwrap_or("?");
println!(" [{:>3}] {} — {} ({})", i + 1, e.key, title, year);
}
}
fn print_work_ratings(r: &WorkRatings) {
if let Some(s) = &r.summary {
if let Some(avg) = s.average { show("Average", &format!("{avg:.2}")); }
if let Some(n) = s.count { show("Ratings", &n.to_string()); }
}
if let Some(c) = &r.counts {
println!("Distribution:");
for (stars, n) in [
(5, c.five), (4, c.four), (3, c.three), (2, c.two), (1, c.one),
] {
let n = n.unwrap_or(0);
println!(" {stars}★ {n}");
}
}
}
fn print_work_bookshelves(b: &WorkBookshelves) {
if let Some(c) = &b.counts {
show("Want to Read", &c.want_to_read.unwrap_or(0).to_string());
show("Currently Reading", &c.currently_reading.unwrap_or(0).to_string());
show("Already Read", &c.already_read.unwrap_or(0).to_string());
}
}
fn print_edition(e: &Edition) {
show("Key", &e.key);
if let Some(t) = &e.title { show("Title", t); }
if let Some(s) = &e.subtitle { show("Subtitle", s); }
if let Some(p) = &e.publishers { show("Publishers", &p.join(", ")); }
if let Some(d) = &e.publish_date { show("Published", d); }
if let Some(n) = e.number_of_pages { show("Pages", &n.to_string()); }
if let Some(l) = &e.languages {
let keys: Vec<_> = l.iter().map(|k| k.key.as_str()).collect();
show("Languages", &keys.join(", "));
}
if let Some(v) = &e.isbn_13 { show("ISBN-13", &v.join(", ")); }
if let Some(v) = &e.isbn_10 { show("ISBN-10", &v.join(", ")); }
if let Some(v) = &e.lccn { show("LCCN", &v.join(", ")); }
if let Some(v) = &e.oclc_numbers { show("OCLC", &v.join(", ")); }
if let Some(f) = &e.physical_format { show("Format", f); }
if let Some(w) = &e.weight { show("Weight", w); }
if let Some(c) = &e.covers {
let ids: Vec<_> = c.iter().map(|n| n.to_string()).collect();
show("Covers", &ids.join(", "));
}
}
fn print_author(a: &Author) {
show("Key", &a.key);
if let Some(n) = &a.name { show("Name", n); }
if let Some(p) = &a.personal_name { show("Personal Name", p); }
if let Some(alt) = &a.alternate_names { show("Also Known As", &alt.join(", ")); }
if let Some(b) = &a.birth_date { show("Born", b); }
if let Some(d) = &a.death_date { show("Died", d); }
if let Some(bio) = &a.bio { show("Bio", bio.as_str()); }
if let Some(w) = &a.wikipedia { show("Wikipedia", w); }
if let Some(p) = &a.photos {
let ids: Vec<_> = p.iter().map(|n| n.to_string()).collect();
show("Photos", &ids.join(", "));
}
}
fn print_author_works(aw: &AuthorWorks) {
println!("Works ({} shown):", aw.entries.len());
for (i, e) in aw.entries.iter().enumerate() {
let title = e.title.as_deref().unwrap_or("(no title)");
println!(" [{:>3}] {} — {}", i + 1, e.key, title);
}
}
fn print_search_books(r: &SearchResponse<BookDoc>) {
println!("Found {} result{} (showing {}):",
r.num_found,
if r.num_found == 1 { "" } else { "s" },
r.docs.len());
println!();
for (i, doc) in r.docs.iter().enumerate() {
let title = doc.title.as_deref().unwrap_or("(no title)");
println!("[{}] {}", i + 1, title);
println!(" Key: {}", doc.key);
if let Some(authors) = &doc.author_name {
println!(" Authors: {}", authors.join(", "));
}
if let Some(year) = doc.first_publish_year {
println!(" Year: {year}");
}
println!(" Editions: {}", doc.edition_count);
println!();
}
}
fn print_search_authors(r: &SearchResponse<AuthorDoc>) {
println!("Found {} author{} (showing {}):",
r.num_found,
if r.num_found == 1 { "" } else { "s" },
r.docs.len());
println!();
for (i, doc) in r.docs.iter().enumerate() {
let name = doc.name.as_deref().unwrap_or("(no name)");
println!("[{}] {} — {}", i + 1, name, doc.key);
if let Some(b) = &doc.birth_date { println!(" Born: {b}"); }
if let Some(n) = doc.work_count { println!(" Works: {n}"); }
println!();
}
}
fn print_search_subjects(r: &SearchResponse<SubjectDoc>) {
println!("Found {} subject{} (showing {}):",
r.num_found,
if r.num_found == 1 { "" } else { "s" },
r.docs.len());
for doc in &r.docs {
let name = doc.name.as_deref().unwrap_or("(no name)");
let count = doc.work_count.unwrap_or(0);
println!(" {} ({count} works) — {}", name, doc.key);
}
}
fn print_search_lists(r: &SearchResponse<ListDoc>) {
println!("Found {} list{} (showing {}):",
r.num_found,
if r.num_found == 1 { "" } else { "s" },
r.docs.len());
for doc in &r.docs {
let name = doc.name.as_deref().unwrap_or("(unnamed)");
println!(" {} — {}", name, doc.key);
}
}
fn print_search_inside(r: &SearchResponse<InsideDoc>) {
println!("Found {} result{} (showing {}):",
r.num_found,
if r.num_found == 1 { "" } else { "s" },
r.docs.len());
for (i, doc) in r.docs.iter().enumerate() {
let title = doc.title.as_deref().unwrap_or("(no title)");
println!("[{}] {}", i + 1, title);
if let Some(a) = &doc.author { println!(" Author: {a}"); }
if let Some(t) = &doc.text { println!(" Excerpt: {}…", &t[..t.len().min(120)]); }
println!();
}
}
fn print_subject(s: &Subject) {
show("Key", &s.key);
if let Some(n) = &s.name { show("Name", n); }
if let Some(k) = &s.subject_type { show("Type", k); }
if let Some(c) = s.work_count { show("Work Count", &c.to_string()); }
println!();
if !s.works.is_empty() {
println!("Works ({} shown):", s.works.len());
for (i, w) in s.works.iter().enumerate() {
let title = w.title.as_deref().unwrap_or("(no title)");
println!(" [{:>3}] {} — {}", i + 1, w.key, title);
}
}
if let Some(authors) = &s.authors {
println!();
println!("Authors:");
for a in authors {
let name = a.name.as_deref().unwrap_or("(unknown)");
println!(" {} — {}", name, a.key);
}
}
if let Some(rel) = &s.related_subjects {
println!();
println!("Related Subjects:");
for r in rel {
println!(" {}", r.name);
}
}
}
fn print_cover_meta(metas: &[CoverMeta]) {
if metas.is_empty() {
println!("No cover metadata found.");
return;
}
for m in metas {
if let Some(id) = m.id { show("Cover ID", &id.to_string()); }
if let Some(s) = &m.size { show("Size", s); }
if let Some(u) = &m.url { show("URL", u); }
println!();
}
}
fn print_user_lists(ul: &UserLists) {
let total = ul.size.unwrap_or(ul.lists.len() as u64);
println!("Lists ({total} total, {} shown):", ul.lists.len());
for l in &ul.lists {
let name = l.name.as_deref().unwrap_or("(unnamed)");
let seeds = l.seed_count.unwrap_or(0);
println!(" {} — {} ({seeds} seeds)", name, l.key);
}
}
fn print_list(l: &List) {
show("Key", &l.key);
if let Some(n) = &l.name { show("Name", n); }
if let Some(d) = &l.description { show("Description", d); }
if let Some(t) = &l.tags { show("Tags", &t.join(", ")); }
if let Some(s) = l.seed_count { show("Seeds", &s.to_string()); }
if let Some(e) = l.edition_count { show("Editions", &e.to_string()); }
if let Some(u) = &l.last_update { show("Last Updated", u); }
}
fn print_list_editions(le: &ListEditions) {
let total = le.size.unwrap_or(le.entries.len() as u64);
println!("Editions ({total} total, {} shown):", le.entries.len());
for (i, e) in le.entries.iter().enumerate() {
let title = e.title.as_deref().unwrap_or("(no title)");
let date = e.publish_date.as_deref().unwrap_or("?");
println!(" [{:>3}] {} — {} ({})", i + 1, e.key, title, date);
}
}
fn print_list_subjects(ls: &ListSubjects) {
if !ls.subjects.is_empty() {
println!("Subjects:");
for s in &ls.subjects {
println!(" {} ({})", s.name, s.count.unwrap_or(0));
}
}
if !ls.places.is_empty() {
println!("Places:");
for s in &ls.places {
println!(" {} ({})", s.name, s.count.unwrap_or(0));
}
}
if !ls.people.is_empty() {
println!("People:");
for s in &ls.people {
println!(" {} ({})", s.name, s.count.unwrap_or(0));
}
}
if !ls.times.is_empty() {
println!("Times:");
for s in &ls.times {
println!(" {} ({})", s.name, s.count.unwrap_or(0));
}
}
}
fn print_list_seeds(ls: &ListSeeds) {
let total = ls.size.unwrap_or(ls.entries.len() as u64);
println!("Seeds ({total} total, {} shown):", ls.entries.len());
for seed in &ls.entries {
match seed {
ListSeed::Key(k) => println!(" {}", k.key),
ListSeed::Subject { url, title } => println!(" {title} — {url}"),
}
}
}
fn print_reading_log(username: &str, log: &ReadingLog) {
println!("{}'s reading log ({} entries):", username, log.reading_log_entries.len());
println!();
for (i, entry) in log.reading_log_entries.iter().enumerate() {
if let Some(work) = &entry.work {
let title = work.title.as_deref().unwrap_or("(no title)");
println!("[{}] {} — {}", i + 1, title, work.key);
if let Some(authors) = &work.author_names {
println!(" Authors: {}", authors.join(", "));
}
if let Some(d) = &entry.logged_date {
println!(" Logged: {d}");
}
println!();
}
}
}
fn print_changes(changes: &[RecentChange]) {
println!("{} change{}:", changes.len(), if changes.len() == 1 { "" } else { "s" });
println!();
for c in changes {
let kind = c.kind.as_ref().map(|k| k.as_str()).unwrap_or("?");
let key = c.key.as_deref().unwrap_or("?");
let ts = c.timestamp.as_deref().unwrap_or("?");
let comment = c.comment.as_deref().unwrap_or("");
println!(" [{kind}] {key}");
println!(" {ts} {comment}");
}
}
fn print_volumes(v: &VolumesResponse) {
if v.records.is_empty() && v.items.is_empty() {
println!("No volumes found.");
return;
}
println!("Records ({}):", v.records.len());
for (key, rec) in &v.records {
println!(" {key}");
if let Some(title) = &rec.title { println!(" Title: {title}"); }
if let Some(url) = &rec.url { println!(" URL: {url}"); }
}
println!();
println!("Items ({}):", v.items.len());
for item in &v.items {
let status = item.status.as_ref().map(|s| format!("{s:?}")).unwrap_or_default();
let url = item.url.as_deref().unwrap_or("?");
println!(" [{status}] {url}");
}
}
fn print_query_results(q: &QueryResponse) {
println!("{} result{}:", q.result.len(), if q.result.len() == 1 { "" } else { "s" });
for r in &q.result {
println!(" {r}");
}
}
fn print_history(entries: &[HistoryEntry]) {
println!("{} revision{}:", entries.len(), if entries.len() == 1 { "" } else { "s" });
println!();
for e in entries {
let rev = e.revision.unwrap_or(0);
let ts = e.timestamp.as_deref().unwrap_or("?");
let comment = e.comment.as_deref().unwrap_or("");
println!(" r{rev} {ts} {comment}");
}
}
fn parse_cover_key(s: &str) -> Result<CoverKey> {
Ok(match s.to_lowercase().as_str() {
"id" => CoverKey::Id,
"isbn" => CoverKey::Isbn,
"oclc" => CoverKey::Oclc,
"lccn" => CoverKey::Lccn,
"olid" => CoverKey::Olid,
_ => return Err(open_library_api_rs::Error::InvalidInput(
format!("unknown cover key type '{s}': expected id|isbn|oclc|lccn|olid")
)),
})
}
fn parse_image_size(s: &str) -> Result<ImageSize> {
Ok(match s.to_lowercase().as_str() {
"s" | "small" => ImageSize::Small,
"m" | "medium" => ImageSize::Medium,
"l" | "large" => ImageSize::Large,
_ => return Err(open_library_api_rs::Error::InvalidInput(
format!("unknown image size '{s}': expected small|medium|large (or s|m|l)")
)),
})
}
fn parse_change_kind(s: &str) -> Result<ChangeKind> {
Ok(match s {
"add-cover" => ChangeKind::AddCover,
"add-book" => ChangeKind::AddBook,
"edit-book" => ChangeKind::EditBook,
"merge-authors" => ChangeKind::MergeAuthors,
"update" => ChangeKind::Update,
"revert" => ChangeKind::Revert,
"new-account" => ChangeKind::NewAccount,
"register" => ChangeKind::Register,
"lists" => ChangeKind::Lists,
_ => return Err(open_library_api_rs::Error::InvalidInput(
format!("unknown change kind '{s}'")
)),
})
}