My personal site
cherry.computer
htmx
tailwind
axum
askama
1mod scrapers;
2mod templates;
3
4use std::{env, net::SocketAddr, sync::Arc};
5
6use crate::scrapers::{MediaType, apple_music::AppleMusicClient};
7#[cfg(debug_assertions)]
8use crate::templates::am_auth_flow::AuthFlowTemplate;
9use crate::templates::{
10 index::{IndexOptions, RootTemplate, Shas},
11 media::{MediaTemplate, fetch_media_of_type},
12};
13
14use askama::Template;
15use axum::{
16 Router,
17 extract::{Path, Query, State},
18 http::{HeaderName, HeaderValue, StatusCode},
19 response::{Html, IntoResponse},
20 routing::get,
21};
22use tower::ServiceBuilder;
23use tower_http::{
24 compression::CompressionLayer, set_header::SetResponseHeaderLayer, trace::TraceLayer,
25};
26
27#[derive(Clone)]
28struct AppState {
29 apple_music_client: Arc<AppleMusicClient>,
30 shas: Shas,
31}
32
33#[tokio::main]
34async fn main() -> anyhow::Result<()> {
35 tracing_subscriber::fmt::init();
36
37 let apple_music_client = Arc::new(AppleMusicClient::new()?);
38 let shas = Shas {
39 website: env::var("MYIVO_GIT_SHA").ok(),
40 };
41 let state = AppState {
42 apple_music_client,
43 shas,
44 };
45
46 let app = Router::new()
47 .route("/", get(render_index_handler))
48 .route("/media/{media_type}", get(render_media_partial_handler));
49 #[cfg(debug_assertions)]
50 let app = app.route("/dev/am-auth-flow", get(render_apple_music_auth_flow));
51 let app = app.with_state(state).layer(
52 ServiceBuilder::new()
53 .layer(TraceLayer::new_for_http())
54 .layer(CompressionLayer::new())
55 .layer(SetResponseHeaderLayer::overriding(
56 HeaderName::from_static("strict-transport-security"),
57 HeaderValue::from_static("max-age=2592000; includeSubDomains"),
58 )),
59 );
60
61 let addr = SocketAddr::from(([0, 0, 0, 0], 53465));
62 tracing::debug!("starting server on {addr}");
63 let listener = tokio::net::TcpListener::bind(addr).await?;
64 axum::serve(listener, app).await?;
65
66 Ok(())
67}
68
69async fn render_index_handler(
70 Query(options): Query<IndexOptions>,
71 State(state): State<AppState>,
72) -> impl IntoResponse {
73 let template = RootTemplate::new(state.apple_music_client, state.shas, &options);
74 template.render().map(Html).map_err(|err| {
75 tracing::error!("failed to render index: {err:?}");
76 StatusCode::INTERNAL_SERVER_ERROR
77 })
78}
79
80async fn render_media_partial_handler(
81 Path(media_type): Path<MediaType>,
82 State(state): State<AppState>,
83) -> impl IntoResponse {
84 let media = fetch_media_of_type(media_type, state.apple_music_client)
85 .await
86 .unwrap();
87 let template = MediaTemplate { media_type, media };
88 template.render().map(Html).map_err(|err| {
89 tracing::error!("failed to render {media_type} media: {err:?}");
90 StatusCode::INTERNAL_SERVER_ERROR
91 })
92}
93
94#[cfg(debug_assertions)]
95async fn render_apple_music_auth_flow(
96 #[allow(unused_variables)] State(state): State<AppState>,
97) -> impl IntoResponse {
98 let template = AuthFlowTemplate::new(&state.apple_music_client);
99 template
100 .and_then(|template| Ok(template.render()?))
101 .map(Html)
102 .map_err(|err| {
103 tracing::error!("failed to render Apple Music auth flow: {err:?}");
104 StatusCode::INTERNAL_SERVER_ERROR
105 })
106}