My personal site cherry.computer
htmx tailwind axum askama

perf: only get Apple Music environment variables on startup

cherry.computer ad595938 8f4535e8

verified
+67 -35
+3 -3
server/src/am_auth_flow.rs
··· 1 1 use askama::Template; 2 2 3 - use crate::scrapers::apple_music; 3 + use crate::scrapers::apple_music::AppleMusicClient; 4 4 5 5 #[derive(Template, Debug, Clone)] 6 6 #[template(path = "am-auth-flow.html")] ··· 9 9 } 10 10 11 11 impl AuthFlowTemplate { 12 - pub fn new() -> anyhow::Result<Self> { 13 - let token = apple_music::build_developer_token()?; 12 + pub fn new(apple_music_client: &AppleMusicClient) -> anyhow::Result<Self> { 13 + let token = apple_music_client.build_developer_token()?; 14 14 Ok(Self { token }) 15 15 } 16 16 }
+3 -3
server/src/index.rs
··· 1 1 use crate::scrapers::{ 2 - apple_music::{self, AppleMusic}, 2 + apple_music::{self, AppleMusic, AppleMusicClient}, 3 3 backloggd::{self, Backloggd}, 4 4 letterboxd::{self, Letterboxd}, 5 5 }; ··· 15 15 } 16 16 17 17 impl RootTemplate { 18 - pub async fn new() -> RootTemplate { 18 + pub async fn new(apple_music_client: &AppleMusicClient) -> RootTemplate { 19 19 let (game, movie, song) = tokio::join!( 20 20 backloggd::cached_fetch(), 21 21 letterboxd::cached_fetch(), 22 - apple_music::cached_fetch() 22 + apple_music::cached_fetch(apple_music_client) 23 23 ); 24 24 RootTemplate { game, movie, song } 25 25 }
+16 -5
server/src/main.rs
··· 3 3 mod index; 4 4 mod scrapers; 5 5 6 - use std::net::SocketAddr; 6 + use std::{net::SocketAddr, sync::Arc}; 7 7 8 8 #[cfg(debug_assertions)] 9 9 use crate::am_auth_flow::AuthFlowTemplate; 10 10 use crate::index::RootTemplate; 11 + use crate::scrapers::apple_music::AppleMusicClient; 11 12 12 13 use askama::Template; 13 14 use axum::{ 14 15 Router, 16 + extract::State, 15 17 http::{HeaderName, HeaderValue, StatusCode}, 16 18 response::{Html, IntoResponse}, 17 19 routing::{get, get_service}, ··· 22 24 trace::TraceLayer, 23 25 }; 24 26 27 + #[derive(Clone)] 28 + struct AppState { 29 + apple_music_client: Arc<AppleMusicClient>, 30 + } 31 + 25 32 #[tokio::main] 26 33 async fn main() -> anyhow::Result<()> { 27 34 tracing_subscriber::fmt::init(); 28 35 36 + let apple_music_client = Arc::new(AppleMusicClient::new()?); 37 + let state = AppState { apple_music_client }; 38 + 29 39 let app = Router::new() 30 40 .route("/", get(render_index_handler)) 31 41 .route("/dev/am-auth-flow", get(render_apple_music_auth_flow)) 32 42 .fallback(get_service(ServeDir::new("."))) 43 + .with_state(state) 33 44 .layer( 34 45 ServiceBuilder::new() 35 46 .layer(TraceLayer::new_for_http()) ··· 48 59 Ok(()) 49 60 } 50 61 51 - async fn render_index_handler() -> impl IntoResponse { 52 - let template = RootTemplate::new().await; 62 + async fn render_index_handler(State(state): State<AppState>) -> impl IntoResponse { 63 + let template = RootTemplate::new(&state.apple_music_client).await; 53 64 template.render().map(Html).map_err(|err| { 54 65 tracing::error!("failed to render index: {err:?}"); 55 66 StatusCode::INTERNAL_SERVER_ERROR 56 67 }) 57 68 } 58 69 59 - async fn render_apple_music_auth_flow() -> impl IntoResponse { 70 + async fn render_apple_music_auth_flow(State(state): State<AppState>) -> impl IntoResponse { 60 71 #[cfg(not(debug_assertions))] 61 72 return StatusCode::NOT_FOUND; 62 73 63 74 #[cfg(debug_assertions)] 64 75 { 65 - let template = AuthFlowTemplate::new(); 76 + let template = AuthFlowTemplate::new(&state.apple_music_client); 66 77 template 67 78 .and_then(|template| Ok(template.render()?)) 68 79 .map(Html)
+45 -24
server/src/scrapers/apple_music.rs
··· 51 51 url: String, 52 52 } 53 53 54 - impl AppleMusic { 55 - pub async fn fetch() -> anyhow::Result<Self> { 56 - let jwt = build_developer_token()?; 54 + pub struct AppleMusicClient { 55 + http_client: Client, 56 + key_id: String, 57 + team_id: String, 58 + key: EncodingKey, 59 + user_token: String, 60 + } 61 + 62 + impl AppleMusicClient { 63 + pub fn new() -> anyhow::Result<Self> { 64 + let key_id = 65 + env::var("APPLE_DEVELOPER_TOKEN_KEY_ID").context("missing apple developer key ID")?; 66 + let team_id = 67 + env::var("APPLE_DEVELOPER_TOKEN_TEAM_ID").context("missing apple developer team ID")?; 68 + let auth_key = env::var("APPLE_DEVELOPER_TOKEN_AUTH_KEY") 69 + .context("missing apple developer auth key")?; 70 + let key = EncodingKey::from_ec_pem(auth_key.as_bytes()) 71 + .context("failed to parse appple developer auth key")?; 57 72 let user_token = env::var("APPLE_USER_TOKEN").context("missing apple user token")?; 58 73 59 - let client = Client::new(); 60 - let response: AppleMusicResponse = client 74 + Ok(Self { 75 + http_client: Client::new(), 76 + key_id, 77 + team_id, 78 + key, 79 + user_token, 80 + }) 81 + } 82 + 83 + pub async fn fetch(&self) -> anyhow::Result<AppleMusic> { 84 + let jwt = self.build_developer_token()?; 85 + 86 + let response: AppleMusicResponse = self 87 + .http_client 61 88 .get("https://api.music.apple.com/v1/me/recent/played/tracks") 62 89 .bearer_auth(jwt) 63 - .header("Music-User-Token", user_token) 90 + .header("Music-User-Token", &self.user_token) 64 91 .query(&[("types", "songs"), ("limit", "1")]) 65 92 .send() 66 93 .await ··· 76 103 .replace("{w}", dimensions) 77 104 .replace("{h}", dimensions); 78 105 79 - Ok(Self { 106 + Ok(AppleMusic { 80 107 name: track.attributes.name.clone(), 81 108 art: artwork_url, 82 109 }) 83 110 } 111 + 112 + pub fn build_developer_token(&self) -> anyhow::Result<String> { 113 + let mut header = Header::new(Algorithm::ES256); 114 + header.kid = Some(self.key_id.clone()); 115 + let claims = Claims::new(self.team_id.clone()); 116 + 117 + jsonwebtoken::encode(&header, &claims, &self.key) 118 + .context("failed to encode apple developer JWT") 119 + } 84 120 } 85 121 86 122 #[once(time = 30, option = false)] 87 - pub async fn cached_fetch() -> Option<AppleMusic> { 88 - AppleMusic::fetch() 123 + pub async fn cached_fetch(this: &AppleMusicClient) -> Option<AppleMusic> { 124 + this.fetch() 89 125 .await 90 126 .map_err(|error| tracing::warn!(?error, "failed to call Apple Music")) 91 127 .ok() 92 128 } 93 - 94 - pub fn build_developer_token() -> anyhow::Result<String> { 95 - let mut header = Header::new(Algorithm::ES256); 96 - header.kid = 97 - Some(env::var("APPLE_DEVELOPER_TOKEN_KEY_ID").context("missing apple developer key ID")?); 98 - let team_id = 99 - env::var("APPLE_DEVELOPER_TOKEN_TEAM_ID").context("missing apple developer team ID")?; 100 - let claims = Claims::new(team_id); 101 - let auth_key = 102 - env::var("APPLE_DEVELOPER_TOKEN_AUTH_KEY").context("missing apple developer auth key")?; 103 - let key = EncodingKey::from_ec_pem(auth_key.as_bytes()) 104 - .context("failed to parse appple developer auth key")?; 105 - 106 - jsonwebtoken::encode(&header, &claims, &key).context("failed to encode apple developer JWT") 107 - }