mod scrapers; mod templates; use std::{env, net::SocketAddr, sync::Arc}; use crate::scrapers::{MediaType, apple_music::AppleMusicClient}; #[cfg(debug_assertions)] use crate::templates::am_auth_flow::AuthFlowTemplate; use crate::templates::{ index::{IndexOptions, RootTemplate, Shas}, media::{MediaTemplate, fetch_media_of_type}, }; use askama::Template; use axum::{ Router, extract::{Path, Query, State}, http::{HeaderName, HeaderValue, StatusCode}, response::{Html, IntoResponse}, routing::get, }; use tower::ServiceBuilder; use tower_http::{ compression::CompressionLayer, set_header::SetResponseHeaderLayer, trace::TraceLayer, }; #[derive(Clone)] struct AppState { apple_music_client: Arc, shas: Shas, } #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let apple_music_client = Arc::new(AppleMusicClient::new()?); let shas = Shas { website: env::var("MYIVO_GIT_SHA").ok(), }; let state = AppState { apple_music_client, shas, }; let app = Router::new() .route("/", get(render_index_handler)) .route("/media/{media_type}", get(render_media_partial_handler)); #[cfg(debug_assertions)] let app = app.route("/dev/am-auth-flow", get(render_apple_music_auth_flow)); let app = app.with_state(state).layer( ServiceBuilder::new() .layer(TraceLayer::new_for_http()) .layer(CompressionLayer::new()) .layer(SetResponseHeaderLayer::overriding( HeaderName::from_static("strict-transport-security"), HeaderValue::from_static("max-age=2592000; includeSubDomains"), )), ); let addr = SocketAddr::from(([0, 0, 0, 0], 53465)); tracing::debug!("starting server on {addr}"); let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app).await?; Ok(()) } async fn render_index_handler( Query(options): Query, State(state): State, ) -> impl IntoResponse { let template = RootTemplate::new(state.apple_music_client, state.shas, &options); template.render().map(Html).map_err(|err| { tracing::error!("failed to render index: {err:?}"); StatusCode::INTERNAL_SERVER_ERROR }) } async fn render_media_partial_handler( Path(media_type): Path, State(state): State, ) -> impl IntoResponse { let media = fetch_media_of_type(media_type, state.apple_music_client) .await .unwrap(); let template = MediaTemplate { media_type, media }; template.render().map(Html).map_err(|err| { tracing::error!("failed to render {media_type} media: {err:?}"); StatusCode::INTERNAL_SERVER_ERROR }) } #[cfg(debug_assertions)] async fn render_apple_music_auth_flow( #[allow(unused_variables)] State(state): State, ) -> impl IntoResponse { let template = AuthFlowTemplate::new(&state.apple_music_client); template .and_then(|template| Ok(template.render()?)) .map(Html) .map_err(|err| { tracing::error!("failed to render Apple Music auth flow: {err:?}"); StatusCode::INTERNAL_SERVER_ERROR }) }