From c649e6809b846fda61cf9843e52b9ffcb06c518b Mon Sep 17 00:00:00 2001 From: mimu Date: Tue, 8 Sep 2026 16:17:25 +0200 Subject: [PATCH] add script to update latest movies --- .gitignore | 1 + Makefile | 2 + data/films.json | 34 +++---- tools/letterboxd-sync/src/main.rs | 94 ++++++++++++------- tools/letterboxd-sync/target/.rustc_info.json | 1 - tools/letterboxd-sync/target/CACHEDIR.TAG | 3 - 6 files changed, 79 insertions(+), 56 deletions(-) create mode 100644 Makefile delete mode 100644 tools/letterboxd-sync/target/.rustc_info.json delete mode 100644 tools/letterboxd-sync/target/CACHEDIR.TAG diff --git a/.gitignore b/.gitignore index b744346..48eddf8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ debug/ +tools/letterboxd-sync/target diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..91a53a6 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +films: + cargo run --manifest-path tools/letterboxd-sync/Cargo.toml diff --git a/data/films.json b/data/films.json index 8014fca..d3c8fff 100644 --- a/data/films.json +++ b/data/films.json @@ -1,32 +1,32 @@ [ { - "title": "The Death of Robin Hood", - "director": "Michael Sarnoski", + "title": "De la Comédie-Française", "year": "2026", + "director": "Bertrand Usclat, Martin Darondeau", + "url": "https://letterboxd.com/film/comedie-francaise/" + }, + { + "title": "The Dog Stars", + "year": "2026", + "director": "Ridley Scott", + "url": "https://letterboxd.com/film/the-dog-stars/" + }, + { + "title": "The Death of Robin Hood", + "year": "2026", + "director": "Michael Sarnoski", "url": "https://letterboxd.com/film/the-death-of-robin-hood/" }, { "title": "Tully", - "director": "Jason Reitman", "year": "2018", + "director": "Jason Reitman", "url": "https://letterboxd.com/film/tully-2018/" }, { "title": "Zoolander", - "director": "Ben Stiller", "year": "2001", + "director": "Ben Stiller", "url": "https://letterboxd.com/film/zoolander/" - }, - { - "title": "One Night Only", - "director": "Will Gluck", - "year": "2026", - "url": "https://letterboxd.com/film/one-night-only-2026/" - }, - { - "title": "Mutiny", - "director": "Jean-François Richet", - "year": "2026", - "url": "https://letterboxd.com/film/mutiny-2026/" } -] +] \ No newline at end of file diff --git a/tools/letterboxd-sync/src/main.rs b/tools/letterboxd-sync/src/main.rs index 2dff1de..8f42ef2 100644 --- a/tools/letterboxd-sync/src/main.rs +++ b/tools/letterboxd-sync/src/main.rs @@ -1,32 +1,15 @@ use reqwest::blocking::Client; use scraper::{Html, Selector}; -use serde::{Deserialize, Serialize}; -use std::fs; +use serde::Serialize; +use std::{collections::HashSet, 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, -} - -#[derive(Deserialize)] -struct Released { - year: u16, -} - -#[derive(Deserialize)] -struct Person { - name: String, -} - #[derive(Serialize)] struct Film { title: String, - year: u16, + year: String, director: String, url: String, } @@ -34,30 +17,70 @@ struct Film { fn main() -> Result<(), Box> { let client = Client::builder().user_agent("Mozilla/5.0").build()?; - // One request: list page (only to discover the 5 film endpoints) + // Get latest 5 film URLs let html = client.get(LIST).send()?.text()?; let doc = Html::parse_document(&html); - let selector = Selector::parse("li.posteritem div.react-component")?; + + let poster_sel = Selector::parse("li.posteritem div.react-component")?; + + let mut urls = Vec::new(); + let mut seen = HashSet::new(); + + for node in doc.select(&poster_sel) { + if let Some(path) = node.value().attr("data-item-link") { + let url = format!("{BASE}{path}"); + if seen.insert(url.clone()) { + urls.push(url); + } + } + if urls.len() == 5 { + break; + } + } let mut films = Vec::new(); + let original_title_sel = Selector::parse(".productioninfo .quoted-creative-work-title")?; + let title_sel = Selector::parse(".details .primaryname .name")?; + let year_sel = Selector::parse(".productioninfo .releasedate a")?; + let directors_sel = Selector::parse(".credits .contributorlist")?; + let a_sel = Selector::parse("a")?; - for node in doc.select(&selector).take(5) { - let details = node.value().attr("data-details-endpoint").unwrap(); + for url in urls { + let html = client.get(&url).send()?.text()?; + let doc = Html::parse_document(&html); - let slug = node.value().attr("data-item-link").unwrap(); + let mut title = doc + .select(&original_title_sel) + .next() + .map(|node| node.text().collect::().trim().to_string()) + .unwrap_or_default(); - // One request per film: JSON metadata - let film: FilmJson = client.get(format!("{BASE}{details}")).send()?.json()?; + if title.is_empty() { + title = doc + .select(&title_sel) + .next() + .map(|node| node.text().collect::().trim().to_string()) + .unwrap_or_default(); + } + + let year = doc + .select(&year_sel) + .next() + .map(|node| node.text().collect::().trim().to_string()) + .unwrap_or_default(); + + let director = doc + .select(&directors_sel) + .flat_map(|node| node.select(&a_sel)) + .map(|a| a.text().collect::().trim().to_string()) + .collect::>() + .join(", "); 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}"), + title, + year, + director, + url, }); } @@ -65,5 +88,6 @@ fn main() -> Result<(), Box> { fs::write("data/films.json", serde_json::to_string_pretty(&films)?)?; println!("Saved {} films", films.len()); + Ok(()) } diff --git a/tools/letterboxd-sync/target/.rustc_info.json b/tools/letterboxd-sync/target/.rustc_info.json deleted file mode 100644 index 209e01e..0000000 --- a/tools/letterboxd-sync/target/.rustc_info.json +++ /dev/null @@ -1 +0,0 @@ -{"rustc_fingerprint":13028192105755907156,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/mimu/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.1 (e408947bf 2026-03-25)\nbinary: rustc\ncommit-hash: e408947bfd200af42db322daf0fadfe7e26d3bd1\ncommit-date: 2026-03-25\nhost: x86_64-unknown-linux-gnu\nrelease: 1.94.1\nLLVM version: 21.1.8\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/tools/letterboxd-sync/target/CACHEDIR.TAG b/tools/letterboxd-sync/target/CACHEDIR.TAG deleted file mode 100644 index 20d7c31..0000000 --- a/tools/letterboxd-sync/target/CACHEDIR.TAG +++ /dev/null @@ -1,3 +0,0 @@ -Signature: 8a477f597d28d172789f06886806bc55 -# This file is a cache directory tag created by cargo. -# For information about cache directory tags see https://bford.info/cachedir/