My personal site cherry.computer
htmx tailwind axum askama

refactor: split scrobble parsing/templating into separate file

Let's not overthink it rn but it would be nice if the ScrobbleMonitor
struct could be more generic and support fetching arbitrary types in the
future.

cherry.computer d640ae17 20a0f9e6

verified
+80 -76
+1
server/src/main.rs
··· 1 + mod scrobble; 1 2 mod scrobble_monitor; 2 3 3 4 use std::{env, net::SocketAddr};
+75
server/src/scrobble.rs
··· 1 + use maud::{html, Markup}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, Clone, Deserialize)] 5 + pub struct ScrobbleArtist { 6 + #[serde(rename = "#text")] 7 + pub text: String, 8 + } 9 + 10 + #[derive(Debug, Clone, Deserialize)] 11 + pub struct ScrobbleImage { 12 + #[serde(rename = "#text")] 13 + pub text: String, 14 + } 15 + 16 + #[derive(Debug, Clone, Deserialize)] 17 + pub struct ScrobbleAttributes { 18 + #[serde(rename = "nowplaying")] 19 + pub now_playing: bool, 20 + } 21 + 22 + #[derive(Debug, Clone, Deserialize)] 23 + pub struct ScrobbleTrack { 24 + pub artist: ScrobbleArtist, 25 + pub image: Vec<ScrobbleImage>, 26 + pub name: String, 27 + #[serde(rename = "@attr")] 28 + pub attributes: Option<ScrobbleAttributes>, 29 + } 30 + 31 + #[derive(Debug, Clone, Deserialize)] 32 + pub struct ScrobbleRecentTracks { 33 + pub track: Vec<ScrobbleTrack>, 34 + } 35 + 36 + #[derive(Debug, Clone, Deserialize)] 37 + pub struct Scrobble { 38 + #[serde(rename = "recenttracks")] 39 + pub recent_tracks: ScrobbleRecentTracks, 40 + } 41 + 42 + pub fn scrobble_partial(scrobble: &Scrobble) -> Markup { 43 + let latest_track = &scrobble.recent_tracks.track[0]; 44 + let srcset = format!( 45 + "{}, {} 2x, {} 3x", 46 + latest_track.image[0].text, latest_track.image[1].text, latest_track.image[2].text 47 + ); 48 + let text_intro = if latest_track 49 + .attributes 50 + .as_ref() 51 + .map_or(false, |attr| attr.now_playing) 52 + { 53 + "Now playing: " 54 + } else { 55 + "Last played: " 56 + }; 57 + let now_playing = format!("{} - {}", latest_track.name, latest_track.artist.text); 58 + 59 + html! { 60 + .bar-container { 61 + img .bar-cover 62 + src=(latest_track.image[0].text) 63 + alt="Cover art" 64 + srcset=(srcset); 65 + 66 + p .bar-text-intro { 67 + (text_intro) 68 + } 69 + 70 + p .bar-text-music { 71 + (now_playing) 72 + } 73 + } 74 + } 75 + }
+4 -76
server/src/scrobble_monitor.rs
··· 3 3 time::{Duration, Instant}, 4 4 }; 5 5 6 - use maud::{html, Markup}; 6 + use maud::Markup; 7 7 use reqwest::Client; 8 - use serde::Deserialize; 9 8 use tokio::sync::RwLock; 9 + 10 + use crate::scrobble::{scrobble_partial, Scrobble}; 10 11 11 12 #[derive(Debug, Clone)] 12 13 struct CachedScrobble { ··· 14 15 fetch_time: Instant, 15 16 } 16 17 17 - #[derive(Debug, Clone, Deserialize)] 18 - struct ScrobbleArtist { 19 - #[serde(rename = "#text")] 20 - text: String, 21 - } 22 - 23 - #[derive(Debug, Clone, Deserialize)] 24 - struct ScrobbleImage { 25 - #[serde(rename = "#text")] 26 - text: String, 27 - } 28 - 29 - #[derive(Debug, Clone, Deserialize)] 30 - struct ScrobbleAttributes { 31 - #[serde(rename = "nowplaying")] 32 - now_playing: bool, 33 - } 34 - 35 - #[derive(Debug, Clone, Deserialize)] 36 - struct ScrobbleTrack { 37 - artist: ScrobbleArtist, 38 - image: Vec<ScrobbleImage>, 39 - name: String, 40 - #[serde(rename = "@attr")] 41 - attributes: Option<ScrobbleAttributes>, 42 - } 43 - 44 - #[derive(Debug, Clone, Deserialize)] 45 - struct ScrobbleRecentTracks { 46 - track: Vec<ScrobbleTrack>, 47 - } 48 - 49 - #[derive(Debug, Clone, Deserialize)] 50 - struct Scrobble { 51 - #[serde(rename = "recenttracks")] 52 - recent_tracks: ScrobbleRecentTracks, 53 - } 54 - 55 18 #[derive(Debug, Clone)] 56 19 pub struct ScrobbleMonitor { 57 20 client: Client, ··· 89 52 _ => { 90 53 tracing::debug!("fetching new scrobble data"); 91 54 let scrobble = self.fetch_scrobble().await?; 92 - let scrobble_partial = self.scrobble_partial(&scrobble); 55 + let scrobble_partial = scrobble_partial(&scrobble); 93 56 *last_scrobble = Some(CachedScrobble { 94 57 data: scrobble_partial.clone(), 95 58 fetch_time: Instant::now(), ··· 113 76 .send() 114 77 .await?; 115 78 Ok(response.json().await?) 116 - } 117 - 118 - fn scrobble_partial(&self, scrobble: &Scrobble) -> Markup { 119 - let latest_track = &scrobble.recent_tracks.track[0]; 120 - let srcset = format!( 121 - "{}, {} 2x, {} 3x", 122 - latest_track.image[0].text, latest_track.image[1].text, latest_track.image[2].text 123 - ); 124 - let text_intro = if latest_track 125 - .attributes 126 - .as_ref() 127 - .map_or(false, |attr| attr.now_playing) 128 - { 129 - "Now playing: " 130 - } else { 131 - "Last played: " 132 - }; 133 - let now_playing = format!("{} - {}", latest_track.name, latest_track.artist.text); 134 - 135 - html! { 136 - .bar-container { 137 - img .bar-cover 138 - src=(latest_track.image[0].text) 139 - alt="Cover art" 140 - srcset=(srcset); 141 - 142 - p .bar-text-intro { 143 - (text_intro) 144 - } 145 - 146 - p .bar-text-music { 147 - (now_playing) 148 - } 149 - } 150 - } 151 79 } 152 80 }