use std::fs; use std::path; use std::io::Read; use anyhow::Context; #[derive(clap::Parser)] struct Args { #[clap(long, short)] username : Option, #[clap(long, short)] password : Option, #[clap(long, short)] output : path::PathBuf, } #[derive(serde::Serialize)] struct Login<'a> { #[serde(rename = "__csrf")] csrf : &'a str, #[serde(rename = "authenticationCode")] authentication_code : &'a str, username : &'a str, password : &'a str, } #[derive(serde::Deserialize)] #[allow(dead_code)] struct LoginResult { result : String, messages : Vec, csrf : String, } fn main() -> anyhow::Result<()> { let args : Args = clap::Parser::parse(); let client = reqwest::blocking::ClientBuilder::new() // CSRF cookies and login cookies must be passed between requests for // authentication to succeed .cookie_store(true) .build()?; let home_response = client.get("https://letterboxd.com") .header("user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0") .header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") .send()?; let home_cookies = home_response.headers() .get_all("set-cookie"); let mut csrf = None; for cookie in home_cookies { if let Ok(cookie) = cookie.to_str() { let cookie = cookie::Cookie::parse(cookie).context("failed to parse cookie value")?; if cookie.name() == "com.xk72.webparts.csrf" && cookie.value() != "" { csrf = Some(cookie.value().to_owned()); } } } let login = Login { username : &match args.username { Some(username) => username, None => { dialoguer::Input::new() .with_prompt("Username") .interact_text() .unwrap() } }, password : &match args.password { Some(password) => password, None => { dialoguer::Password::new() .with_prompt("Password") .interact() .context("failed to read password")? } }, authentication_code : "", csrf: &csrf.context("csrf cookie was not found when fetching homepage")?, }; let login = serde_urlencoded::to_string(&login) .context("issue with login request serialization")?; let mut login_response = client.post("https://letterboxd.com/user/login.do") .body(login) .header("Content-Type", "application/x-www-form-urlencoded") .send()?; { let mut buffer = String::new(); login_response.read_to_string(&mut buffer)?; let login_response = serde_json::from_str::(buffer.as_str())?; if login_response.result != "success" { anyhow::bail!("login failed") } } let mut export_response = client.get("https://letterboxd.com/data/export/") .send()?; if export_response.status() != reqwest::StatusCode::OK { anyhow::bail!("non 200 status code returned from data export") } let mut zip = export_response.headers().get("Content-Length") .and_then(|val| { val.to_str().ok() }) .and_then(|val| { str::parse::(val).ok() }) .map(|len| { Vec::with_capacity(len) }).unwrap_or(Vec::new()); export_response.read_to_end(&mut zip)?; fs::write(args.output, zip)?; Ok(()) }