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