A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1use std::{
2 path::PathBuf,
3 sync::{mpsc::Sender, Arc, Mutex},
4};
5
6use actix_cors::Cors;
7use actix_files::{self as fs, NamedFile};
8use actix_web::{
9 error::ErrorNotFound,
10 guard,
11 http::header::{ContentDisposition, DispositionType, HOST},
12 web::{self, Data},
13 App, HttpRequest, HttpResponse, HttpServer, Result,
14};
15use anyhow::Error;
16use async_graphql::{http::GraphiQLSource, Schema};
17use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
18use rockbox_library::{create_connection_pool, repo};
19use rockbox_search::create_indexes;
20use rockbox_sys::events::RockboxCommand;
21use rockbox_webui::{dist, index, index_spa};
22use sqlx::{Pool, Sqlite};
23
24use crate::{
25 schema::{Mutation, Query, Subscription},
26 RockboxSchema,
27};
28
29async fn index_ws(
30 schema: web::Data<RockboxSchema>,
31 req: HttpRequest,
32 payload: web::Payload,
33) -> Result<HttpResponse> {
34 GraphQLSubscription::new(Schema::clone(&*schema)).start(&req, payload)
35}
36
37#[actix_web::post("/graphql")]
38async fn index_graphql(schema: web::Data<RockboxSchema>, req: GraphQLRequest) -> GraphQLResponse {
39 schema.execute(req.into_inner()).await.into()
40}
41
42#[actix_web::get("/graphiql")]
43async fn index_graphiql(req: HttpRequest) -> Result<HttpResponse> {
44 let host = req
45 .headers()
46 .get(HOST)
47 .unwrap()
48 .to_str()
49 .unwrap()
50 .split(":")
51 .next()
52 .unwrap();
53
54 let http_port = std::env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or("6062".to_string());
55 let graphql_endpoint = format!("http://{}:{}/graphql", host, http_port);
56 let ws_endpoint = format!("ws://{}:{}/graphql", host, http_port);
57 Ok(HttpResponse::Ok()
58 .content_type("text/html; charset=utf-8")
59 .body(
60 GraphiQLSource::build()
61 .endpoint(&graphql_endpoint)
62 .subscription_endpoint(&ws_endpoint)
63 .finish(),
64 ))
65}
66
67async fn index_file(req: HttpRequest) -> Result<NamedFile, actix_web::Error> {
68 let id = req.match_info().get("id").unwrap();
69 let id = id.split('.').next().unwrap();
70 let mut path = PathBuf::new();
71
72 println!("id: {}", id);
73
74 let pool = req.app_data::<Pool<Sqlite>>().unwrap();
75 match repo::track::find(pool.clone(), id).await {
76 Ok(Some(track)) => {
77 path.push(track.path);
78 println!("Serving file: {}", path.display());
79 let file = NamedFile::open(path)?;
80 Ok(file.set_content_disposition(ContentDisposition {
81 disposition: DispositionType::Attachment,
82 parameters: vec![],
83 }))
84 }
85 Ok(None) => Err(ErrorNotFound("Track not found").into()),
86 Err(_) => Err(ErrorNotFound("Track not found").into()),
87 }
88}
89
90pub async fn start(cmd_tx: Arc<Mutex<Sender<RockboxCommand>>>) -> Result<(), Error> {
91 let client = reqwest::Client::new();
92 let pool = create_connection_pool().await?;
93 let indexes = create_indexes()?;
94 let schema = Schema::build(
95 Query::default(),
96 Mutation::default(),
97 Subscription::default(),
98 )
99 .data(cmd_tx)
100 .data(client)
101 .data(pool.clone())
102 .data(indexes)
103 .finish();
104
105 let graphql_port = std::env::var("ROCKBOX_GRAPHQL_PORT").unwrap_or("6062".to_string());
106 let addr = format!("{}:{}", "0.0.0.0", graphql_port);
107
108 HttpServer::new(move || {
109 let home = std::env::var("HOME").unwrap();
110 let rockbox_data_dir = format!("{}/.config/rockbox.org", home);
111 let covers_path = format!("{}/covers", rockbox_data_dir);
112 std::fs::create_dir_all(&covers_path).unwrap();
113
114 let cors = Cors::permissive();
115 App::new()
116 .app_data(pool.clone())
117 .app_data(Data::new(schema.clone()))
118 .wrap(cors)
119 .service(index_graphql)
120 .service(index_graphiql)
121 .service(
122 web::resource("/graphql")
123 .guard(guard::Get())
124 .guard(guard::Header("upgrade", "websocket"))
125 .to(index_ws),
126 )
127 .service(fs::Files::new("/covers", covers_path).show_files_listing())
128 .service(index)
129 .route("/tracks", web::get().to(index_spa))
130 .route("/artists", web::get().to(index_spa))
131 .route("/albums", web::get().to(index_spa))
132 .route("/files", web::get().to(index_spa))
133 .route("/likes", web::get().to(index_spa))
134 .route("/settings", web::get().to(index_spa))
135 .route("/artists/{_:.*}", web::get().to(index_spa))
136 .route("/albums/{_:.*}", web::get().to(index_spa))
137 .route("/playlists/{_:.*}", web::get().to(index_spa))
138 .route("/files/{_:.*}", web::get().to(index_spa))
139 .route("/tracks/{id}", web::get().to(index_file))
140 .route("/tracks/{id}", web::head().to(index_file))
141 .service(dist)
142 })
143 .bind(addr)?
144 .run()
145 .await
146 .map_err(Error::new)
147}