add script to update latest movies
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
debug/
|
debug/
|
||||||
|
tools/letterboxd-sync/target
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
films:
|
||||||
|
cargo run --manifest-path tools/letterboxd-sync/Cargo.toml
|
||||||
+16
-16
@@ -1,32 +1,32 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"title": "The Death of Robin Hood",
|
"title": "De la Comédie-Française",
|
||||||
"director": "Michael Sarnoski",
|
|
||||||
"year": "2026",
|
"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/"
|
"url": "https://letterboxd.com/film/the-death-of-robin-hood/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Tully",
|
"title": "Tully",
|
||||||
"director": "Jason Reitman",
|
|
||||||
"year": "2018",
|
"year": "2018",
|
||||||
|
"director": "Jason Reitman",
|
||||||
"url": "https://letterboxd.com/film/tully-2018/"
|
"url": "https://letterboxd.com/film/tully-2018/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Zoolander",
|
"title": "Zoolander",
|
||||||
"director": "Ben Stiller",
|
|
||||||
"year": "2001",
|
"year": "2001",
|
||||||
|
"director": "Ben Stiller",
|
||||||
"url": "https://letterboxd.com/film/zoolander/"
|
"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/"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,32 +1,15 @@
|
|||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use scraper::{Html, Selector};
|
use scraper::{Html, Selector};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Serialize;
|
||||||
use std::fs;
|
use std::{collections::HashSet, fs};
|
||||||
|
|
||||||
const BASE: &str = "https://letterboxd.com";
|
const BASE: &str = "https://letterboxd.com";
|
||||||
const LIST: &str = "https://letterboxd.com/mimuuu/list/2026/by/reverse/";
|
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)]
|
#[derive(Serialize)]
|
||||||
struct Film {
|
struct Film {
|
||||||
title: String,
|
title: String,
|
||||||
year: u16,
|
year: String,
|
||||||
director: String,
|
director: String,
|
||||||
url: String,
|
url: String,
|
||||||
}
|
}
|
||||||
@@ -34,30 +17,70 @@ struct Film {
|
|||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let client = Client::builder().user_agent("Mozilla/5.0").build()?;
|
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 html = client.get(LIST).send()?.text()?;
|
||||||
let doc = Html::parse_document(&html);
|
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 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) {
|
for url in urls {
|
||||||
let details = node.value().attr("data-details-endpoint").unwrap();
|
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::<String>().trim().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
// One request per film: JSON metadata
|
if title.is_empty() {
|
||||||
let film: FilmJson = client.get(format!("{BASE}{details}")).send()?.json()?;
|
title = doc
|
||||||
|
.select(&title_sel)
|
||||||
|
.next()
|
||||||
|
.map(|node| node.text().collect::<String>().trim().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let year = doc
|
||||||
|
.select(&year_sel)
|
||||||
|
.next()
|
||||||
|
.map(|node| node.text().collect::<String>().trim().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let director = doc
|
||||||
|
.select(&directors_sel)
|
||||||
|
.flat_map(|node| node.select(&a_sel))
|
||||||
|
.map(|a| a.text().collect::<String>().trim().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
films.push(Film {
|
films.push(Film {
|
||||||
title: film.name,
|
title,
|
||||||
year: film.released.year,
|
year,
|
||||||
director: film
|
director,
|
||||||
.directors
|
url,
|
||||||
.first()
|
|
||||||
.map(|d| d.name.clone())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
url: format!("{BASE}{slug}"),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,5 +88,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
fs::write("data/films.json", serde_json::to_string_pretty(&films)?)?;
|
fs::write("data/films.json", serde_json::to_string_pretty(&films)?)?;
|
||||||
|
|
||||||
println!("Saved {} films", films.len());
|
println!("Saved {} films", films.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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":{}}
|
|
||||||
@@ -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/
|
|
||||||
Reference in New Issue
Block a user