diff --git a/.gitmodules b/.gitmodules index c7bfef07d9c523de8c8c4b80d41aaf1b92c2fdf9..4449e724ef3620f0d58908dc22a79b33f5a19a5c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,5 +2,5 @@ path = cc-player url = git@gitlab.fachschaften.org:spoccify/spoccify-cc-player.git [submodule "frontend"] - path = frontend + path = svelte-frontend url = git@gitlab.fachschaften.org:spoccify/spoccify-frontend.git diff --git a/Cargo.lock b/Cargo.lock index 19e092329cb6d70e5f2bfdbd48a4fdb1e4528f77..0362cffa6f9babd52592cbf016fb4ff9defb8029 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,12 @@ dependencies = [ "serde", ] +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + [[package]] name = "futures-task" version = "0.3.25" @@ -377,6 +383,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mio" version = "0.8.5" @@ -656,6 +672,8 @@ dependencies = [ "serde", "serde_json", "tokio", + "tower", + "tower-http", "tungstenite", "url", "uuid", @@ -744,6 +762,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.4.13" @@ -773,10 +804,17 @@ dependencies = [ "http", "http-body", "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -843,6 +881,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.8" diff --git a/Cargo.toml b/Cargo.toml index 5ee89e754ad0e158c7fae2c170b1099769c03d47..b7b73d1cb0769c6b4352ad3b50317eef7b8afc76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ rand = "0.8.5" serde = { version = "1.0.147", features = ["derive"] } serde_json = "1.0.88" tokio = { version = "1", features = ["full"] } +tower = { version = "0.4.13", features = ["util"] } +tower-http = { version = "0.3.4", features = ["fs", "trace"] } tungstenite = "0.17.3" url = { version = "2.3.1", features = ["serde"] } uuid = { version = "1.2.2", features = ["v4"] } diff --git a/src/downloader.rs b/src/downloader.rs index 903ec30a71dc39ac27128dcb062cac7164db5b95..14d373a3fabb70fbb5e85870f968f8fd7efce902 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -8,7 +8,6 @@ use url::Url; use uuid::Uuid; use crate::song::Song; -use crate::util; const TMP_DOWNLOAD_DIR: &str = "./tmp"; @@ -167,25 +166,28 @@ pub async fn manage_downloads(playlist: &Mutable<Vec<Song>>) -> () { } fn fetch_info(playlist: Mutable<Vec<Song>>, uuid: &Uuid, url: &Url) { - println!("Fetching info for {}!", url); + println!("Fetching info for {}!", uuid); - let tmp_file_name = util::random_base64(); - let tmp_file_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(tmp_file_name); + let base_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(uuid.to_string()); + let info_json_path = base_path.join("audio"); + + std::fs::create_dir(&base_path) + .expect(&format!("Failed to create directory for song {}!", uuid)); process::Command::new("yt-dlp") .args([ "--output", - tmp_file_path.as_str(), + info_json_path.as_str(), "--write-info-json", - "--write-thumbnail", "--skip-download", url.as_str(), ]) .output() .expect("Failed to fetch info!"); - let info_json_file = format!("{}.info.json", tmp_file_path); + + let info_json_path = base_path.join("audio.info.json"); - let data = std::fs::read_to_string(info_json_file).expect("Could not find info.json!"); + let data = std::fs::read_to_string(&info_json_path).expect("Could not find info.json!"); let info_json: Value = serde_json::from_str(&data).expect("Could not parse info.json!"); @@ -220,9 +222,27 @@ fn fetch_info(playlist: Mutable<Vec<Song>>, uuid: &Uuid, url: &Url) { } else { eprintln!("Could not fetch artist_url!"); } + println!("Fetched all info!"); - let thumbnail_file = format!("{}.webm", tmp_file_path); + let thumbnail_path = base_path.join("thumbnail"); + + process::Command::new("yt-dlp") + .args([ + "--load-info-json", + info_json_path.as_str(), + "--output", + thumbnail_path.as_str(), + "--write-thumbnail", + "--skip-download", + url.as_str(), + ]) + .output() + .expect("Failed to download thumbnail!"); + + let thumbnail_path = base_path.join("thumbnail.webp"); + + println!("Downloaded thumbnail!"); let mut playlist = playlist.lock_mut(); if let Some(index) = (*playlist).iter().position(|song| song.equals_uuid(uuid)) { @@ -233,7 +253,7 @@ fn fetch_info(playlist: Mutable<Vec<Song>>, uuid: &Uuid, url: &Url) { artist: artist, artist_url: artist_url, length: length, - thumbnail_file: thumbnail_file.to_owned(), + thumbnail_file: thumbnail_path.to_string(), }; } drop(playlist); @@ -249,20 +269,27 @@ fn download_song( length: &Option<u64>, thumbnail_file: &str, ) { - let audio_file_name = util::random_base64(); - let audio_file_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(audio_file_name); + let base_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(uuid.to_string()); + let info_json_path = base_path.join("audio.info.json"); + let audio_file_path = base_path.join("audio"); println!("Downloading from {} to {}!", url, audio_file_path); process::Command::new("yt-dlp") - .args(["--output", audio_file_path.as_str(), url.as_str()]) + .args([ + "--load-info-json", + info_json_path.as_str(), + "--output", + audio_file_path.as_str(), + url.as_str(), + ]) .output() .expect("Failed to download audio!"); + + let audio_file_path = base_path.join("audio.webm"); println!("Download successful!"); - let audio_file_path = format!("{}.webm", audio_file_path); - let mut playlist = playlist.lock_mut(); if let Some(index) = (*playlist).iter().position(|song| song.equals_uuid(uuid)) { playlist[index] = Song::Downloaded { @@ -273,7 +300,7 @@ fn download_song( artist_url: artist_url.to_owned(), length: length.to_owned(), thumbnail_file: thumbnail_file.to_owned(), - audio_file: audio_file_path.to_owned(), + audio_file: audio_file_path.to_string(), }; } drop(playlist); @@ -290,17 +317,16 @@ fn convert_song( thumbnail_file: &str, audio_file: &str, ) { - let audio_file = Utf8Path::new(audio_file); - - let raw_file_name = format!("{}.raw", util::random_base64()); - let raw_file_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(raw_file_name); + let base_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(uuid.to_string()); + let audio_file_path = base_path.join("audio.webm"); + let raw_file_path = base_path.join("audio.raw"); println!("Converting from {} to {}!", audio_file, raw_file_path); process::Command::new("ffmpeg") .args([ "-i", - audio_file.as_str(), + audio_file_path.as_str(), "-f", "u8", "-acodec", @@ -313,7 +339,8 @@ fn convert_song( ]) .output() .expect("Failed to convert audio!"); - std::fs::remove_file(audio_file).expect("Failed to remove audio file!"); + + std::fs::remove_file(&audio_file_path).expect("Failed to remove audio file!"); println!("Conversion successful!"); @@ -327,7 +354,7 @@ fn convert_song( artist_url: artist_url.to_owned(), length: length.to_owned(), thumbnail_file: thumbnail_file.to_owned(), - raw_file: raw_file_path.as_str().to_owned(), + raw_file: raw_file_path.to_string(), }; } drop(playlist); diff --git a/src/http.rs b/src/http.rs index 9a9bfbfd1c1d2874a04d553e6c91de35d984eec4..5d2a982a2b8dd8d0b365be6a653193f781281197 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,11 +1,47 @@ +use axum::handler::HandlerWithoutStateExt; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::routing::get_service; use axum::Router; -use axum::routing::get; +// use futures_signals::signal::Mutable; +use std::io; +use tower::util::ServiceExt; +use tower_http::services::ServeDir; -pub async fn handle_http(http_bind_address: &str) { - let app = Router::new().route("/", get(|| async { "Hello, World!" })); +// use crate::song::Song; - axum::Server::bind(&http_bind_address.to_string().parse().expect("Invalid http bind address!")) - .serve(app.into_make_service()) - .await - .expect("Failed to bind http port!"); +pub async fn handle_http(http_bind_address: &str, /* playlist_websocket: &Mutable<Vec<Song>> */) { + println!("Listening for http connections on {}!", http_bind_address); + + async fn handle_404() -> (StatusCode, &'static str) { + (StatusCode::NOT_FOUND, "Not found") + } + + let handle_404_service = handle_404 + .into_service() + .map_err(|err| -> std::io::Error { match err {} }); + + let serve_frontend = ServeDir::new("frontend").not_found_service(handle_404_service.clone()); + let serve_frontend = get_service(serve_frontend).handle_error(handle_error); + + let serve_thumbnails = ServeDir::new("tmp").not_found_service(handle_404_service.clone()); + let serve_thumbnails = get_service(serve_thumbnails).handle_error(handle_error); + + let app = Router::new() + .nest_service("/", serve_frontend) + .nest_service("/thumbnail", serve_thumbnails); + + axum::Server::bind( + &http_bind_address + .to_string() + .parse() + .expect("Invalid http bind address!"), + ) + .serve(app.into_make_service()) + .await + .expect("Failed to bind http port!"); +} + +async fn handle_error(_err: io::Error) -> impl IntoResponse { + (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error") } diff --git a/src/main.rs b/src/main.rs index 75839f57264eca9193b69fa3956757435803e5bc..d21526b6e5dfc2305236c77c27ec4e04bde1146a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,32 +15,30 @@ use song::Song; #[tokio::main] async fn main() { - let playlist = Mutable::new(Vec::<Song>::new()); - println!("Starting!"); - let http_bind_host = env::var("SPOCCIFY_HTTP_BIND_HOST").unwrap_or("localhost".to_owned()); + let playlist = Mutable::new(Vec::<Song>::new()); + // let playlist_http = Mutable::clone(&playlist); + let playlist_websockets = Mutable::clone(&playlist); + let playlist_downloader = Mutable::clone(&playlist); + + let http_bind_host = env::var("SPOCCIFY_HTTP_BIND_HOST").unwrap_or("127.0.0.1".to_owned()); let http_bind_port = env::var("SPOCCIFY_HTTP_BIND_PORT").unwrap_or("9000".to_owned()); let http_bind_address = format!("{}:{}", http_bind_host, http_bind_port); - // tokio::spawn(async move { - // http::handle_http(&http_bind_address).await; - // }); - let player_token = Arc::new(env::var("SPOCCIFY_PLAYER_TOKEN").expect("No player token provided!")); let client_token = Arc::new(env::var("SPOCCIFY_CLIENT_TOKEN").expect("No client token provided!")); let websocket_bind_host = - env::var("SPOCCIFY_WEBSOCKET_BIND_HOST").unwrap_or("localhost".to_owned()); + env::var("SPOCCIFY_WEBSOCKET_BIND_HOST").unwrap_or("127.0.0.1".to_owned()); let websocket_bind_port = env::var("SPOCCIFY_WEBSOCKET_BIND_PORT").unwrap_or("9001".to_owned()); let websocket_bind_address = format!("{}:{}", websocket_bind_host, websocket_bind_port); - let playlist_websockets = Mutable::clone(&playlist); - tokio::spawn(async move { + let websockets_task = tokio::task::spawn(async move { websocket::handle_websockets( &player_token, &client_token, @@ -50,11 +48,20 @@ async fn main() { .await; }); - let playlist_downloader = Mutable::clone(&playlist); - - tokio::spawn(async move { + let downloader_task = tokio::task::spawn(async move { downloader::manage_downloads(&playlist_downloader).await; }); - loop {} + tokio::join!( + http::handle_http(&http_bind_address /* , &playlist_http */), + /* websocket::handle_websockets( + &player_token, + &client_token, + &websocket_bind_address, + &playlist_websockets, + ), */ + // websockets_task.await.unwrap(), + /* downloader::manage_downloads(&playlist_downloader) */ + // downloader_task.await.unwrap(), + ); } diff --git a/src/util.rs b/src/util.rs index 3cec313d2eeac7369266777fe937c387196b9361..70b1ca26a98f70f82d2ac3e1be229236a4e9b39a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,6 @@ use rand::random; -pub fn random_base64() -> String { +pub fn _random_base64() -> String { base64::encode_config( random::<[u8; 8]>(), base64::Config::new(base64::CharacterSet::UrlSafe, false), diff --git a/src/websocket.rs b/src/websocket.rs index bcf0c626317e127e0ec1730d68a3d0b681efe0de..5e24b6aaef9d31ddfc0a67f2c85ed51e3b649c9f 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -12,9 +12,10 @@ use crate::client; use crate::player; pub async fn handle_websockets(player_token: &Arc<String>, client_token: &Arc<String>, websocket_bind_address: &str, playlist_websocket: &Mutable<Vec<Song>>) { - println!("Listening for websocket connections on {}!", websocket_bind_address); - let server = TcpListener::bind(websocket_bind_address).expect("Failed to bind websocket port!"); + + println!("Listening for websocket connections on {}!", websocket_bind_address); + for stream in server.incoming() { let playlist_clone = Mutable::clone(playlist_websocket); let player_token_clone = Arc::clone(player_token);