backend api server
This commit is contained in:
parent
cf4411bd7c
commit
5878cf9d8c
8
.cargo/config.toml
Normal file
8
.cargo/config.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[registry]
|
||||||
|
default = "crates-io"
|
||||||
|
|
||||||
|
[registries.elnafo-vcs]
|
||||||
|
index = "sparse+https://vcs.elnafo.ru/api/packages/L-Nafaryus/cargo/"
|
||||||
|
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
1116
Cargo.lock
generated
Normal file
1116
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
Normal file
27
Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "elnafo-radio"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = ["elnafo-vcs"]
|
||||||
|
authors = ["L-Nafaryus <l.nafaryus@elnafo.ru>"]
|
||||||
|
description = "Elnafo radio"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://vcs.elnafo.ru/L-Nafaryus/elnafo-radio"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.7.6", features = ["http2", "macros"] }
|
||||||
|
#elnafo-radio-frontend = { version = "0.1.0", path = "crates/frontend" }
|
||||||
|
mpd = "0.1.0"
|
||||||
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
|
serde_json = "1.0.128"
|
||||||
|
time = "0.3.36"
|
||||||
|
tokio = { version = "1.40.0", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||||
|
toml = "0.8.19"
|
||||||
|
tower-http = { version = "0.6.0", features = ["trace", "cors", "compression-gzip", "decompression-gzip"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
|
||||||
|
|
||||||
|
#[workspace]
|
||||||
|
#members = ["crates/frontend"]
|
||||||
|
#resolver = "2"
|
186
src/api.rs
Normal file
186
src/api.rs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::DefaultBodyLimit,
|
||||||
|
extract::State,
|
||||||
|
http::{header::*, Method, StatusCode},
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use mpd::Client;
|
||||||
|
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
|
use crate::Context;
|
||||||
|
use crate::{
|
||||||
|
config,
|
||||||
|
config::{Station, StationId, StationStatus},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn routes(state: Arc<Context>) -> Router {
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
|
||||||
|
.allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT, CONTENT_TYPE, COOKIE])
|
||||||
|
.allow_origin([
|
||||||
|
"http://localhost:54605".parse().unwrap(),
|
||||||
|
"http://localhost:5173".parse().unwrap(),
|
||||||
|
])
|
||||||
|
.allow_credentials(true);
|
||||||
|
|
||||||
|
Router::new()
|
||||||
|
.route("/healthcheck", get(healthcheck))
|
||||||
|
.route("/stations", get(stations))
|
||||||
|
.route("/status", get(status))
|
||||||
|
.layer(cors)
|
||||||
|
.fallback(fallback)
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub enum Playback {
|
||||||
|
Stopped,
|
||||||
|
Playing,
|
||||||
|
Paused,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct SongInfo {
|
||||||
|
pub artist: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub tags: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
pub struct StationInfo {
|
||||||
|
pub id: StationId,
|
||||||
|
pub name: String,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub status: config::StationStatus,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub genre: Option<String>,
|
||||||
|
pub playback: Option<Playback>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StationInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
StationInfo {
|
||||||
|
id: StationId(String::from("station")),
|
||||||
|
name: String::from("Station"),
|
||||||
|
url: None,
|
||||||
|
status: config::StationStatus::Receive,
|
||||||
|
location: None,
|
||||||
|
genre: None,
|
||||||
|
playback: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Station> for StationInfo {
|
||||||
|
fn from(station: Station) -> Self {
|
||||||
|
StationInfo {
|
||||||
|
id: station.id,
|
||||||
|
name: station.name,
|
||||||
|
url: station.url,
|
||||||
|
status: station.status,
|
||||||
|
location: station.location,
|
||||||
|
genre: station.genre,
|
||||||
|
..StationInfo::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stations(State(state): State<Arc<Context>>) -> (StatusCode, Json<Vec<StationInfo>>) {
|
||||||
|
match &state.config.stations {
|
||||||
|
Some(stations) => {
|
||||||
|
let mut stations_info: Vec<StationInfo> = Vec::with_capacity(stations.len());
|
||||||
|
|
||||||
|
for station in stations {
|
||||||
|
stations_info.push(match station.status {
|
||||||
|
StationStatus::Online | StationStatus::Offline => {
|
||||||
|
StationInfo::from(station.clone())
|
||||||
|
}
|
||||||
|
StationStatus::Receive => {
|
||||||
|
let connection =
|
||||||
|
Client::connect(format!("{}:{}", station.host, station.port));
|
||||||
|
|
||||||
|
if let Ok(mut client) = connection {
|
||||||
|
let mut info = StationInfo::from(station.clone());
|
||||||
|
|
||||||
|
if let Ok(status) = client.status() {
|
||||||
|
info.playback = match status.state {
|
||||||
|
mpd::State::Play => Some(Playback::Playing),
|
||||||
|
mpd::State::Stop => Some(Playback::Stopped),
|
||||||
|
mpd::State::Pause => Some(Playback::Paused),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
info
|
||||||
|
} else {
|
||||||
|
StationInfo::from(station.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(StatusCode::OK, Json(stations_info))
|
||||||
|
}
|
||||||
|
None => (StatusCode::OK, Json(Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct GetSongInfo {
|
||||||
|
pub id: StationId,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn status(
|
||||||
|
State(state): State<Arc<Context>>,
|
||||||
|
Json(body): Json<GetSongInfo>,
|
||||||
|
) -> (StatusCode, Json<Option<SongInfo>>) {
|
||||||
|
match &state.config.stations {
|
||||||
|
Some(stations) => {
|
||||||
|
for station in stations {
|
||||||
|
if station.id.0.eq(&body.id.0) {
|
||||||
|
let connection = Client::connect(format!("{}:{}", station.host, station.port));
|
||||||
|
|
||||||
|
if let Ok(mut client) = connection {
|
||||||
|
if let Ok(Some(current)) = client.currentsong() {
|
||||||
|
return (
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(Some(SongInfo {
|
||||||
|
artist: current.artist,
|
||||||
|
title: current.title,
|
||||||
|
tags: current.tags,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (StatusCode::OK, Json(None));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (StatusCode::OK, Json(None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(StatusCode::OK, Json(None))
|
||||||
|
}
|
||||||
|
None => (StatusCode::OK, Json(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn healthcheck() -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(json!({
|
||||||
|
"status": StatusCode::OK.to_string(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fallback() -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
Json(json!({
|
||||||
|
"status": StatusCode::NOT_FOUND.to_string(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
121
src/config.rs
Normal file
121
src/config.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum StationStatus {
|
||||||
|
Online,
|
||||||
|
Offline,
|
||||||
|
Receive,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct StationId(pub String);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Station {
|
||||||
|
pub id: StationId,
|
||||||
|
pub name: String,
|
||||||
|
pub host: String,
|
||||||
|
pub port: i32,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub status: StationStatus,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub genre: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Server {
|
||||||
|
pub address: String,
|
||||||
|
pub port: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub server: Server,
|
||||||
|
pub stations: Option<Vec<Station>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Config::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(path: &std::path::Path) -> Result<Config, ConfigError> {
|
||||||
|
fs::read_to_string(path)?.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_dir() -> Result<std::path::PathBuf, ConfigError> {
|
||||||
|
let cwd = std::env::current_dir()?;
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
Ok(cwd.join("temp"))
|
||||||
|
} else {
|
||||||
|
Ok(cwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> Result<String, ConfigError> {
|
||||||
|
Ok(toml::to_string(self)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&self, path: &std::path::Path) -> Result<(), ConfigError> {
|
||||||
|
Ok(fs::write(path, self.to_string()?)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Config {
|
||||||
|
server: Server {
|
||||||
|
address: String::from("127.0.0.1"),
|
||||||
|
port: 54605,
|
||||||
|
},
|
||||||
|
stations: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Config {
|
||||||
|
type Err = ConfigError;
|
||||||
|
fn from_str(s: &str) -> Result<Self, ConfigError> {
|
||||||
|
toml::from_str(s).map_err(|_| ConfigError::Parse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
Parse,
|
||||||
|
StringParse,
|
||||||
|
Serialize,
|
||||||
|
IO,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ConfigError {}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Parse => write!(f, "Failed to parse Config from string"),
|
||||||
|
Self::StringParse => write!(f, "Failed to parse environment variable"),
|
||||||
|
Self::Serialize => write!(f, "Failed to serialize Config to TOML"),
|
||||||
|
Self::IO => write!(f, "Faild to write file"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::ser::Error> for ConfigError {
|
||||||
|
fn from(_: toml::ser::Error) -> Self {
|
||||||
|
ConfigError::Serialize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for ConfigError {
|
||||||
|
fn from(_: std::io::Error) -> Self {
|
||||||
|
ConfigError::IO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::num::ParseIntError> for ConfigError {
|
||||||
|
fn from(_: std::num::ParseIntError) -> Self {
|
||||||
|
ConfigError::StringParse
|
||||||
|
}
|
||||||
|
}
|
54
src/main.rs
Normal file
54
src/main.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
use axum::{http::Uri, response::IntoResponse, routing::get, Router};
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tower_http::trace::{self, TraceLayer};
|
||||||
|
use tracing::Level;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
pub config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_target(false)
|
||||||
|
.compact()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let config = match Config::open(Config::data_dir()?.join("config.toml").as_path()) {
|
||||||
|
Ok(config) => {
|
||||||
|
println!("Config loaded: {:?}", config);
|
||||||
|
config
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{}", err);
|
||||||
|
Config::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = Arc::new(Context {
|
||||||
|
config: config.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let app = Router::new().nest("/api", api::routes(state)).layer(
|
||||||
|
TraceLayer::new_for_http()
|
||||||
|
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
|
||||||
|
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let address: SocketAddr =
|
||||||
|
format!("{}:{}", config.server.address, config.server.port).parse()?;
|
||||||
|
|
||||||
|
let lister = tokio::net::TcpListener::bind(&address).await?;
|
||||||
|
|
||||||
|
println!("Listening on {}", address);
|
||||||
|
|
||||||
|
axum::serve(lister, app.into_make_service()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user