You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

159 lines
3.8 KiB
Rust

mod entities;
mod providers;
mod templates;
use std::{
env, fs,
net::{IpAddr, SocketAddr},
num::NonZeroU16,
path::PathBuf,
sync::Arc,
};
use anyhow::{bail, Context, Result};
use askama::Template;
use axum::{
http::header,
response::{Html, IntoResponse},
routing::get,
Extension, Router,
};
use deadpool_redis::{Config as RedisConfig, Pool, Runtime};
use once_cell::sync::OnceCell;
use serde::Deserialize;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tracing::info;
use tracing_subscriber::EnvFilter;
use crate::providers::Providers;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
Configuration::load()?;
let redis = create_redis_pool(&Configuration::get().redis.url)?;
let providers = providers::start();
let state = Arc::new(State { providers, redis });
let app = Router::new()
.route("/", get(front_page))
.route("/style.css", get(styles))
.route("/releases/:provider/:artist_id", get(providers::get))
.layer(Extension(state))
.layer(CompressionLayer::new())
.layer(TraceLayer::new_for_http());
let config = Configuration::get();
let server_addr = SocketAddr::new(config.http.address, config.http.port.get());
info!(address = %server_addr, "starting");
axum::Server::try_bind(&server_addr)?
.serve(app.into_make_service())
.await?;
Ok(())
}
async fn styles() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/css")],
include_str!("../static/style.css"),
)
}
async fn front_page() -> impl IntoResponse {
let domain = &Configuration::get().http.domain;
Html(templates::Frontpage { domain }.render().unwrap())
}
static CONFIGURATION: OnceCell<Configuration> = OnceCell::new();
#[derive(Debug, Deserialize)]
struct Configuration {
http: Http,
redis: Redis,
spotify: Spotify,
bandcamp: Bandcamp,
}
#[derive(Debug, Deserialize)]
struct Http {
domain: String,
address: IpAddr,
port: NonZeroU16,
}
#[derive(Debug, Deserialize)]
struct Redis {
url: String,
cache_duration: usize,
}
#[derive(Debug, Deserialize)]
struct Spotify {
client_id: String,
client_secret: String,
}
#[derive(Debug, Deserialize)]
struct Bandcamp {
releases_limit: usize,
}
impl Configuration {
fn load() -> Result<(), anyhow::Error> {
let path = env::args_os()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("./config.toml"));
let config_file = fs::read(&path).with_context(|| {
format!("Failed to open configuration file at '{}'", path.display())
})?;
let config =
toml::from_slice::<Self>(&config_file).context("failed to read configuration file")?;
if config.redis.url.is_empty() {
bail!("Redis URL cannot be empty");
}
if config.spotify.client_secret.is_empty() {
bail!("Spotify client secret cannot be empty");
}
if config.spotify.client_secret.is_empty() {
bail!("Spotify client secret cannot be empty");
}
CONFIGURATION
.set(config)
.expect("configuration was already initialized");
Ok(())
}
fn get() -> &'static Configuration {
CONFIGURATION
.get()
.expect("configuration was not yet initialized")
}
}
fn create_redis_pool(url: &str) -> Result<Pool> {
let pool = RedisConfig::from_url(url)
.create_pool(Some(Runtime::Tokio1))
.with_context(|| format!("Failed to create Redis connection pool (at {:?})", url))?;
Ok(pool)
}
struct State {
providers: Providers,
redis: Pool,
}