From 7062c750133e7ffd681968697ef2a54466ee0e5d Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 11 Apr 2024 23:06:04 -0400 Subject: [PATCH 01/56] Add log crate --- Cargo.lock | 1 + Cargo.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 90dc998..f3a10bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1510,6 +1510,7 @@ dependencies = [ "leptos_icons", "leptos_meta", "leptos_router", + "log", "openssl", "pbkdf2", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4677ca9..e1fa462 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ tower-sessions = { version = "0.11", default-features = false } tower-sessions-redis-store = { version = "0.11", optional = true } async-trait = "0.1.79" axum-login = { version = "0.14.0", optional = true } +log = { version = "0.4.21", optional = true } [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } @@ -61,6 +62,7 @@ ssr = [ "tower-http", "tower-sessions-redis-store", "axum-login", + "log", ] # Defines a size-optimized profile for the WASM bundle in release mode From 4f6f3f90c169a97069c48d66940e4516c5a90e89 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 11 Apr 2024 23:14:53 -0400 Subject: [PATCH 02/56] Add flexi_logger crate --- Cargo.lock | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 86 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index f3a10bc..5b2514e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.81" @@ -316,6 +331,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.4", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -422,6 +449,12 @@ dependencies = [ "futures", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -613,6 +646,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "flexi_logger" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f248c29a6d4bc5d065c9e9068d858761a0dcd796759f7801cc14db35db23abd8" +dependencies = [ + "chrono", + "glob", + "log", + "thiserror", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -796,6 +841,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "gloo-net" version = "0.5.0" @@ -965,6 +1016,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icondata" version = "0.3.0" @@ -1501,6 +1575,7 @@ dependencies = [ "diesel", "diesel_migrations", "dotenv", + "flexi_logger", "futures", "http", "icondata", @@ -3067,6 +3142,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index e1fa462..48f3b78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ tower-sessions-redis-store = { version = "0.11", optional = true } async-trait = "0.1.79" axum-login = { version = "0.14.0", optional = true } log = { version = "0.4.21", optional = true } +flexi_logger = { version = "0.28.0", optional = true, default-features = false } [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } @@ -63,6 +64,7 @@ ssr = [ "tower-sessions-redis-store", "axum-login", "log", + "flexi_logger", ] # Defines a size-optimized profile for the WASM bundle in release mode From 557c189a17cdf2df0bfce0629b88d6acfa611d4e Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 11 Apr 2024 23:19:02 -0400 Subject: [PATCH 03/56] Add ASCII art of logo and name --- ascii_art.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 ascii_art.txt diff --git a/ascii_art.txt b/ascii_art.txt new file mode 100644 index 0000000..bdd918f --- /dev/null +++ b/ascii_art.txt @@ -0,0 +1,10 @@ + .-=. =+==-. + .=++++. =+++++=. + _ _ _ _______ =++++++= :*++++++= + | | (_) | |__ __| -++++**+. -+**++++- + | | _| |__ _ __ ___| |_ _ _ __ ___ ___ *+++**+ +**+++* + | | | | '_ \| '__/ _ \ | | | | '_ \ / _ \/ __| *+++**+ +**+++* + | |____| | |_) | | | __/ | |_| | | | | __/\__ \ -++++**+-: +**++++- + |______|_|_.__/|_| \___|_|\__,_|_| |_|\___||___/ =++++++**+ -+++++= + .=+++++++=.+++=. + .-==++++---. From 241389e5f7fca172b2da189cc888507afd6753a9 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 11 Apr 2024 23:19:21 -0400 Subject: [PATCH 04/56] Add logging to main --- src/main.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6f9b667..e647089 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,13 +23,23 @@ async fn main() { use tower_sessions_redis_store::{fred::prelude::*, RedisStore}; use axum_login::AuthManagerLayerBuilder; use libretunes::auth_backend::AuthBackend; + use log::*; + + flexi_logger::Logger::try_with_env_or_str("debug").unwrap().format(flexi_logger::opt_format).start().unwrap(); + + info!("\n{}", include_str!("../ascii_art.txt")); + info!("Starting Leptos server..."); use dotenv::dotenv; dotenv().ok(); + debug!("Running database migrations..."); + // Bring the database up to date libretunes::database::migrate(); + debug!("Connecting to Redis..."); + let redis_url = std::env::var("REDIS_URL").expect("REDIS_URL must be set"); let redis_config = RedisConfig::from_url(&redis_url).expect(&format!("Unable to parse Redis URL: {}", redis_url)); let redis_pool = RedisPool::new(redis_config, None, None, None, 1).expect("Unable to create Redis pool"); @@ -55,9 +65,10 @@ async fn main() { .fallback(file_and_error_handler) .with_state(leptos_options); - println!("listening on http://{}", &addr); - let listener = tokio::net::TcpListener::bind(&addr).await.expect(&format!("Could not bind to {}", &addr)); + + info!("Listening on http://{}", &addr); + axum::serve(listener, app.into_make_service()).await.expect("Server failed"); } From b6066e2d3e325bc7d3d6d82d9c4432e6a8a43aed Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 11 Apr 2024 23:42:22 -0400 Subject: [PATCH 05/56] Include ASCII art file in Docker build --- .dockerignore | 1 + Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 59dfed4..5ab2a05 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,4 @@ !/style !/Cargo.lock !/Cargo.toml +!/ascii_art.txt diff --git a/Dockerfile b/Dockerfile index c02cd2f..a0390e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ COPY style /app/style # Minify CSS RUN npx tailwindcss -i /app/style/main.scss -o /app/style/main.scss --minify +COPY ascii_art.txt /app/ascii_art.txt COPY assets /app/assets COPY src /app/src COPY migrations /app/migrations From 04e0a661221f3cbafeb106996376c0069878f22f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 12 Apr 2024 17:04:51 -0400 Subject: [PATCH 06/56] Add server_fn crate --- Cargo.lock | 6 ++++-- Cargo.toml | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90dc998..fb9df85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1513,6 +1513,7 @@ dependencies = [ "openssl", "pbkdf2", "serde", + "server_fn", "thiserror", "time", "tokio", @@ -2298,9 +2299,9 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a46a2ffdecb81430ecfb995989218a18b6e94c1ead50cb806b5927c986a8ce" +checksum = "536a5b959673643ee01e59ae41bf01425482c8070dee95d7061ee2d45296b59c" dependencies = [ "axum", "bytes", @@ -2314,6 +2315,7 @@ dependencies = [ "hyper", "inventory", "js-sys", + "multer", "once_cell", "send_wrapper", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4677ca9..d4f43a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ tower-sessions = { version = "0.11", default-features = false } tower-sessions-redis-store = { version = "0.11", optional = true } async-trait = "0.1.79" axum-login = { version = "0.14.0", optional = true } +server_fn = { version = "0.6.11", features = ["multipart"] } [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } From 6bf08b7507f25632214b79f48c5a88041f61a91e Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 19 Apr 2024 09:23:10 -0400 Subject: [PATCH 07/56] Add symphonia crate --- Cargo.lock | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 64 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index fb9df85..b47c5f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-recursion" version = "1.1.0" @@ -244,6 +250,12 @@ version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" + [[package]] name = "byteorder" version = "1.5.0" @@ -1514,6 +1526,7 @@ dependencies = [ "pbkdf2", "serde", "server_fn", + "symphonia", "thiserror", "time", "tokio", @@ -2421,6 +2434,55 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index d4f43a8..aab7d94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ tower-sessions-redis-store = { version = "0.11", optional = true } async-trait = "0.1.79" axum-login = { version = "0.14.0", optional = true } server_fn = { version = "0.6.11", features = ["multipart"] } +symphonia = { version = "0.5.4", default-features = false, features = ["mp3"], optional = true } [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } @@ -62,6 +63,7 @@ ssr = [ "tower-http", "tower-sessions-redis-store", "axum-login", + "symphonia", ] # Defines a size-optimized profile for the WASM bundle in release mode From 91812f4695b800a5254a3c583d3e562b0c3434a7 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 10:32:19 -0400 Subject: [PATCH 08/56] Fix misspelling of "version" for serde --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 48f3b78..d9b7338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ icondata = { version = "0.3.0" } dotenv = { version = "0.15.0", optional = true } diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional = true } lazy_static = { version = "1.4.0", optional = true } -serde = { versions = "1.0.195", features = ["derive"] } +serde = { version = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } time = { version = "0.3.34", features = ["serde"] } diesel_migrations = { version = "2.1.0", optional = true } From b2fd8dc8e5438d933bc5fe508f80ced39859e734 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 10:33:16 -0400 Subject: [PATCH 09/56] Fix misspelling of "version" for tower --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d9b7338..52ad1ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } futures = { version = "0.3.30", default-features = false, optional = true } tokio = { version = "1", optional = true, features = ["rt-multi-thread"] } axum = { version = "0.7.5", optional = true } -tower = { veresion = "0.4.13", optional = true } +tower = { version = "0.4.13", optional = true } tower-http = { version = "0.5", optional = true, features = ["fs"] } thiserror = "1.0.57" tower-sessions = { version = "0.11", default-features = false } From 490772654d0fe85e2cfae4e989017442e22d9853 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 10:55:26 -0400 Subject: [PATCH 10/56] Add multer crate --- Cargo.lock | 1 + Cargo.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b47c5f2..9939a7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1522,6 +1522,7 @@ dependencies = [ "leptos_icons", "leptos_meta", "leptos_router", + "multer", "openssl", "pbkdf2", "serde", diff --git a/Cargo.toml b/Cargo.toml index aab7d94..78b142c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ async-trait = "0.1.79" axum-login = { version = "0.14.0", optional = true } server_fn = { version = "0.6.11", features = ["multipart"] } symphonia = { version = "0.5.4", default-features = false, features = ["mp3"], optional = true } +multer = { version = "3.0.0", optional = true } [patch.crates-io] gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" } @@ -64,6 +65,7 @@ ssr = [ "tower-sessions-redis-store", "axum-login", "symphonia", + "multer", ] # Defines a size-optimized profile for the WASM bundle in release mode From 3338cc26624ff0294d01fbd7c8e9311da368e687 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 11:28:11 -0400 Subject: [PATCH 11/56] Add util module --- src/lib.rs | 2 ++ src/util/mod.rs | 0 2 files changed, 2 insertions(+) create mode 100644 src/util/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 89cd04e..e26e2c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ pub mod users; pub mod search; pub mod fileserv; pub mod error_template; +pub mod util; + use cfg_if::cfg_if; cfg_if! { diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..e69de29 From 79ba1914152f5d07cd484e777484aaa058f55fbd Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 11:28:43 -0400 Subject: [PATCH 12/56] Add function to get audio file duration --- src/util/audio.rs | 33 +++++++++++++++++++++++++++++++++ src/util/mod.rs | 7 +++++++ 2 files changed, 40 insertions(+) create mode 100644 src/util/audio.rs diff --git a/src/util/audio.rs b/src/util/audio.rs new file mode 100644 index 0000000..719052f --- /dev/null +++ b/src/util/audio.rs @@ -0,0 +1,33 @@ +use symphonia::core::formats::FormatOptions; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; +use std::fs::File; + +/// Measure the duration (in seconds) of an audio file +pub fn measure_duration(file: File) -> Result> { + let source_stream = MediaSourceStream::new(Box::new(file), Default::default()); + + let hint = Hint::new(); + let format_opts = FormatOptions::default(); + let metadata_opts = MetadataOptions::default(); + + let probe = symphonia::default::get_probe().format(&hint, source_stream, &format_opts, &metadata_opts)?; + let reader = probe.format; + + if reader.tracks().len() != 1 { + return Err(format!("Expected 1 track, found {}", reader.tracks().len()).into()) + } + + let track = &reader.tracks()[0]; + + let time_base = track.codec_params.time_base.ok_or("Missing time base")?; + let duration = track.codec_params.n_frames + .map(|frames| track.codec_params.start_ts + frames) + .ok_or("Missing number of frames")?; + + duration + .checked_mul(time_base.numer as u64) + .and_then(|v| v.checked_div(time_base.denom as u64)) + .ok_or("Overflow while computing duration".into()) +} diff --git a/src/util/mod.rs b/src/util/mod.rs index e69de29..cf7196e 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -0,0 +1,7 @@ +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + pub mod audio; + } +} From 5d7ac2bb805c27dce8491458c32796cfffa5c7c9 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 14:09:24 -0400 Subject: [PATCH 13/56] Add returning codec type to audio util function --- src/util/audio.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/util/audio.rs b/src/util/audio.rs index 719052f..ff7291f 100644 --- a/src/util/audio.rs +++ b/src/util/audio.rs @@ -1,11 +1,13 @@ +use symphonia::core::codecs::CodecType; use symphonia::core::formats::FormatOptions; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use std::fs::File; -/// Measure the duration (in seconds) of an audio file -pub fn measure_duration(file: File) -> Result> { +/// Extract the codec and duration of an audio file +/// This is combined into one function because the file object will be consumed +pub fn extract_metadata(file: File) -> Result<(CodecType, u64), Box> { let source_stream = MediaSourceStream::new(Box::new(file), Default::default()); let hint = Hint::new(); @@ -26,8 +28,10 @@ pub fn measure_duration(file: File) -> Result> { .map(|frames| track.codec_params.start_ts + frames) .ok_or("Missing number of frames")?; - duration + let duration = duration .checked_mul(time_base.numer as u64) .and_then(|v| v.checked_div(time_base.denom as u64)) - .ok_or("Overflow while computing duration".into()) + .ok_or("Overflow while computing duration")?; + + Ok((track.codec_params.codec, duration)) } From 353a8e3a9ce3303b867ca93c13cf851cafc6afae Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 14:18:48 -0400 Subject: [PATCH 14/56] Allow finding Artist and Album by primary key --- src/models.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models.rs b/src/models.rs index 15a6d12..73dbc62 100644 --- a/src/models.rs +++ b/src/models.rs @@ -44,7 +44,7 @@ pub struct User { } /// Model for an artist -#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable, Identifiable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize)] @@ -165,7 +165,7 @@ impl Artist { } /// Model for an album -#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable, Identifiable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize)] From 161ea5f9c20daf38b079dfa0b3fc0fb8dd568655 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 15:20:57 -0400 Subject: [PATCH 15/56] Implement file upload backend --- src/lib.rs | 1 + src/upload.rs | 274 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 src/upload.rs diff --git a/src/lib.rs b/src/lib.rs index e26e2c5..e7949e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ pub mod users; pub mod search; pub mod fileserv; pub mod error_template; +pub mod upload; pub mod util; use cfg_if::cfg_if; diff --git a/src/upload.rs b/src/upload.rs new file mode 100644 index 0000000..db3a3e5 --- /dev/null +++ b/src/upload.rs @@ -0,0 +1,274 @@ +use leptos::*; +use server_fn::{codec::{MultipartData, MultipartFormData}, error::NoCustomError}; +use time::Date; + +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + use multer::Field; + use crate::database::get_db_conn; + use diesel::prelude::*; + use log::*; + } +} + +/// Extract the text from a multipart field +#[cfg(feature = "ssr")] +async fn extract_field(field: Field<'static>) -> Result { + let field = match field.text().await { + Ok(field) => field, + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading field: {}", e)))?, + }; + + Ok(field) +} + +/// Validate the artist ids in a multipart field +/// Expects a field with a comma-separated list of artist ids, and ensures each is a valid artist id in the database +#[cfg(feature = "ssr")] +async fn validate_artist_ids(artist_ids: Field<'static>) -> Result, ServerFnError> { + use crate::models::Artist; + use diesel::result::Error::NotFound; + + // Extract the artist id from the field + match artist_ids.text().await { + Ok(artist_ids) => { + let artist_ids = artist_ids.split(','); + + artist_ids.map(|artist_id| { + // Parse the artist id as an integer + if let Ok(artist_id) = artist_id.parse::() { + // Check if the artist exists + let db_con = &mut get_db_conn(); + let artist = crate::schema::artists::dsl::artists.find(artist_id).first::(db_con); + + match artist { + Ok(_) => Ok(artist_id), + Err(NotFound) => Err(ServerFnError:::: + ServerError("Artist does not exist".to_string())), + Err(e) => Err(ServerFnError:::: + ServerError(format!("Error finding artist id: {}", e))), + } + } else { + Err(ServerFnError::::ServerError("Error parsing artist id".to_string())) + } + }).collect() + }, + + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading artist id: {}", e))), + } +} + +/// Validate the album id in a multipart field +/// Expects a field with an album id, and ensures it is a valid album id in the database +#[cfg(feature = "ssr")] +async fn validate_album_id(album_id: Field<'static>) -> Result { + use crate::models::Album; + use diesel::result::Error::NotFound; + + // Extract the album id from the field + match album_id.text().await { + Ok(album_id) => { + // Parse the album id as an integer + if let Ok(album_id) = album_id.parse::() { + // Check if the album exists + let db_con = &mut get_db_conn(); + let album = crate::schema::albums::dsl::albums.find(album_id).first::(db_con); + + match album { + Ok(_) => Ok(album_id), + Err(NotFound) => Err(ServerFnError:::: + ServerError("Album does not exist".to_string())), + Err(e) => Err(ServerFnError:::: + ServerError(format!("Error finding album id: {}", e))), + } + } else { + Err(ServerFnError::::ServerError("Error parsing album id".to_string())) + } + }, + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading album id: {}", e))), + } +} + +/// Validate the track number in a multipart field +/// Expects a field with a track number, and ensures it is a valid track number (non-negative integer) +#[cfg(feature = "ssr")] +async fn validate_track_number(track_number: Field<'static>) -> Result { + match track_number.text().await { + Ok(track_number) => { + if let Ok(track_number) = track_number.parse::() { + if track_number < 0 { + return Err(ServerFnError:::: + ServerError("Track number must be positive or 0".to_string())); + } else { + Ok(track_number) + } + } else { + return Err(ServerFnError::::ServerError("Error parsing track number".to_string())); + } + }, + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading track number: {}", e)))?, + } +} + +/// Validate the release date in a multipart field +/// Expects a field with a release date, and ensures it is a valid date in the format [year]-[month]-[day] +#[cfg(feature = "ssr")] +async fn validate_release_date(release_date: Field<'static>) -> Result { + match release_date.text().await { + Ok(release_date) => { + let date_format = time::macros::format_description!("[year]-[month]-[day]"); + let release_date = Date::parse(&release_date.trim(), date_format); + + match release_date { + Ok(release_date) => Ok(release_date), + Err(_) => Err(ServerFnError::::ServerError("Invalid release date".to_string())), + } + }, + Err(e) => Err(ServerFnError::::ServerError(format!("Error reading release date: {}", e))), + } +} + +/// Handle the file upload form +#[server(input = MultipartFormData, endpoint = "/upload")] +pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { + // Safe to unwrap - "On the server side, this always returns Some(_). On the client side, always returns None." + let mut data = data.into_inner().unwrap(); + + let mut title = None; + let mut artist_ids = None; + let mut album_id = None; + let mut track = None; + let mut release_date = None; + let mut file_name = None; + let mut duration = None; + + // Fetch the fields from the form data + while let Ok(Some(mut field)) = data.next_field().await { + let name = field.name().unwrap_or_default().to_string(); + + println!("Field name: {}", name); + + match name.as_str() { + "title" => { title = Some(extract_field(field).await?); }, + "artist_ids" => { artist_ids = Some(validate_artist_ids(field).await?); }, + "album_id" => { album_id = Some(validate_album_id(field).await?); }, + "track_number" => { track = Some(validate_track_number(field).await?); }, + "release_date" => { release_date = Some(validate_release_date(field).await?); }, + "file" => { + use symphonia::core::codecs::CODEC_TYPE_MP3; + use crate::util::audio::extract_metadata; + use std::fs::OpenOptions; + use std::io::{Seek, Write}; + + // Some logging is done here where there is high potential for bugs / failures, + // or behavior that we may wish to change in the future + + // Create file name + let title = title.clone().ok_or(ServerFnError:::: + ServerError("Title field required and must precede file field".to_string()))?; + + let clean_title = title.replace(" ", "_").replace("/", "_"); + let date_format = time::macros::format_description!("[year]-[month]-[day]_[hour]:[minute]:[second]"); + let date_str = time::OffsetDateTime::now_utc().format(date_format).unwrap_or_default(); + let upload_path = format!("assets/audio/upload-{}_{}.mp3", date_str, clean_title); + file_name = Some(format!("upload-{}_{}.mp3", date_str, clean_title)); + + debug!("Saving uploaded file {}", upload_path); + + // Save file to disk + // Use these open options to create the file, write to it, then read from it + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(upload_path.clone())?; + + while let Some(chunk) = field.chunk().await? { + file.write(&chunk)?; + } + + file.flush()?; + + // Rewind the file so the duration can be measured + file.rewind()?; + + // Get the codec and duration of the file + let (file_codec, file_duration) = extract_metadata(file) + .map_err(|e| { + let msg = format!("Error measuring duration of audio file {}: {}", upload_path, e); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; + + if file_codec != CODEC_TYPE_MP3 { + let msg = format!("Invalid uploaded audio file codec: {}", file_codec); + warn!("{}", msg); + return Err(ServerFnError::::ServerError(msg)); + } + + duration = Some(file_duration); + }, + _ => { + warn!("Unknown file upload field: {}", name); + } + } + } + + // Unwrap mandatory fields + let title = title.ok_or(ServerFnError::::ServerError("Missing title".to_string()))?; + let artist_ids = artist_ids.unwrap_or(vec![]); + let file_name = file_name.ok_or(ServerFnError::::ServerError("Missing file".to_string()))?; + let duration = duration.ok_or(ServerFnError::::ServerError("Missing duration".to_string()))?; + let duration = i32::try_from(duration).map_err(|e| ServerFnError:::: + ServerError(format!("Error converting duration to i32: {}", e)))?; + + // Create the song + use crate::models::Song; + let song = Song { + id: None, + title, + album_id, + track, + duration, + release_date, + storage_path: file_name, + image_path: None, + }; + + // Save the song to the database + let db_con = &mut get_db_conn(); + let song = song.insert_into(crate::schema::songs::table) + .get_result::(db_con) + .map_err(|e| { + let msg = format!("Error saving song to database: {}", e); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; + + // Save the song's artists to the database + let song_id = song.id.ok_or_else(|| { + let msg = "Error saving song to database: song id not found after insertion".to_string(); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; + + use crate::schema::song_artists; + use diesel::ExpressionMethods; + + let artist_ids = artist_ids.into_iter().map(|artist_id| { + (song_artists::song_id.eq(song_id), song_artists::artist_id.eq(artist_id)) + }).collect::>(); + + diesel::insert_into(crate::schema::song_artists::table) + .values(&artist_ids) + .execute(db_con) + .map_err(|e| { + let msg = format!("Error saving song artists to database: {}", e); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; + + Ok(()) +} From 7abfbaf60033c9092d29a4e741491d48a5e8f73f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 15:25:57 -0400 Subject: [PATCH 16/56] Fix unused imports --- src/upload.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/upload.rs b/src/upload.rs index db3a3e5..b3c48b7 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -1,6 +1,5 @@ use leptos::*; -use server_fn::{codec::{MultipartData, MultipartFormData}, error::NoCustomError}; -use time::Date; +use server_fn::codec::{MultipartData, MultipartFormData}; use cfg_if::cfg_if; @@ -10,6 +9,8 @@ cfg_if! { use crate::database::get_db_conn; use diesel::prelude::*; use log::*; + use server_fn::error::NoCustomError; + use time::Date; } } From 8a959d530d85be3024a888a18f7522fbacff100a Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 10 May 2024 15:51:26 -0400 Subject: [PATCH 17/56] Implement basic upload dialog --- src/app.rs | 6 ++- src/components.rs | 3 +- src/components/sidebar.rs | 8 +++- src/components/upload.rs | 86 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/components/upload.rs diff --git a/src/app.rs b/src/app.rs index 946b4c2..9fbbf2d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,18 +46,20 @@ use crate::components::sidebar::*; use crate::components::dashboard::*; use crate::components::search::*; use crate::components::personal::*; +use crate::components::upload::*; /// Renders the home page of your application. #[component] fn HomePage() -> impl IntoView { let play_status = PlayStatus::default(); let play_status = create_rw_signal(play_status); - + let upload_open = create_rw_signal(false); let (dashboard_open, set_dashboard_open) = create_signal(true); view! {
- + + } diff --git a/src/components.rs b/src/components.rs index 4d0c8a5..ede9b31 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,4 +1,5 @@ pub mod sidebar; pub mod dashboard; pub mod search; -pub mod personal; \ No newline at end of file +pub mod personal; +pub mod upload; diff --git a/src/components/sidebar.rs b/src/components/sidebar.rs index 805fa4e..9f8fff3 100644 --- a/src/components/sidebar.rs +++ b/src/components/sidebar.rs @@ -1,9 +1,10 @@ use leptos::leptos_dom::*; use leptos::*; use leptos_icons::*; +use crate::components::upload::*; #[component] -pub fn Sidebar(setter: WriteSignal, active: ReadSignal) -> impl IntoView { +pub fn Sidebar(setter: WriteSignal, active: ReadSignal, upload_open: RwSignal) -> impl IntoView { let open_dashboard = move |_| { setter.update(|value| *value = true); log!("open dashboard"); @@ -16,7 +17,10 @@ pub fn Sidebar(setter: WriteSignal, active: ReadSignal) -> impl Into view! {