letterboxd appears to now send multiple csrf set-cookie headers one of which is empty. this means that we are now required to check both of these to make sure we grab the correct one.
130 lines
3.6 KiB
Rust
130 lines
3.6 KiB
Rust
use std::fs;
|
|
use std::path;
|
|
use std::io::Read;
|
|
|
|
use anyhow::Context;
|
|
|
|
#[derive(clap::Parser)]
|
|
struct Args {
|
|
#[clap(long, short)]
|
|
username : Option<String>,
|
|
#[clap(long, short)]
|
|
password : Option<String>,
|
|
#[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<String>,
|
|
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::<LoginResult>(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::<usize>(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(())
|
|
}
|
|
|