My personal site cherry.computer
htmx tailwind axum askama

feat: add context field to media

cherry.computer 3f881d6b 83fabd7e

verified
+58 -5
+1
server/src/scrapers.rs
··· 6 6 pub struct Media { 7 7 pub name: String, 8 8 pub image: String, 9 + pub context: String, 9 10 }
+8
server/src/scrapers/apple_music.rs
··· 37 37 } 38 38 39 39 #[derive(Deserialize, Debug, Clone)] 40 + #[serde(rename_all = "camelCase")] 40 41 struct AppleMusicTrackAttributes { 41 42 name: String, 43 + album_name: String, 44 + artist_name: String, 42 45 artwork: AppleMusicTrackArtwork, 43 46 } 44 47 ··· 98 101 .replace("{w}", dimensions) 99 102 .replace("{h}", dimensions); 100 103 104 + let artist = &track.attributes.artist_name; 105 + let album = &track.attributes.album_name; 106 + let context = format!("{artist} — {album}"); 107 + 101 108 Ok(Media { 102 109 name: track.attributes.name.clone(), 103 110 image: artwork_url, 111 + context, 104 112 }) 105 113 } 106 114
+15 -1
server/src/scrapers/backloggd.rs
··· 12 12 static NAME_SEL: LazyLock<Selector> = 13 13 LazyLock::new(|| Selector::parse(".game-name a").unwrap()); 14 14 static IMAGE_SEL: LazyLock<Selector> = LazyLock::new(|| Selector::parse(".card-img").unwrap()); 15 + static PLATFORM_SEL: LazyLock<Selector> = 16 + LazyLock::new(|| Selector::parse(".journal-platform").unwrap()); 15 17 16 18 let html = reqwest::get("https://backloggd.com/u/cherryfunk/journal") 17 19 .await ··· 40 42 .attr("src") 41 43 .context("image element didn't have src attribute")? 42 44 .to_owned(); 45 + let platform = first_entry 46 + .select(&PLATFORM_SEL) 47 + .next() 48 + .context("couldn't find platform element")? 49 + .text() 50 + .next() 51 + .context("platform element didn't have any text")? 52 + .to_owned(); 43 53 44 - Ok(Media { name, image }) 54 + Ok(Media { 55 + name, 56 + image, 57 + context: platform, 58 + }) 45 59 } 46 60 47 61 #[once(time = 300, option = false)]
+30 -3
server/src/scrapers/letterboxd.rs
··· 16 16 struct Extracted { 17 17 name: String, 18 18 image_url: Url, 19 + rating: Option<u8>, 19 20 } 20 21 21 22 pub async fn fetch() -> anyhow::Result<Media> { ··· 28 29 .text() 29 30 .await 30 31 .context("failed to get HTML text")?; 31 - let Extracted { name, image_url } = parse_html(&html)?; 32 + let Extracted { 33 + name, 34 + image_url, 35 + rating, 36 + } = parse_html(&html)?; 32 37 33 38 let image_url_data: ImageUrlMetadata = client 34 39 .get(image_url.clone()) 35 40 .send() 36 41 .await 37 - .with_context(|| format!("failed to fetch image metadata from URL {}", image_url))? 42 + .with_context(|| format!("failed to fetch image metadata from URL {image_url}"))? 38 43 .json() 39 44 .await 40 45 .context("failed to parse image metadata")?; 46 + let formatted_rating = match rating { 47 + Some(rating) => format!( 48 + "{} {}", 49 + f32::from(rating) / 2.0, 50 + if rating == 2 { "star" } else { "stars" } 51 + ), 52 + None => "no rating".to_owned(), 53 + }; 41 54 42 55 Ok(Media { 43 56 name, 44 57 image: image_url_data.url, 58 + context: formatted_rating, 45 59 }) 46 60 } 47 61 ··· 51 65 static NAME_SEL: LazyLock<Selector> = LazyLock::new(|| Selector::parse(".name").unwrap()); 52 66 static POSTER_COMPONENT_SEL: LazyLock<Selector> = 53 67 LazyLock::new(|| Selector::parse(".react-component:has(> .poster)").unwrap()); 68 + static RATING_SEL: LazyLock<Selector> = LazyLock::new(|| Selector::parse(".rating").unwrap()); 54 69 55 70 let document = Html::parse_document(html); 56 71 ··· 70 85 .select(&POSTER_COMPONENT_SEL) 71 86 .next() 72 87 .context("couldn't find post component")?; 88 + let rating = first_entry 89 + .select(&RATING_SEL) 90 + .next() 91 + .context("couldn't find rating component")? 92 + .value() 93 + .classes() 94 + .find_map(|class| class.strip_prefix("rated-")) 95 + .and_then(|rating| rating.parse().ok()); 73 96 74 97 let image_url = build_image_url(poster_component)?; 75 98 76 - Ok(Extracted { name, image_url }) 99 + Ok(Extracted { 100 + name, 101 + image_url, 102 + rating, 103 + }) 77 104 } 78 105 79 106 fn build_image_url(poster_component: ElementRef) -> anyhow::Result<Url> {
+4 -1
server/templates/index.html
··· 25 25 src="{{ media.image }}" 26 26 alt="Cover art for {{ media.name }}" 27 27 /> 28 - <p class="self-center text-2xl text-white">{{ media.name }}</p> 28 + <div class="flex flex-col self-center"> 29 + <p class="text-2xl text-white">{{ media.name }}</p> 30 + <p class="text-xl text-gray-700 italic">{{ media.context }}</p> 31 + </div> 29 32 </div> 30 33 {%- endfor %} 31 34 </div>