aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorA Farzat <a@farzat.xyz>2026-02-12 14:23:43 +0300
committerA Farzat <a@farzat.xyz>2026-02-12 14:23:43 +0300
commit57bc69a7f9af497526695e5a0bfbc60939f667e9 (patch)
tree10d70a1334128cf8894d0c759b8dac2b3d752968
parent2d9314aa3145ec7948341f38164e13c2a2d945ad (diff)
downloadsafaribooks-rs-57bc69a7f9af497526695e5a0bfbc60939f667e9.tar.gz
safaribooks-rs-57bc69a7f9af497526695e5a0bfbc60939f667e9.zip
Add the ability to fetch book info
-rw-r--r--Cargo.lock96
-rw-r--r--Cargo.toml2
-rw-r--r--src/main.rs11
-rw-r--r--src/orly.rs29
4 files changed, 135 insertions, 3 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ef277f6..1b1a448 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,6 +3,12 @@
version = 4
[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -68,6 +74,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]]
+name = "async-compression"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40"
+dependencies = [
+ "compression-codecs",
+ "compression-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -224,6 +242,23 @@ dependencies = [
]
[[package]]
+name = "compression-codecs"
+version = "0.4.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a"
+dependencies = [
+ "compression-core",
+ "flate2",
+ "memchr",
+]
+
+[[package]]
+name = "compression-core"
+version = "0.4.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
+
+[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -240,6 +275,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -263,6 +307,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -293,6 +347,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -661,6 +721,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -883,6 +953,8 @@ dependencies = [
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
+ "serde",
+ "serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
@@ -1111,6 +1183,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
+name = "simd-adler32"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+
+[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1292,6 +1370,19 @@ dependencies = [
]
[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "tower"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1312,13 +1403,18 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
+ "async-compression",
"bitflags",
"bytes",
+ "futures-core",
"futures-util",
"http",
"http-body",
+ "http-body-util",
"iri-string",
"pin-project-lite",
+ "tokio",
+ "tokio-util",
"tower",
"tower-layer",
"tower-service",
diff --git a/Cargo.toml b/Cargo.toml
index 42e52da..70b1e11 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,7 @@ edition = "2024"
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
colored = "3.1"
-reqwest = { version = "0.13", default-features = false, features = ["rustls"] }
+reqwest = { version = "0.13", default-features = false, features = ["gzip", "json", "rustls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.49", features = ["rt-multi-thread", "macros"] }
diff --git a/src/main.rs b/src/main.rs
index f79f191..d393a55 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,8 +10,7 @@ use cli::Args;
use cookies::CookieStore;
use display::Display;
use http_client::HttpClient;
-use orly::check_login;
-use reqwest::Client;
+use orly::{check_login, fetch_book_info};
#[tokio::main]
async fn main() {
@@ -60,6 +59,14 @@ async fn main() {
Err(e) => ui.error_and_exit(&format!("Login check failed: {e}")),
};
+ // Retrieve book info.
+ ui.info("Retrieving book info...");
+ let bookinfo = match fetch_book_info(&client, &args.bookid).await {
+ Ok(info) => info,
+ Err(e) => ui.error_and_exit(&format!("Failed to fetch book info: {}", e)),
+ };
+ ui.info(&format!("{:#?}", bookinfo));
+
let output_dir = config::books_root().join(format!("(pending) ({})", args.bookid));
ui.set_output_dir(output_dir);
diff --git a/src/orly.rs b/src/orly.rs
index cd8b645..0e34e5c 100644
--- a/src/orly.rs
+++ b/src/orly.rs
@@ -1,8 +1,16 @@
use crate::http_client::HttpClient;
use anyhow::{bail, Result};
+use serde::Deserialize;
pub const PROFILE_URL: &str = "https://learning.oreilly.com/profile/";
+/// Minimal subset of the book that we care about.
+#[derive(Debug, Deserialize)]
+pub struct BookInfo {
+ pub title: String,
+ pub web_url: String,
+}
+
/// Check whether cookies keep us logged in by fetching the profile page.
/// Returns:
/// - Ok(true) => HTTP 200 (assume logged in)
@@ -20,3 +28,24 @@ pub async fn check_login(client: &HttpClient) -> Result<bool> {
bail!("Profile request returned unexpected status {}", status)
}
}
+
+/// Build the v1 API URL for the book.
+pub fn book_api_url(bookid: &str) -> String {
+ format!("https://learning.oreilly.com/api/v1/book/{bookid}")
+}
+
+/// Fetch book metadata from the website.
+pub async fn fetch_book_info(client: &HttpClient, bookid: &str) -> Result<BookInfo> {
+ let url = book_api_url(bookid);
+ let res = client.client().get(url).send().await?;
+ let status = res.status();
+
+ if status == 200 {
+ let info = res.json::<BookInfo>().await?;
+ return Ok(info);
+ }
+ if status == 404 {
+ bail!("Book not found (HTTP 404). Please double-check the book ID provided")
+ }
+ bail!("Got status: {}", status)
+}