diff --git a/Cargo.lock b/Cargo.lock index ea87fe0..4906efd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,9 +192,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.0.0-beta.4" +version = "3.0.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406" +checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63" dependencies = [ "atty", "bitflags", @@ -205,14 +205,14 @@ dependencies = [ "strsim", "termcolor", "textwrap", - "vec_map", + "unicase", ] [[package]] name = "clap_derive" -version = "3.0.0-beta.4" +version = "3.0.0-beta.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac" +checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3" dependencies = [ "heck", "proc-macro-error", @@ -864,7 +864,7 @@ dependencies = [ [[package]] name = "orly" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "askama", @@ -887,9 +887,12 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "3.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d" +checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799" +dependencies = [ + "memchr", +] [[package]] name = "parking_lot" @@ -1587,6 +1590,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.5" @@ -1641,12 +1653,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.3" diff --git a/Cargo.toml b/Cargo.toml index e25537e..67c6cfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orly" -version = "0.1.0" +version = "0.1.1" edition = "2018" authors = ["hurlenko"] description = "Download O'Reilly books as EPUB" @@ -31,7 +31,7 @@ askama = "0.10.5" bytes = "1.0.1" zip = "0.5.13" lazy_static = "1.4.0" -clap = "3.0.0-beta.4" +clap = "3.0.0-beta.5" sanitize-filename = "0.3.0" log = "0.4.14" fern = { version="0.6.0", features=["colored"] } diff --git a/README.md b/README.md index d0755c6..823e956 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,12 @@ After installation, the `orly` command will be available. Check the [command lin - Find the book you want to download and copy its id (the digits at the end of the url). -- Use your credentials to download the book: +- Use your credentials or a cookie string to download the book: ```bash - orly --creds "email@example.com" "password" 1234567890 + orly 1234567890 --creds "email@example.com" "password" + # or + orly 1234567890 --cookie 'BrowserCookie=....' ``` ## Command line interface @@ -101,6 +103,7 @@ FLAGS: OPTIONS: -c, --creds Sign in credentials + --cookie Cookie string -o, --output Directory to save the final epub to [default: .] -t, --threads Sets the maximum number of concurrent http requests [default: 20] ``` diff --git a/src/client.rs b/src/client.rs index 1822f13..c668d21 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,9 +8,10 @@ use anyhow::Context; use log::{error, info, trace}; use reqwest::{ header::{ - HeaderMap, HeaderValue, ACCEPT, ACCEPT_ENCODING, UPGRADE_INSECURE_REQUESTS, USER_AGENT, + HeaderMap, HeaderValue, ACCEPT, ACCEPT_ENCODING, COOKIE, UPGRADE_INSECURE_REQUESTS, + USER_AGENT, }, - Client, Url, + Client, ClientBuilder, Url, }; use crate::{ @@ -49,17 +50,8 @@ impl OreillyClient { impl Default for OreillyClient { fn default() -> Self { - let mut headers = HeaderMap::new(); - headers.insert(ACCEPT, HeaderValue::from_static("application/json,text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")); - headers.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip, deflate")); - headers.insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1")); - headers.insert(USER_AGENT, HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36")); Self { - client: reqwest::Client::builder() - .default_headers(headers) - .cookie_store(true) - .build() - .expect("to build the client"), + client: Self::default_client().build().expect("to build the client"), base_url: "https://learning.oreilly.com" .parse() .expect("correct base url"), @@ -77,10 +69,20 @@ impl OreillyClient { } } - async fn check_subscription(&self) -> Result<()> { + fn default_client() -> ClientBuilder { + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("application/json,text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")); + headers.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip, deflate")); + headers.insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1")); + headers.insert(USER_AGENT, HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36")); + reqwest::Client::builder() + .default_headers(headers) + .cookie_store(true) + } + + async fn check_subscription(&self, client: &Client) -> Result<()> { info!("Validating subscription"); - let response = self - .client + let response = client .get(self.make_url("api/v1/payments/next_billing_date/")?) .send() .await?; @@ -139,7 +141,7 @@ impl OreillyClient { )); } - self.check_subscription().await?; + self.check_subscription(&self.client).await?; Ok(OreillyClient { client: self.client, @@ -148,6 +150,28 @@ impl OreillyClient { marker: std::marker::PhantomData, }) } + + pub async fn cookie_auth(self, cookie: &str) -> Result> { + info!("Logging into Safari Books Online usig cookies..."); + + let mut request_headers = HeaderMap::new(); + request_headers.insert( + COOKIE, + HeaderValue::from_str(cookie).context("Invalid cookie")?, + ); + + let client = Self::default_client() + .default_headers(request_headers) + .build()?; + self.check_subscription(&client).await?; + + Ok(OreillyClient { + client, + base_url: self.base_url, + concurrent_requests: self.concurrent_requests, + marker: std::marker::PhantomData, + }) + } } impl OreillyClient { diff --git a/src/main.rs b/src/main.rs index 487c05e..3fd0a6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use clap::{Clap, ValueHint}; +use clap::{Parser, ValueHint}; use fern::colors::{Color, ColoredLevelConfig}; use log::{error, info}; use orly::{client::OreillyClient, epub::builder::EpubBuilder, error::Result, models::Book}; @@ -15,7 +15,7 @@ fn path_exists(v: &str) -> std::result::Result<(), String> { Err(format!("The specifiied path does not exist: {}", v)) } -#[derive(Clap, Debug)] +#[derive(Parser, Debug)] #[clap(author, about, version)] struct CliArgs { #[clap( @@ -23,10 +23,18 @@ struct CliArgs { long, value_name = "EMAIL PASSWORD", about = "Sign in credentials", - required = true, + required_unless_present = "cookie", + conflicts_with = "cookie", number_of_values = 2 )] - creds: Vec, + creds: Option>, + #[clap( + long, + value_name = "COOKIE_STRING", + about = "Cookie string", + required_unless_present = "creds" + )] + cookie: Option, #[clap( short, long, @@ -80,13 +88,14 @@ fn generate_filename(book: &Book) -> String { } async fn run(cli_args: &CliArgs) -> Result<()> { - let email = &cli_args.creds[0]; - let password = &cli_args.creds[1]; let book_id = &cli_args.book_id; - let client = OreillyClient::new(cli_args.threads) - .cred_auth(email, password) - .await?; + let client = OreillyClient::new(cli_args.threads); + let client = if let Some(creds) = &cli_args.creds { + client.cred_auth(&creds[0], &creds[1]).await? + } else { + client.cookie_auth(cli_args.cookie.as_ref().unwrap()).await? + }; info!("Getting book info"); let book = client.fetch_book_details(book_id).await?;