initial commit
This commit is contained in:
127
src/main.rs
Normal file
127
src/main.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
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")
|
||||
.send()?;
|
||||
|
||||
let home_cookies = home_response.headers()
|
||||
.get("set-cookie")
|
||||
.context("did not recieve csrf cookie when making home page request")?
|
||||
.to_str()
|
||||
.context("cookie value could not be read as string")?;
|
||||
|
||||
let cookie = cookie::Cookie::parse(home_cookies).context("failed to parse cookie value")?;
|
||||
let csrf = if cookie.name() == "com.xk72.webparts.csrf" {
|
||||
cookie.value()
|
||||
} else {
|
||||
anyhow::bail!("unexpected cookie from home page request")
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user