music_player_webui/
lib.rs

1#[cfg(test)]
2mod tests;
3
4use actix_cors::Cors;
5use actix_files as fs;
6use actix_web::{
7    error::ErrorNotFound,
8    guard,
9    http::header::{ContentDisposition, DispositionType, HOST},
10    web::{self, Data},
11    App, Error, HttpRequest, HttpResponse, HttpServer, Responder, Result,
12};
13use async_graphql::{http::GraphiQLSource, Schema};
14use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
15use fs::NamedFile;
16use mime_guess::from_path;
17use music_player_addons::{CurrentDevice, CurrentReceiverDevice, CurrentSourceDevice};
18use music_player_entity::track as track_entity;
19use music_player_graphql::{
20    scan_devices,
21    schema::{Mutation, Query, Subscription},
22    MusicPlayerSchema,
23};
24use music_player_playback::player::PlayerCommand;
25use music_player_settings::{get_application_directory, read_settings, Settings};
26use music_player_storage::{searcher::Searcher, Database};
27use music_player_tracklist::Tracklist;
28use owo_colors::OwoColorize;
29use rust_embed::RustEmbed;
30use sea_orm::EntityTrait;
31use std::{path::PathBuf, sync::Arc};
32use tokio::sync::{mpsc::UnboundedSender, Mutex};
33
34#[derive(RustEmbed)]
35#[folder = "musicplayer/build/"]
36struct Asset;
37
38fn handle_embedded_file(path: &str) -> HttpResponse {
39    match Asset::get(path) {
40        Some(content) => HttpResponse::Ok()
41            .content_type(from_path(path).first_or_octet_stream().as_ref())
42            .body(content.data.into_owned()),
43        None => HttpResponse::NotFound().body("404 Not Found"),
44    }
45}
46
47#[actix_web::get("/")]
48async fn index() -> impl Responder {
49    handle_embedded_file("index.html")
50}
51
52async fn index_spa() -> impl Responder {
53    handle_embedded_file("index.html")
54}
55
56async fn index_file(db: Data<Database>, req: HttpRequest) -> Result<NamedFile, Error> {
57    let id = req.match_info().get("id").unwrap();
58    let id = id.split('.').next().unwrap();
59    let mut path = PathBuf::new();
60
61    println!("id: {}", id);
62
63    let track = track_entity::Entity::find_by_id(id.to_owned())
64        .one(db.get_connection())
65        .await
66        .unwrap();
67
68    if let Some(track) = track {
69        path.push(track.uri);
70        println!("Serving file: {}", path.display());
71        let file = NamedFile::open(path)?;
72        Ok(file.set_content_disposition(ContentDisposition {
73            disposition: DispositionType::Attachment,
74            parameters: vec![],
75        }))
76    } else {
77        Err(ErrorNotFound("Track not found".to_string()))
78    }
79}
80
81async fn index_ws(
82    schema: web::Data<MusicPlayerSchema>,
83    req: HttpRequest,
84    payload: web::Payload,
85) -> Result<HttpResponse> {
86    GraphQLSubscription::new(Schema::clone(&*schema)).start(&req, payload)
87}
88
89#[actix_web::post("/graphql")]
90async fn index_graphql(
91    schema: web::Data<MusicPlayerSchema>,
92    req: GraphQLRequest,
93) -> GraphQLResponse {
94    schema.execute(req.into_inner()).await.into()
95}
96
97#[actix_web::get("/graphiql")]
98async fn index_graphiql(req: HttpRequest) -> Result<HttpResponse> {
99    let host = req
100        .headers()
101        .get(HOST)
102        .unwrap()
103        .to_str()
104        .unwrap()
105        .split(":")
106        .next()
107        .unwrap();
108
109    let config = read_settings().unwrap();
110    let settings = config.try_deserialize::<Settings>().unwrap();
111    let graphql_endpoint = format!("http://{}:{}/graphql", host, settings.http_port);
112    let ws_endpoint = format!("ws://{}:{}/graphql", host, settings.http_port);
113    Ok(HttpResponse::Ok()
114        .content_type("text/html; charset=utf-8")
115        .body(
116            GraphiQLSource::build()
117                .endpoint(&graphql_endpoint)
118                .subscription_endpoint(&ws_endpoint)
119                .finish(),
120        ))
121}
122
123#[actix_web::get("/{_:.*}")]
124async fn dist(path: web::Path<String>) -> impl Responder {
125    handle_embedded_file(path.as_str())
126}
127
128pub async fn start_webui(
129    cmd_tx: Arc<std::sync::Mutex<UnboundedSender<PlayerCommand>>>,
130    tracklist: Arc<std::sync::Mutex<Tracklist>>,
131) -> std::io::Result<()> {
132    let config = read_settings().unwrap();
133    let settings = config.try_deserialize::<Settings>().unwrap();
134
135    let addr = format!("0.0.0.0:{}", settings.http_port);
136
137    let devices = scan_devices().await.unwrap();
138    let current_device = Arc::new(Mutex::new(CurrentDevice::new()));
139    let source_device = Arc::new(Mutex::new(CurrentSourceDevice::new()));
140    let receiver_device = Arc::new(Mutex::new(CurrentReceiverDevice::new()));
141    let searcher = Arc::new(Mutex::new(Searcher::new()));
142    let schema = Schema::build(
143        Query::default(),
144        Mutation::default(),
145        Subscription::default(),
146    )
147    .data(Database::new().await)
148    .data(cmd_tx)
149    .data(tracklist)
150    .data(devices)
151    .data(current_device)
152    .data(source_device)
153    .data(receiver_device)
154    .data(searcher)
155    .finish();
156    println!("Starting webui at {}", addr.bright_green());
157
158    HttpServer::new(move || {
159        let cors = Cors::permissive();
160        let db = futures::executor::block_on(Database::new());
161        let covers_path = format!("{}/covers", get_application_directory());
162        App::new()
163            .app_data(Data::new(db.clone()))
164            .app_data(Data::new(schema.clone()))
165            .wrap(cors)
166            .service(index_graphql)
167            .service(index_graphiql)
168            .service(
169                web::resource("/graphql")
170                    .guard(guard::Get())
171                    .guard(guard::Header("upgrade", "websocket"))
172                    .to(index_ws),
173            )
174            .service(fs::Files::new("/covers", covers_path).show_files_listing())
175            .service(index)
176            .route("/tracks", web::get().to(index_spa))
177            .route("/artists", web::get().to(index_spa))
178            .route("/albums", web::get().to(index_spa))
179            .route("/artists/{_:.*}", web::get().to(index_spa))
180            .route("/albums/{_:.*}", web::get().to(index_spa))
181            .route("/folders/{_:.*}", web::get().to(index_spa))
182            .route("/playlists/{_:.*}", web::get().to(index_spa))
183            .route("/search", web::get().to(index_spa))
184            .route("/tracks/{id}", web::get().to(index_file))
185            .route("/tracks/{id}", web::head().to(index_file))
186            .service(dist)
187    })
188    .bind(addr)?
189    .run()
190    .await
191}