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
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,
|
|
}
|