music_player_webui/
lib.rs1#[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}