Files
website/tools/letterboxd-sync/src/main.rs
T
2026-09-07 00:47:32 +02:00

70 lines
1.7 KiB
Rust

use reqwest::blocking::Client;
use scraper::{Html, Selector};
use serde::{Deserialize, Serialize};
use std::fs;
const BASE: &str = "https://letterboxd.com";
const LIST: &str = "https://letterboxd.com/mimuuu/list/2026/by/reverse/";
#[derive(Deserialize)]
struct FilmJson {
name: String,
released: Released,
directors: Vec<Person>,
}
#[derive(Deserialize)]
struct Released {
year: u16,
}
#[derive(Deserialize)]
struct Person {
name: String,
}
#[derive(Serialize)]
struct Film {
title: String,
year: u16,
director: String,
url: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::builder().user_agent("Mozilla/5.0").build()?;
// One request: list page (only to discover the 5 film endpoints)
let html = client.get(LIST).send()?.text()?;
let doc = Html::parse_document(&html);
let selector = Selector::parse("li.posteritem div.react-component")?;
let mut films = Vec::new();
for node in doc.select(&selector).take(5) {
let details = node.value().attr("data-details-endpoint").unwrap();
let slug = node.value().attr("data-item-link").unwrap();
// One request per film: JSON metadata
let film: FilmJson = client.get(format!("{BASE}{details}")).send()?.json()?;
films.push(Film {
title: film.name,
year: film.released.year,
director: film
.directors
.first()
.map(|d| d.name.clone())
.unwrap_or_default(),
url: format!("{BASE}{slug}"),
});
}
fs::create_dir_all("data")?;
fs::write("data/films.json", serde_json::to_string_pretty(&films)?)?;
println!("Saved {} films", films.len());
Ok(())
}