diff --git a/MUSIC_CREDITS b/MUSIC_CREDITS new file mode 100644 index 0000000..36f60e6 --- /dev/null +++ b/MUSIC_CREDITS @@ -0,0 +1,29 @@ +All sample music in this repo has been sourced from Free Music Archive and from FreePD https://freepd.com +https://freemusicarchive.org. + +Lukewarm Banjo by Kevin MacLeod +Creative Commons CC BY 3.0 +https://creativecommons.org/licenses/by/3.0/ + +Study and Relax by Kevin MacLeod | https://incompetech.com/ +Creative Commons CC BY 3.0 +Music promoted by https://www.chosic.com/free-music/all/ + +Finally See The Light by Bryan Teoh +Creative Commons CC BY NC "This music is available for commercial and non-commercial purposes." +https://freepd.com/misc.php + +Neptunian Princess by Bryan Teoh +Creative Commons CC BY NC "This music is available for commercial and non-commercial purposes." +https://freepd.com/misc.php + + +Future Technology Corporate instrumental | Business as Usual (short 4) by Alex-Productions +Attribution-NonCommercial 4.0 International License. +https://freemusicarchive.org/music/alex-productions/corporate-instrumental-v2/future-technology-corporate-instrumental-business-as-usual-short-4/ + + +Tracks from the album "Microsong Challenge" +Licensed under: CC0 1.0 Universal +https://freemusicarchive.org/music/microSong_Challenge/2015021275957958 + diff --git a/Makefile b/Makefile index 4b23bca..74d8386 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ -.PHONY: frontend go rs clean lint +.PHONY: all frontend go rs clean lint + +all: go rs go: cd go && make pickup diff --git a/music/Alex-Productions/Business/01 - Business as Usual (short 4).mp3 b/music/Alex-Productions/Business/01 - Business as Usual (short 4).mp3 new file mode 100644 index 0000000..0bbcc62 Binary files /dev/null and b/music/Alex-Productions/Business/01 - Business as Usual (short 4).mp3 differ diff --git a/music/_Free/Bryan Teoh b/music/_Free/Bryan Teoh deleted file mode 120000 index 7c10f38..0000000 --- a/music/_Free/Bryan Teoh +++ /dev/null @@ -1 +0,0 @@ -../Bryan Teoh \ No newline at end of file diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/01 - Breakmaster Cylinder feat. Dislotec - Paint Your Grandma's Portrait.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/01 - Breakmaster Cylinder feat. Dislotec - Paint Your Grandma's Portrait.mp3 new file mode 100644 index 0000000..4879350 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/01 - Breakmaster Cylinder feat. Dislotec - Paint Your Grandma's Portrait.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/07 - Pipe Choir - Betrayal.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/07 - Pipe Choir - Betrayal.mp3 new file mode 100644 index 0000000..93c540a Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/07 - Pipe Choir - Betrayal.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/08 - Pipe Choir - Electricity.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/08 - Pipe Choir - Electricity.mp3 new file mode 100644 index 0000000..d3b3161 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/08 - Pipe Choir - Electricity.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/09 - Pipe Choir - Enya.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/09 - Pipe Choir - Enya.mp3 new file mode 100644 index 0000000..0ea4540 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/09 - Pipe Choir - Enya.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/10 - Pipe Choir - Homesick.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/10 - Pipe Choir - Homesick.mp3 new file mode 100644 index 0000000..a80f09e Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/10 - Pipe Choir - Homesick.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/11 - Pipe Choir - Infinius Fail.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/11 - Pipe Choir - Infinius Fail.mp3 new file mode 100644 index 0000000..77472ad Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/11 - Pipe Choir - Infinius Fail.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/12 - Pipe Choir - Mammitonk 1.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/12 - Pipe Choir - Mammitonk 1.mp3 new file mode 100644 index 0000000..7ed02a3 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/12 - Pipe Choir - Mammitonk 1.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/13 - Pipe Choir - Mammitonk 2.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/13 - Pipe Choir - Mammitonk 2.mp3 new file mode 100644 index 0000000..9cda2bb Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/13 - Pipe Choir - Mammitonk 2.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/14 - Pipe Choir - Mammitonk 3.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/14 - Pipe Choir - Mammitonk 3.mp3 new file mode 100644 index 0000000..5ea136c Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/14 - Pipe Choir - Mammitonk 3.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/15 - Pipe Choir - Morning Pond.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/15 - Pipe Choir - Morning Pond.mp3 new file mode 100644 index 0000000..2d01a1d Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/15 - Pipe Choir - Morning Pond.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/16 - Pipe Choir - Nebulus.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/16 - Pipe Choir - Nebulus.mp3 new file mode 100644 index 0000000..3dc104d Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/16 - Pipe Choir - Nebulus.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/17 - Pipe Choir - Nikki.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/17 - Pipe Choir - Nikki.mp3 new file mode 100644 index 0000000..0e905b1 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/17 - Pipe Choir - Nikki.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/18 - Pipe Choir - Spy Theme.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/18 - Pipe Choir - Spy Theme.mp3 new file mode 100644 index 0000000..45e9f22 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/18 - Pipe Choir - Spy Theme.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 1/19 - Pipe Choir - Static Check.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 1/19 - Pipe Choir - Static Check.mp3 new file mode 100644 index 0000000..5f747be Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 1/19 - Pipe Choir - Static Check.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/20 - Pipe Choir - Sunbeam 1.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/20 - Pipe Choir - Sunbeam 1.mp3 new file mode 100644 index 0000000..417a8af Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/20 - Pipe Choir - Sunbeam 1.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/21 - Pipe Choir - Sunbeam 2.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/21 - Pipe Choir - Sunbeam 2.mp3 new file mode 100644 index 0000000..5cd1100 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/21 - Pipe Choir - Sunbeam 2.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/22 - Pipe Choir - The Desert Archer.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/22 - Pipe Choir - The Desert Archer.mp3 new file mode 100644 index 0000000..b0c64d5 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/22 - Pipe Choir - The Desert Archer.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/23 - Pipe Choir - The Goof.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/23 - Pipe Choir - The Goof.mp3 new file mode 100644 index 0000000..799a6ea Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/23 - Pipe Choir - The Goof.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/24 - Pipe Choir - The Shimmer.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/24 - Pipe Choir - The Shimmer.mp3 new file mode 100644 index 0000000..c105f16 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/24 - Pipe Choir - The Shimmer.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/25 - Pipe Choir - The Snow Falcon.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/25 - Pipe Choir - The Snow Falcon.mp3 new file mode 100644 index 0000000..fde324a Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/25 - Pipe Choir - The Snow Falcon.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/62 - Breakmaster Cylinder feat. Dislotec - A Bird Poem.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/62 - Breakmaster Cylinder feat. Dislotec - A Bird Poem.mp3 new file mode 100644 index 0000000..85419ff Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/62 - Breakmaster Cylinder feat. Dislotec - A Bird Poem.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/63 - Breakmaster Cylinder - Basement Harp.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/63 - Breakmaster Cylinder - Basement Harp.mp3 new file mode 100644 index 0000000..2597c28 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/63 - Breakmaster Cylinder - Basement Harp.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/65 - Breakmaster Cylinder - Floorways Congealing.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/65 - Breakmaster Cylinder - Floorways Congealing.mp3 new file mode 100644 index 0000000..2271a41 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/65 - Breakmaster Cylinder - Floorways Congealing.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/67 - Breakmaster Cylinder - Stumpbox.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/67 - Breakmaster Cylinder - Stumpbox.mp3 new file mode 100644 index 0000000..d681bdf Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/67 - Breakmaster Cylinder - Stumpbox.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/68 - Breakmaster Cylinder - Zap Master.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/68 - Breakmaster Cylinder - Zap Master.mp3 new file mode 100644 index 0000000..f331fc7 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/68 - Breakmaster Cylinder - Zap Master.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/69 - Breakmaster Cylinder - Key Cards.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/69 - Breakmaster Cylinder - Key Cards.mp3 new file mode 100644 index 0000000..aa268d2 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/69 - Breakmaster Cylinder - Key Cards.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/70 - Pipe Choir - Atmospheres.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/70 - Pipe Choir - Atmospheres.mp3 new file mode 100644 index 0000000..6295eee Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/70 - Pipe Choir - Atmospheres.mp3 differ diff --git a/music/_Free/Various Artists/Microsong Challenge/Disc 2/71 - Pipe Choir - Afterlife.mp3 b/music/_Free/Various Artists/Microsong Challenge/Disc 2/71 - Pipe Choir - Afterlife.mp3 new file mode 100644 index 0000000..c5ab215 Binary files /dev/null and b/music/_Free/Various Artists/Microsong Challenge/Disc 2/71 - Pipe Choir - Afterlife.mp3 differ diff --git a/pickup.code-workspace b/pickup.code-workspace index bbb48a3..86cf42f 100644 --- a/pickup.code-workspace +++ b/pickup.code-workspace @@ -8,8 +8,10 @@ "name": "frontend" }, { - "path": "pickup-rust" + "path": "rs", + "name": "rust" }, ], - "settings": {} -} \ No newline at end of file + "settings": { + } +} diff --git a/rs/Cargo.lock b/rs/Cargo.lock index b1a9388..2dfc23e 100644 --- a/rs/Cargo.lock +++ b/rs/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "bytes", "futures-core", "futures-sink", @@ -31,7 +31,7 @@ dependencies = [ "actix-utils", "ahash", "base64", - "bitflags 2.4.2", + "bitflags 2.5.0", "brotli", "bytes", "bytestring", @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -178,7 +178,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -211,9 +211,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -240,7 +240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" dependencies = [ "alsa-sys", - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", ] @@ -316,15 +316,15 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -356,7 +356,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cexpr", "clang-sys", "itertools", @@ -367,7 +367,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -378,9 +378,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -414,9 +414,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" @@ -432,9 +432,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytestring" @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" dependencies = [ "jobserver", "libc", @@ -489,9 +489,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -511,14 +511,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -527,12 +527,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" -[[package]] -name = "claxon" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" - [[package]] name = "colorchoice" version = "1.0.0" @@ -541,9 +535,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -704,24 +698,34 @@ checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" -version = "0.10.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ + "anstream", + "anstyle", + "env_filter", "humantime", - "is-terminal", "log", - "regex", - "termcolor", ] [[package]] @@ -730,6 +734,32 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "factori" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff6b50917609e530c145de1c6aa8df9c38c40562375e8aa5eeaaf6c737a0b31" +dependencies = [ + "factori-impl", +] + +[[package]] +name = "factori-impl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6344ded92b0a4a1d90a816632f7ff2a12e01401d325d6295810dacca1dbdd6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "flate2" version = "1.0.28" @@ -765,12 +795,54 @@ dependencies = [ "winapi", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + [[package]] name = "futures-sink" version = "0.3.30" @@ -789,10 +861,15 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -816,9 +893,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -868,18 +945,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - [[package]] name = "http" version = "0.2.12" @@ -921,9 +986,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -938,17 +1003,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "is-terminal" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "itertools" version = "0.12.1" @@ -960,9 +1014,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jni" @@ -1022,17 +1076,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - [[package]] name = "libc" version = "0.2.153" @@ -1093,9 +1136,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" @@ -1136,7 +1179,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "jni-sys", "log", "ndk-sys", @@ -1183,7 +1226,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -1213,7 +1256,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -1248,15 +1291,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -1332,22 +1366,25 @@ dependencies = [ "bincode", "clap", "env_logger", + "factori", "lazy_static", "log", + "once_cell", "rand", "regex", "rodio", "serde", "serde_json", + "serial_test", "sled", "walkdir", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1393,9 +1430,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1450,9 +1487,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1473,9 +1510,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rodio" @@ -1483,10 +1520,7 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" dependencies = [ - "claxon", "cpal", - "hound", - "lewton", "symphonia", ] @@ -1526,12 +1560,27 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +dependencies = [ + "sdd", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" + [[package]] name = "semver" version = "1.0.22" @@ -1555,14 +1604,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1581,6 +1630,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot 0.12.1", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1634,9 +1708,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -1650,9 +1724,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "symphonia" @@ -1661,11 +1735,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" dependencies = [ "lazy_static", + "symphonia-bundle-flac", "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-riff", "symphonia-metadata", ] +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + [[package]] name = "symphonia-bundle-mp3" version = "0.5.4" @@ -1678,6 +1771,48 @@ dependencies = [ "symphonia-metadata", ] +[[package]] +name = "symphonia-codec-aac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + [[package]] name = "symphonia-core" version = "0.5.4" @@ -1691,6 +1826,31 @@ dependencies = [ "log", ] +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "symphonia-metadata" version = "0.5.4" @@ -1703,6 +1863,16 @@ dependencies = [ "symphonia-core", ] +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.109" @@ -1716,24 +1886,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.53" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.58" @@ -1751,14 +1912,14 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -1777,9 +1938,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -1802,9 +1963,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -1955,7 +2116,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -1989,7 +2150,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2294,32 +2455,32 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] name = "zstd" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.0.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config", diff --git a/rs/Cargo.toml b/rs/Cargo.toml index 5cbb5f0..2a454e4 100644 --- a/rs/Cargo.toml +++ b/rs/Cargo.toml @@ -12,7 +12,9 @@ bincode = "^1" clap = { version = "^4", features = ["derive"] } env_logger = "^0" log = "^0" -rodio = "^0" +rodio = { version = "^0", default-features = false, features = [ + "symphonia-all", +] } sled = "^0" serde = { version = "^1", features = ["derive"] } serde_json = "^1" @@ -20,7 +22,9 @@ walkdir = "^2" rand = "^0" regex = "^1" lazy_static = "^1" +once_cell = "1.19.0" [dev-dependencies] assert_matches = "^1" - +factori = "1.1.0" +serial_test = "3.1.1" diff --git a/rs/src/api/control.rs b/rs/src/api/control.rs index 2b4a6b3..01dafd3 100644 --- a/rs/src/api/control.rs +++ b/rs/src/api/control.rs @@ -1,12 +1,14 @@ use actix_web::{post, web, HttpResponse, Responder}; -use crate::player::commands::{PlayCommand, StopCommand, VolumeCommand}; use crate::{app_state::AppState, player::Command}; +use crate::{ + filemanager::model::Track, + player::commands::{PlayCommand, StopCommand, VolumeCommand}, +}; -#[post("/play")] -pub async fn play(data: web::Data) -> impl Responder { +fn get_first_track(app_state: &AppState) -> &Track { // TODO for now let's just look for the first track, it seems to work on our demo music - let track = data + return app_state .collection .values() .next() @@ -22,18 +24,24 @@ pub async fn play(data: web::Data) -> impl Responder { .tracks .first() .unwrap(); +} + +#[post("/play")] +pub async fn play(data: web::Data) -> impl Responder { + // TODO for now let's just look for the first track, it seems to work on our demo music + let track = get_first_track(&data); // TODO shouldn't the path be absolute or relative already? Or maybe the Player needs to know the prefix let path = format!("../music/{}", track.path.as_os_str().to_str().unwrap()); let command = Box::new(PlayCommand { file: path }) as Box; - let _ = data.sender.send(command); + let _ = data.player_sender.send(command); HttpResponse::Ok().body("ok") } #[post("/stop")] pub async fn stop(data: web::Data) -> impl Responder { let command = Box::new(StopCommand {}) as Box; - let _ = data.sender.send(command); + let _ = data.player_sender.send(command); HttpResponse::Ok().body("ok") } @@ -42,6 +50,6 @@ pub async fn volume(data: web::Data, volume: web::Path) -> impl R let command = Box::new(VolumeCommand { volume: volume.into_inner(), }) as Box; - let _ = data.sender.send(command); + let _ = data.player_sender.send(command); HttpResponse::Ok().body("ok") } diff --git a/rs/src/api/list.rs b/rs/src/api/list.rs index f2a11c8..8262790 100644 --- a/rs/src/api/list.rs +++ b/rs/src/api/list.rs @@ -4,11 +4,35 @@ use serde::Serialize; use crate::app_state::AppState; +// TODO fill this out #[derive(Debug, Serialize)] struct ApiCategory { name: String, } +#[derive(Debug, Serialize)] +pub struct ApiTrack { + id: String, + title: String, + artist: Option, + album: Option, + disc: Option, + category: String, +} + +impl ApiTrack { + pub fn from_track(track: &crate::filemanager::model::Track) -> Self { + ApiTrack { + id: track.id.clone(), + title: track.name.clone(), + artist: track.artist.clone(), + album: track.album.clone(), + disc: track.disc.clone(), + category: track.category.clone(), + } + } +} + #[derive(Debug, Serialize)] struct ListCategoriesResponse { categories: Vec, diff --git a/rs/src/api/mod.rs b/rs/src/api/mod.rs index c0f780f..4bc3883 100644 --- a/rs/src/api/mod.rs +++ b/rs/src/api/mod.rs @@ -3,3 +3,4 @@ pub use index::hello; pub mod control; pub mod list; +pub mod queue; diff --git a/rs/src/api/queue.rs b/rs/src/api/queue.rs new file mode 100644 index 0000000..6b4e658 --- /dev/null +++ b/rs/src/api/queue.rs @@ -0,0 +1,77 @@ +use actix_web::{get, http::StatusCode, post, web, Responder, Result}; +use serde::{Deserialize, Serialize}; + +use crate::error::AppError; +use crate::filemanager::dto::CollectionLocation; +use crate::{api::list::ApiTrack, app_state::AppState}; + +// TODO extract 'ApiCollectionLocaltion out of here +#[derive(Deserialize, Debug)] +struct ApiQueueInput { + category: String, + artist: Option, + album: Option, + disc: Option, + track: Option, + #[serde(default)] // Defaults to false + clear: bool, +} + +#[derive(Serialize, Debug)] +struct ApiQueueResponse { + tracks: Vec, + position: usize, +} + +#[post("/queue/add")] +pub async fn add( + data: web::Data, + input: web::Json, +) -> Result { + let collection_location = CollectionLocation { + category: input.category.clone(), + artist: input.artist.clone(), + album: input.album.clone(), + disc: input.disc.clone(), + track: input.track.clone(), + }; + + let maybe_tracks = data.collection.get_tracks_under(&collection_location); + if maybe_tracks.is_none() { + return Err(AppError::new( + "No matching music found in the collection", + StatusCode::NOT_FOUND, + )); + } + let tracks = maybe_tracks.unwrap(); + + log::info!("Adding {} track to playlist", tracks.len()); + let mut queue = data.queue.write().unwrap(); + if input.clear { + queue.clear(); + } + queue.add_tracks(tracks.into_iter().cloned().collect()); + Ok(web::Json(ApiQueueResponse { + tracks: queue.tracks.iter().map(ApiTrack::from_track).collect(), + position: queue.position, + })) +} + +#[post("/queue/clear")] +pub async fn clear(data: web::Data) -> impl Responder { + let mut queue = data.queue.write().unwrap(); + queue.clear(); + web::Json(ApiQueueResponse { + tracks: queue.tracks.iter().map(ApiTrack::from_track).collect(), + position: queue.position, + }) +} + +#[get("/queue")] +pub async fn get_queue(data: web::Data) -> impl Responder { + let queue = data.queue.read().unwrap(); + web::Json(ApiQueueResponse { + tracks: queue.tracks.iter().map(ApiTrack::from_track).collect(), + position: queue.position, + }) +} diff --git a/rs/src/app_state.rs b/rs/src/app_state.rs index 97652c5..925fd6e 100644 --- a/rs/src/app_state.rs +++ b/rs/src/app_state.rs @@ -1,9 +1,10 @@ -use std::sync::{mpsc::Sender, Arc}; +use std::sync::{mpsc::Sender, Arc, RwLock}; -use crate::{filemanager::collection::Collection, player::Command}; +use crate::{filemanager::collection::Collection, player::Command, queue::PlaybackQueue}; -#[derive(Clone)] +// #[derive(Clone)] pub struct AppState { - pub sender: Sender>, + pub player_sender: Sender>, pub collection: Arc, + pub queue: RwLock, } diff --git a/rs/src/error.rs b/rs/src/error.rs new file mode 100644 index 0000000..b3dbfc6 --- /dev/null +++ b/rs/src/error.rs @@ -0,0 +1,31 @@ +use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use std::fmt; + +#[derive(Debug)] +pub struct AppError { + pub msg: String, + pub status: StatusCode, +} + +impl AppError { + pub fn new(msg: &str, status: StatusCode) -> AppError { + AppError { + msg: msg.to_string(), + status, + } + } +} + +impl fmt::Display for AppError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl ResponseError for AppError { + // builds the actual response to send back when an error occurs + fn error_response(&self) -> HttpResponse { + let err_json = serde_json::json!({ "error": self.msg }); + HttpResponse::build(self.status).json(err_json) + } +} diff --git a/rs/src/filemanager/collection.rs b/rs/src/filemanager/collection.rs index bf52278..534d366 100644 --- a/rs/src/filemanager/collection.rs +++ b/rs/src/filemanager/collection.rs @@ -1,296 +1,344 @@ -use std::{ - borrow::Cow, - collections::{BTreeMap, VecDeque}, - path::PathBuf, +use std::collections::BTreeMap; + +use super::{ + cache, + collection_builder::build, + dto::CollectionLocation, + model::{Category, Track}, + options::CollectionOptions, }; -use lazy_static::lazy_static; -use regex::Regex; - -use super::{cache, model::Category, model::Track, options::CollectionOptions, utils::generate_id}; - -const DEFAULT_CATEGORY: &str = "Music"; -const CATEGORY_PREFIX: &str = "_"; -const CD_REGEX_STR: &str = r"(?i)(cd|dis(c|k)) ?\d"; - -pub type Collection = BTreeMap; +pub struct Collection { + pub categories: BTreeMap, +} pub fn init(options: CollectionOptions) -> std::io::Result { let files = cache::init(options)?; - Ok(build(files)) + let collection = build(files); + Ok(collection) } -struct CollectionBuilder { - collection: Collection, +impl Default for Collection { + fn default() -> Self { + Self::new() + } } -impl CollectionBuilder { +impl Collection { pub fn new() -> Self { Self { - collection: BTreeMap::new(), + categories: BTreeMap::new(), } } - fn add_category(&mut self, name: String) -> &mut Category { - if !(self.collection.contains_key(&name)) { + + // TODO: implement iterator for Collection? + // https://dev.to/wrongbyte/implementing-iterator-and-intoiterator-in-rust-3nio + pub fn values(&self) -> impl Iterator { + self.categories.values() + } + + pub fn add_category(&mut self, name: String) -> &mut Category { + if !(self.categories.contains_key(&name)) { let pretty_name = name.strip_prefix('_').unwrap_or(name.as_str()).to_string(); let category = Category { name: pretty_name, artists: BTreeMap::new(), albums: BTreeMap::new(), }; - self.collection.insert(name.clone(), category); + self.categories.insert(name.clone(), category); } - return self.collection.get_mut(&name).unwrap(); + return self.categories.get_mut(&name).unwrap(); } - fn add_track(&mut self, track: Track) { - log::info!("Adding track [{:?}] {:?}", track.category, track.path); - let category = self.add_category(track.category.clone()); - match (&track.artist, &track.album) { - (Some(artist_name), Some(album_name)) => { - let artist = category.add_artist(artist_name.clone()); - let album = artist.add_album(album_name.clone()); - match &track.disc { - Some(disc_name) => { - let disc = album.add_disc(disc_name.clone()); - disc.tracks.push(track); - } - None => { - album.tracks.push(track); - } - } - } - (Some(artist_name), None) => { - // No album - let artist = category.add_artist(artist_name.clone()); - artist.tracks.push(track); - } - (None, Some(album_name)) => { - let album = category.add_album(album_name.clone()); - match &track.disc { - Some(disc_name) => { - let disc = album.add_disc(disc_name.clone()); - disc.tracks.push(track.clone()); - } - None => { - album.tracks.push(track); - } - } - } - (None, None) => { - // No artist, no album? we don't actually handle this yet but we can swallow the error - log::error!("No artist or album for {:?}", track.path); - } + pub fn all_tracks(&self) -> Vec<&Track> { + let mut tracks = Vec::new(); + for category in self.values() { + tracks.extend(category.all_tracks()); } + tracks } -} -fn to_track(path: &PathBuf) -> Result { - let id = generate_id(); - let mut category: String = DEFAULT_CATEGORY.to_string(); - let mut disc: Option = None; - - let mut components: VecDeque> = path - .components() - .map(|c| c.as_os_str().to_string_lossy()) - .collect(); - - if components.front().is_some() && is_category(components.front()) { - category = components.pop_front().unwrap().to_string(); - } - - let stem = get_stem(path)?; - let extension = get_extentsion(path)?; - - // Pop off the name because we got it from the path buf directly - components.pop_back(); - - if is_disc(components.back()) { - disc = components.pop_back().map(|c| c.to_string()); + /** + * Return the flat list of tracks under a certain location (idemntified by catrgory, artist, album, and disc). + * If the location is not found, return None. + */ + pub fn get_tracks_under(&self, location: &CollectionLocation) -> Option> { + let maybe_category = self.categories.get(&location.category); + maybe_category?; + let category = maybe_category.unwrap(); + + let maybe_tracks = match (&location.artist, &location.album, &location.disc) { + (Some(artist), Some(album), Some(disc)) => category + .artists + .get(artist) + .and_then(|artist| artist.albums.get(album)) + .and_then(|album| album.discs.get(disc)) + .map(|disc| disc.all_tracks()), + (Some(artist), Some(album), None) => category + .artists + .get(artist) + .and_then(|artist| artist.albums.get(album)) + .map(|album| album.all_tracks()), + (Some(artist), None, _) => category + .artists + .get(artist) + .map(|artist| artist.all_tracks()), + (None, Some(album), Some(disc)) => category + .albums + .get(album) + .and_then(|album| album.discs.get(disc)) + .map(|disc| disc.all_tracks()), + (None, Some(album), None) => category.albums.get(album).map(|album| album.all_tracks()), + (None, None, _) => Some(category.all_tracks()), + }; + + if location.track.is_none() || maybe_tracks.is_none() { + return maybe_tracks; + } + return maybe_tracks.and_then(|tracks| { + tracks + .iter() + .find(|track| track.name == *location.track.as_ref().unwrap()) + .map(|track| vec![*track]) + }); } +} - let artist = components.pop_front().map(|c| c.to_string()); - let album = components.pop_front().map(|c| c.to_string()); - if !components.is_empty() { - return Err(format!( - "Had some extra path components for '{}': {components:?}", - path.to_string_lossy() - )); - } +#[cfg(test)] +mod tests { + use super::*; + use crate::filemanager::model::factories::*; - Ok(Track { - id, - name: stem, - extension, - path: path.clone(), - category, - artist, - album, - disc, - }) -} + use factori::create; -fn is_disc(dir: Option<&Cow>) -> bool { - lazy_static! { - static ref CD_REGEX: Regex = Regex::new(CD_REGEX_STR).unwrap(); - } - dir.is_some() && CD_REGEX.is_match(dir.unwrap()) -} + #[test] + fn test_add_category() { + let mut collection = Collection::new(); + let category = collection.add_category("Music".to_string()); + assert_eq!(category.name, "Music"); -fn is_category(dir: Option<&Cow>) -> bool { - dir.is_some() && dir.unwrap().starts_with(CATEGORY_PREFIX) -} + let category = collection.add_category("Cat2".to_string()); + assert_eq!(category.name, "Cat2"); -fn get_extentsion(path: &PathBuf) -> Result { - let maybe_extension = path.extension().map(|s| s.to_string_lossy()); - if maybe_extension.is_none() { - return Err(format!("Path '{path:?}' has no extension",)); - } - Ok(maybe_extension.unwrap().to_string()) -} + let category = collection.add_category("Music".to_string()); + assert_eq!(category.name, "Music"); -fn get_stem(path: &PathBuf) -> Result { - let maybe_stem = path.file_stem().map(|s| s.to_string_lossy()); - if maybe_stem.is_none() { - return Err(format!("Path '{path:?}' has no stem",)); + assert_eq!(collection.categories.len(), 2); } - Ok(maybe_stem.unwrap().to_string()) -} -/** - * TODO: convert this to taking an iterator? - */ -pub fn build(files: Vec) -> Collection { - let mut builder = CollectionBuilder::new(); - for file in files.iter() { - match to_track(file) { - Ok(track) => builder.add_track(track), - Err(err) => log::error!("{:?}", err), - } + #[test] + fn test_all_tracks() { + let collection = build_test_collection(); + + let tracks = collection.all_tracks(); + + assert_eq!(tracks.len(), 8); + let track_names = tracks + .into_iter() + .map(|track| track.name.clone()) + .collect::>(); + assert_eq!( + track_names, + vec![ + "Album One Track One", + "Album One Track Two", + "Album Two Disc 1 Track One", + "Album Two Disc 2 Track One", + "Album Four Track One", // Bare album comes before artist albums + "Album Four Track Two", + "Album Three Track One", + "Album Three Track Two", + ] + ); } - builder.collection -} - -#[cfg(test)] -mod tests { - - use super::*; #[test] - fn test_is_category() { - assert!(is_category(Some(&Cow::from("_Trance")))); - assert!(!is_category(Some(&Cow::from("Smashing Pumpkins")))); + fn test_get_tracks_under_category() { + let collection = build_test_collection(); + + let maybe_tracks = collection.get_tracks_under(&CollectionLocation { + category: "Music".to_string(), + artist: None, + album: None, + disc: None, + track: None, + }); + + assert!(maybe_tracks.is_some()); + + let track_names = maybe_tracks + .unwrap() + .into_iter() + .map(|track| track.name.clone()) + .collect::>(); + assert_eq!( + track_names, + vec![ + "Album One Track One", + "Album One Track Two", + "Album Two Disc 1 Track One", + "Album Two Disc 2 Track One", + ] + ); } #[test] - fn test_is_disc() { - let disc_strings = vec!["CD 1", "CD2", "cd2", "cd 3", "disc 1", "Disc 2", "Disk 3"]; - let non_disc_strings = vec!["C D1", "thing 1", "An Album", "Album Part 2", "CD II"]; - - for test_string in disc_strings { - assert!(is_disc(Some(&Cow::from(test_string)))); - } - for test_string in non_disc_strings { - assert!(!is_disc(Some(&Cow::from(test_string)))); - } + fn test_get_tracks_under_artist() { + let collection = build_test_collection(); + + let maybe_tracks = collection.get_tracks_under(&CollectionLocation { + category: "Music".to_string(), + artist: Some("Artist One".to_string()), + album: None, + disc: None, + track: None, + }); + + assert!(maybe_tracks.is_some()); + + let track_names = maybe_tracks + .unwrap() + .into_iter() + .map(|track| track.name.clone()) + .collect::>(); + assert_eq!( + track_names, + vec!["Album One Track One", "Album One Track Two",] + ); } #[test] - fn test_to_track_no_disc_no_category() { - let path = PathBuf::from("Smashing Pumpkins/Gish/01 I Am One.mp3"); - - let result = to_track(&path); - - assert_matches!(result, Ok(_)); - assert_track_matches( - &result.unwrap(), - Track { - id: String::from("any"), - path, - name: String::from("01 I Am One"), - extension: String::from("mp3"), - artist: Some(String::from("Smashing Pumpkins")), - album: Some(String::from("Gish")), - category: String::from(DEFAULT_CATEGORY), - disc: None, - }, + fn test_get_tracks_under_artist_album() { + let collection = build_test_collection(); + + let maybe_tracks = collection.get_tracks_under(&CollectionLocation { + category: "Music".to_string(), + artist: Some("Artist Two".to_string()), + album: Some("Album Two".to_string()), + disc: None, + track: None, + }); + + assert!(maybe_tracks.is_some()); + + let track_names = maybe_tracks + .unwrap() + .into_iter() + .map(|track| track.name.clone()) + .collect::>(); + assert_eq!( + track_names, + vec!["Album Two Disc 1 Track One", "Album Two Disc 2 Track One",] ); } #[test] - fn test_to_track_no_disc_with_category() { - let path = PathBuf::from("_Grunge/Smashing Pumpkins/Gish/01 I Am One.mp3"); - - let result = to_track(&path); - - assert_matches!(result, Ok(_)); - assert_track_matches( - &result.unwrap(), - Track { - id: String::from("any"), - path, - name: String::from("01 I Am One"), - extension: String::from("mp3"), - artist: Some(String::from("Smashing Pumpkins")), - album: Some(String::from("Gish")), - category: String::from("_Grunge"), - disc: None, - }, - ); + fn test_get_tracks_under_disc() { + let collection = build_test_collection(); + + let maybe_tracks = collection.get_tracks_under(&CollectionLocation { + category: "Music".to_string(), + artist: Some("Artist Two".to_string()), + album: Some("Album Two".to_string()), + disc: Some("Disc One".to_string()), + track: None, + }); + + assert!(maybe_tracks.is_some()); + + let track_names = maybe_tracks + .unwrap() + .into_iter() + .map(|track| track.name.clone()) + .collect::>(); + assert_eq!(track_names, vec!["Album Two Disc 1 Track One",]); } #[test] - fn test_to_track_with_disc_with_category() { - let path = PathBuf::from("_Grunge/Smashing Pumpkins/Mellon Collie and the Infinite Sadness/CD 1/01 Mellon Collie And The Infinite Sadness.mp3"); - - let result = to_track(&path); - - assert_matches!(result, Ok(_)); - assert_track_matches( - &result.unwrap(), - Track { - id: String::from("any"), - path, - name: String::from("01 Mellon Collie And The Infinite Sadness"), - extension: String::from("mp3"), - artist: Some(String::from("Smashing Pumpkins")), - album: Some(String::from("Mellon Collie and the Infinite Sadness")), - category: String::from("_Grunge"), - disc: Some(String::from("CD 1")), - }, + fn test_get_tracks_under_bare_album() { + let collection = build_test_collection(); + + let maybe_tracks = collection.get_tracks_under(&CollectionLocation { + category: "_Other".to_string(), + artist: None, + album: Some("Album Four".to_string()), + disc: None, + track: None, + }); + + assert!(maybe_tracks.is_some()); + + let track_names = maybe_tracks + .unwrap() + .into_iter() + .map(|track| track.name.clone()) + .collect::>(); + assert_eq!( + track_names, + vec!["Album Four Track One", "Album Four Track Two",] ); } #[test] - fn test_to_track_no_album_with_category() { - let path = PathBuf::from("_Grunge/Smashing Pumpkins/01 I Am One.mp3"); - - let result = to_track(&path); - - assert_matches!(result, Ok(_)); - assert_track_matches( - &result.unwrap(), - Track { - id: String::from("any"), - path, - name: String::from("01 I Am One"), - extension: String::from("mp3"), - artist: Some(String::from("Smashing Pumpkins")), - album: None, - category: String::from("_Grunge"), - disc: None, - }, - ); + fn test_get_tracks_under_track() { + let collection = build_test_collection(); + + let maybe_tracks = collection.get_tracks_under(&CollectionLocation { + category: "_Other".to_string(), + artist: None, + album: Some("Album Four".to_string()), + disc: None, + track: Some("Album Four Track One".to_string()), + }); + + assert!(maybe_tracks.is_some()); + + let track_names = maybe_tracks + .unwrap() + .into_iter() + .map(|track| track.name.clone()) + .collect::>(); + assert_eq!(track_names, vec!["Album Four Track One",]); } - // Assert that every field except for ID matches - fn assert_track_matches(a: &Track, b: Track) { - assert_eq!(a.name, b.name); - assert_eq!(a.extension, b.extension); - assert_eq!(a.path, b.path); - assert_eq!(a.album, b.album); - assert_eq!(a.artist, b.artist); - assert_eq!(a.category, b.category); - assert_eq!(a.disc, b.disc); + /** + * Build a test collection with the following types of data: + * 1. The default "Music" category containing two artists + * a) Each artist has one album. + * b) One album has two discs. + * 2. A category with a name starting with an underscore: + * a) One artist with an album + * b) One bare album (no artist) + */ + fn build_test_collection() -> Collection { + let mut collection = Collection::new(); + let category = collection.add_category("Music".to_string()); + // 1. First artist in default category + let artist = category.add_artist("Artist One".to_string()); + let album = artist.add_album("Album One".to_string()); + album.add_track(create!(Track, name: "Album One Track One".to_string())); + album.add_track(create!(Track, name: "Album One Track Two".to_string())); + + // 2. Second artist in default category + let artist = category.add_artist("Artist Two".to_string()); + let album = artist.add_album("Album Two".to_string()); + let disc = album.add_disc("Disc One".to_string()); + disc.add_track(create!(Track, name: "Album Two Disc 1 Track One".to_string())); + let disc = album.add_disc("Disc Two".to_string()); + disc.add_track(create!(Track, name: "Album Two Disc 2 Track One".to_string())); + + // 3. Artist in non-default category + let category = collection.add_category("_Other".to_string()); + let artist = category.add_artist("Artist Three".to_string()); + let album = artist.add_album("Album Three".to_string()); + album.add_track(create!(Track, name: "Album Three Track One".to_string())); + album.add_track(create!(Track, name: "Album Three Track Two".to_string())); + + // 4. Bare album in non-default category + let album = category.add_album("Album Four".to_string()); + album.add_track(create!(Track, name: "Album Four Track One".to_string())); + album.add_track(create!(Track, name: "Album Four Track Two".to_string())); + + collection } } diff --git a/rs/src/filemanager/collection_builder.rs b/rs/src/filemanager/collection_builder.rs new file mode 100644 index 0000000..7b8fd74 --- /dev/null +++ b/rs/src/filemanager/collection_builder.rs @@ -0,0 +1,280 @@ +use std::{borrow::Cow, collections::VecDeque, path::PathBuf}; + +use lazy_static::lazy_static; +use regex::Regex; + +use super::{ + collection::Collection, + model::{Category, Track}, + utils::generate_id, +}; + +const DEFAULT_CATEGORY: &str = "Music"; +const CATEGORY_PREFIX: &str = "_"; +const CD_REGEX_STR: &str = r"(?i)(cd|dis(c|k)) ?\d"; + +pub fn build(files: Vec) -> Collection { + let mut builder = CollectionBuilder::new(); + for file in files.iter() { + match to_track(file) { + Ok(track) => builder.add_track(track), + Err(err) => log::error!("{:?}", err), + } + } + builder.collection +} + +pub struct CollectionBuilder { + collection: Collection, +} + +impl CollectionBuilder { + pub fn new() -> Self { + Self { + collection: Collection::new(), + } + } + /** + * TODO: convert this to taking an iterator? + */ + fn add_category(&mut self, name: String) -> &mut Category { + self.collection.add_category(name) + } + + fn add_track(&mut self, track: Track) { + log::info!("Adding track [{:?}] {:?}", track.category, track.path); + let category = self.add_category(track.category.clone()); + match (&track.artist, &track.album) { + (Some(artist_name), Some(album_name)) => { + let artist = category.add_artist(artist_name.clone()); + let album = artist.add_album(album_name.clone()); + match &track.disc { + Some(disc_name) => { + let disc = album.add_disc(disc_name.clone()); + disc.tracks.push(track); + } + None => { + album.tracks.push(track); + } + } + } + (Some(artist_name), None) => { + // No album + let artist = category.add_artist(artist_name.clone()); + artist.tracks.push(track); + } + (None, Some(album_name)) => { + let album = category.add_album(album_name.clone()); + match &track.disc { + Some(disc_name) => { + let disc = album.add_disc(disc_name.clone()); + disc.tracks.push(track.clone()); + } + None => { + album.tracks.push(track); + } + } + } + (None, None) => { + // No artist, no album? we don't actually handle this yet but we can swallow the error + log::error!("No artist or album for {:?}", track.path); + } + } + } +} + +fn to_track(path: &PathBuf) -> Result { + let id = generate_id(); + let mut category: String = DEFAULT_CATEGORY.to_string(); + let mut disc: Option = None; + + let mut components: VecDeque> = path + .components() + .map(|c| c.as_os_str().to_string_lossy()) + .collect(); + + if components.front().is_some() && is_category(components.front()) { + category = components.pop_front().unwrap().to_string(); + } + + let stem = get_stem(path)?; + let extension = get_extentsion(path)?; + + // Pop off the name because we got it from the path buf directly + components.pop_back(); + + if is_disc(components.back()) { + disc = components.pop_back().map(|c| c.to_string()); + } + + let artist = components.pop_front().map(|c| c.to_string()); + let album = components.pop_front().map(|c| c.to_string()); + if !components.is_empty() { + return Err(format!( + "Had some extra path components for '{}': {components:?}", + path.to_string_lossy() + )); + } + + Ok(Track { + id, + name: stem, + extension, + path: path.clone(), + category, + artist, + album, + disc, + }) +} + +fn is_disc(dir: Option<&Cow>) -> bool { + lazy_static! { + static ref CD_REGEX: Regex = Regex::new(CD_REGEX_STR).unwrap(); + } + dir.is_some() && CD_REGEX.is_match(dir.unwrap()) +} + +fn is_category(dir: Option<&Cow>) -> bool { + dir.is_some() && dir.unwrap().starts_with(CATEGORY_PREFIX) +} + +fn get_extentsion(path: &PathBuf) -> Result { + let maybe_extension = path.extension().map(|s| s.to_string_lossy()); + if maybe_extension.is_none() { + return Err(format!("Path '{path:?}' has no extension",)); + } + Ok(maybe_extension.unwrap().to_string()) +} + +fn get_stem(path: &PathBuf) -> Result { + let maybe_stem = path.file_stem().map(|s| s.to_string_lossy()); + if maybe_stem.is_none() { + return Err(format!("Path '{path:?}' has no stem",)); + } + Ok(maybe_stem.unwrap().to_string()) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_is_category() { + assert!(is_category(Some(&Cow::from("_Trance")))); + assert!(!is_category(Some(&Cow::from("Smashing Pumpkins")))); + } + + #[test] + fn test_is_disc() { + let disc_strings = vec!["CD 1", "CD2", "cd2", "cd 3", "disc 1", "Disc 2", "Disk 3"]; + let non_disc_strings = vec!["C D1", "thing 1", "An Album", "Album Part 2", "CD II"]; + + for test_string in disc_strings { + assert!(is_disc(Some(&Cow::from(test_string)))); + } + for test_string in non_disc_strings { + assert!(!is_disc(Some(&Cow::from(test_string)))); + } + } + + #[test] + fn test_to_track_no_disc_no_category() { + let path = PathBuf::from("Smashing Pumpkins/Gish/01 I Am One.mp3"); + + let result = to_track(&path); + + assert_matches!(result, Ok(_)); + assert_track_matches( + &result.unwrap(), + Track { + id: String::from("any"), + path, + name: String::from("01 I Am One"), + extension: String::from("mp3"), + artist: Some(String::from("Smashing Pumpkins")), + album: Some(String::from("Gish")), + category: String::from(DEFAULT_CATEGORY), + disc: None, + }, + ); + } + + #[test] + fn test_to_track_no_disc_with_category() { + let path = PathBuf::from("_Grunge/Smashing Pumpkins/Gish/01 I Am One.mp3"); + + let result = to_track(&path); + + assert_matches!(result, Ok(_)); + assert_track_matches( + &result.unwrap(), + Track { + id: String::from("any"), + path, + name: String::from("01 I Am One"), + extension: String::from("mp3"), + artist: Some(String::from("Smashing Pumpkins")), + album: Some(String::from("Gish")), + category: String::from("_Grunge"), + disc: None, + }, + ); + } + + #[test] + fn test_to_track_with_disc_with_category() { + let path = PathBuf::from("_Grunge/Smashing Pumpkins/Mellon Collie and the Infinite Sadness/CD 1/01 Mellon Collie And The Infinite Sadness.mp3"); + + let result = to_track(&path); + + assert_matches!(result, Ok(_)); + assert_track_matches( + &result.unwrap(), + Track { + id: String::from("any"), + path, + name: String::from("01 Mellon Collie And The Infinite Sadness"), + extension: String::from("mp3"), + artist: Some(String::from("Smashing Pumpkins")), + album: Some(String::from("Mellon Collie and the Infinite Sadness")), + category: String::from("_Grunge"), + disc: Some(String::from("CD 1")), + }, + ); + } + + #[test] + fn test_to_track_no_album_with_category() { + let path = PathBuf::from("_Grunge/Smashing Pumpkins/01 I Am One.mp3"); + + let result = to_track(&path); + + assert_matches!(result, Ok(_)); + assert_track_matches( + &result.unwrap(), + Track { + id: String::from("any"), + path, + name: String::from("01 I Am One"), + extension: String::from("mp3"), + artist: Some(String::from("Smashing Pumpkins")), + album: None, + category: String::from("_Grunge"), + disc: None, + }, + ); + } + + // Assert that every field except for ID matches + fn assert_track_matches(a: &Track, b: Track) { + assert_eq!(a.name, b.name); + assert_eq!(a.extension, b.extension); + assert_eq!(a.path, b.path); + assert_eq!(a.album, b.album); + assert_eq!(a.artist, b.artist); + assert_eq!(a.category, b.category); + assert_eq!(a.disc, b.disc); + } +} diff --git a/rs/src/filemanager/dto.rs b/rs/src/filemanager/dto.rs new file mode 100644 index 0000000..f539c53 --- /dev/null +++ b/rs/src/filemanager/dto.rs @@ -0,0 +1,8 @@ +#[derive(Clone, Debug)] +pub struct CollectionLocation { + pub category: String, + pub artist: Option, + pub album: Option, + pub disc: Option, + pub track: Option, +} diff --git a/rs/src/filemanager/list.rs b/rs/src/filemanager/list.rs index 74eb529..0af301e 100644 --- a/rs/src/filemanager/list.rs +++ b/rs/src/filemanager/list.rs @@ -7,9 +7,9 @@ use super::{ pub fn list(collection_options: CollectionOptions) -> std::io::Result<()> { let collection = init(collection_options).unwrap(); - log::info!("We have got {} categories", collection.len()); + log::info!("We have got {} categories", collection.categories.len()); - for (category_name, category) in collection { + for (category_name, category) in collection.categories { log::info!("[cat]{category_name}"); list_category(&category); } @@ -41,6 +41,10 @@ fn list_album(album: &Album, indent: usize) { let space = " ".repeat(indent); log::info!("{space}[al]{}", album.name); + for disc in album.discs.values() { + list_album(disc, indent + 2); + } + for track in &album.tracks { log::info!("{space} [tr]{}", track.name); } diff --git a/rs/src/filemanager/mod.rs b/rs/src/filemanager/mod.rs index 8bf9e4f..6c7bda0 100644 --- a/rs/src/filemanager/mod.rs +++ b/rs/src/filemanager/mod.rs @@ -1,5 +1,7 @@ pub mod cache; pub mod collection; +mod collection_builder; +pub mod dto; pub mod list; pub mod model; pub mod options; diff --git a/rs/src/filemanager/model.rs b/rs/src/filemanager/model.rs index d9983c1..bf4533c 100644 --- a/rs/src/filemanager/model.rs +++ b/rs/src/filemanager/model.rs @@ -30,6 +30,10 @@ impl Album { } return self.discs.get_mut(&name).unwrap(); } + + pub fn add_track(&mut self, track: Track) { + self.tracks.push(track); + } } pub struct Artist { @@ -82,4 +86,73 @@ impl Category { } return self.albums.get_mut(&name).unwrap(); } + + pub fn all_tracks(&self) -> Vec<&Track> { + let mut tracks = Vec::new(); + for album in self.albums.values() { + tracks.extend(album.all_tracks()); + } + for artist in self.artists.values() { + tracks.extend(artist.all_tracks()); + } + tracks + } +} + +impl Artist { + /** + * Return all the tracks under this artist in a flat list. + */ + pub fn all_tracks(&self) -> Vec<&Track> { + let mut tracks = Vec::new(); + for album in self.albums.values() { + tracks.extend(album.all_tracks()); + } + tracks.extend(self.tracks.iter()); + tracks + } +} + +impl Album { + /** + * Return all the tracks under this album in a flat list. + */ + pub fn all_tracks(&self) -> Vec<&Track> { + let mut tracks = Vec::new(); + for disc in self.discs.values() { + tracks.extend(disc.all_tracks()); + } + tracks.extend(self.tracks.iter()); + tracks + } +} + +#[cfg(test)] +pub mod factories { + use super::Track; + use factori::factori; + use rand::distributions::Alphanumeric; + use rand::{thread_rng, Rng}; + use std::path::PathBuf; + + factori!(Track, { + default { + id = random_string(), + name = random_string(), + extension = "mp3".to_string(), + path = PathBuf::from(random_string()), + category = random_string(), + artist = Some(random_string()), + album = Some(random_string()), + disc = None, + } + }); + + pub fn random_string() -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(30) + .map(char::from) + .collect() + } } diff --git a/rs/src/lib.rs b/rs/src/lib.rs index 30c586d..6da4863 100644 --- a/rs/src/lib.rs +++ b/rs/src/lib.rs @@ -1,11 +1,13 @@ pub mod api; pub mod app_state; +pub mod error; pub mod filemanager; pub mod player; +pub mod queue; use std::sync::mpsc; use std::sync::mpsc::Sender; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use std::thread; use actix_web::{ @@ -37,12 +39,14 @@ pub struct ServeOptions { * Create the app state we need to pass into the app. */ pub fn build_app_state(options: &ServeOptions) -> AppState { - let sender = spawn_player(); + let player_sender = spawn_player(); let collection = collection::init(options.collection_options.clone()).unwrap(); let collection_arc = Arc::new(collection); + let queue = queue::PlaybackQueue::new(); AppState { - sender, + player_sender, collection: collection_arc, + queue: RwLock::new(queue), } } @@ -51,7 +55,7 @@ pub fn build_app_state(options: &ServeOptions) -> AppState { * https://github.com/actix/actix-web/blob/b1c85ba85be91b5ea34f31264853b411fadce1ef/actix-web/src/app.rs#L698 */ pub fn build_app( - app_state: AppState, + app_state: Data, ) -> App< impl ServiceFactory< ServiceRequest, @@ -62,19 +66,21 @@ pub fn build_app( >, > { App::new() - .app_data(Data::new(app_state)) + .app_data(app_state) .wrap(Logger::default()) .service(api::hello) .service(api::control::play) .service(api::control::stop) .service(api::control::volume) .service(api::list::list_categories) + .service(api::queue::add) + .service(api::queue::clear) + .service(api::queue::get_queue) } pub async fn serve(options: ServeOptions) -> std::io::Result<()> { - let app_state = build_app_state(&options); - let address = format!("0.0.0.0:{}", options.port); + let app_state = Data::new(build_app_state(&options)); log::info!("Starting on http://{}", address); HttpServer::new(move || build_app(app_state.clone())) .workers(2) diff --git a/rs/src/player/mod.rs b/rs/src/player/mod.rs index 755c272..749e605 100644 --- a/rs/src/player/mod.rs +++ b/rs/src/player/mod.rs @@ -44,11 +44,23 @@ impl Player { self.sink = sink; // TODO handle missing file error - don't stop the playing until we have a good file - let file = BufReader::new(File::open(path).unwrap()); + let file = BufReader::new(File::open(path.clone()).unwrap()); // Decode that sound file into a source // TODO handle error let source = Decoder::new(file).unwrap(); self.sink.append(source); + + // TODO handle how to trigger the next song in the playlist when the current song is finished. + } + + pub fn status(&self) -> usize { + let len = self.sink.len(); + log::info!( + "Status: {} tracks in the sink queue. paused={}", + len, + self.sink.is_paused() + ); + len } pub fn stop(&mut self) { diff --git a/rs/src/queue/mod.rs b/rs/src/queue/mod.rs new file mode 100644 index 0000000..9de4d98 --- /dev/null +++ b/rs/src/queue/mod.rs @@ -0,0 +1,46 @@ +use crate::filemanager::model::Track; + +pub struct PlaybackQueue { + pub tracks: Vec, + pub position: usize, +} + +impl Default for PlaybackQueue { + fn default() -> Self { + Self::new() + } +} + +/** + * PlaybackQueue is a struct that holds a list of tracks to be played on the server (in jukebox mode). + */ +impl PlaybackQueue { + pub fn new() -> PlaybackQueue { + PlaybackQueue { + tracks: vec![], + position: 0, + } + } + + pub fn add_track(&mut self, track: Track) { + self.tracks.push(track); + } + + pub fn add_tracks(&mut self, tracks: Vec) { + self.tracks.extend(tracks); + } + + pub fn clear(&mut self) { + self.tracks.clear(); + } + + pub fn pop(&mut self) { + if !self.tracks.is_empty() { + self.tracks.remove(0); + } + } + + pub fn print_tracks(&self) { + log::info!("{:?}", self.tracks); + } +} diff --git a/rs/tests/filemanager.rs b/rs/tests/filemanager.rs index b1145f7..3dad31b 100644 --- a/rs/tests/filemanager.rs +++ b/rs/tests/filemanager.rs @@ -1,10 +1,10 @@ -use ::assert_matches::assert_matches; +use assert_matches::assert_matches; use pickup::filemanager::{ cache::{init, refresh}, options::CollectionOptions, }; -const NUM_FILES: usize = 6; +const NUM_FILES: usize = 33; #[test] fn test_refresh_and_load() { diff --git a/rs/tests/helpers.rs b/rs/tests/helpers/mod.rs similarity index 58% rename from rs/tests/helpers.rs rename to rs/tests/helpers/mod.rs index 510ec7e..28e7d0c 100644 --- a/rs/tests/helpers.rs +++ b/rs/tests/helpers/mod.rs @@ -1,10 +1,15 @@ use actix_web::{ body::MessageBody, dev::{ServiceFactory, ServiceRequest, ServiceResponse}, + web::Data, App, Error, }; -use pickup::{build_app, build_app_state, filemanager::options::CollectionOptions, ServeOptions}; +use pickup::{ + app_state::AppState, build_app, build_app_state, filemanager::options::CollectionOptions, + ServeOptions, +}; +#[allow(dead_code)] // Used in tests, but triggers dead_code on test binaries that don't use it. pub fn build_test_app() -> App< impl ServiceFactory< ServiceRequest, @@ -14,6 +19,10 @@ pub fn build_test_app() -> App< Error = Error, >, > { + build_app(build_test_app_state()) +} + +pub fn build_test_app_state() -> Data { let options = ServeOptions { collection_options: CollectionOptions { dir: String::from("../music"), @@ -22,6 +31,5 @@ pub fn build_test_app() -> App< port: 3001, }; - let app_state = build_app_state(&options); - return build_app(app_state); + Data::new(build_app_state(&options)) } diff --git a/rs/tests/queue_e2e.rs b/rs/tests/queue_e2e.rs new file mode 100644 index 0000000..4d5a8eb --- /dev/null +++ b/rs/tests/queue_e2e.rs @@ -0,0 +1,162 @@ +use actix_web::{ + test::{self, read_body_json}, + web::Data, +}; + +use serial_test::serial; + +mod helpers; + +use helpers::build_test_app_state; +use once_cell::sync::Lazy; +use pickup::{app_state::AppState, build_app}; +use serde_json::{self, json}; + +// Make a shared app state for use in all tests, because Sled won't let us open multiple instances of the DB file +// from differet threads. +static APP_STATE: Lazy> = Lazy::new(build_test_app_state); + +pub async fn clear_playlist() { + let service = test::init_service(build_app((*APP_STATE).clone())).await; + let req = test::TestRequest::post().uri("/queue/clear").to_request(); + + let resp: serde_json::Value = test::call_and_read_body_json(&service, req).await; + + assert_eq!(resp.get("position").unwrap().as_u64().unwrap(), 0); + assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 0); +} + +#[serial(queue)] +#[actix_web::test] +async fn test_not_found() { + let app = test::init_service(build_app((*APP_STATE).clone())).await; + clear_playlist().await; + let req = test::TestRequest::post() + .uri("/queue/add") + .set_json(json!({"category": "DoesNotExist"})) + .to_request(); + + let resp = test::call_service(&app, req).await; + + assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND); + let resp_json: serde_json::Value = read_body_json(resp).await; + assert_eq!( + resp_json, + json!({ "error": "No matching music found in the collection" }) + ); +} + +#[serial(queue)] +#[actix_web::test] +async fn test_add_category() { + let app = test::init_service(build_app((*APP_STATE).clone())).await; + clear_playlist().await; + let req = test::TestRequest::post() + .uri("/queue/add") + .set_json(json!({ + "category": "Music", + })) + .to_request(); + + let resp: serde_json::Value = test::call_and_read_body_json(&app, req).await; + + assert_eq!(resp.get("position").unwrap().as_u64().unwrap(), 0); + assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 5); + + // Now if we re-add the category, we should get 5 more tracks + let req = test::TestRequest::post() + .uri("/queue/add") + .set_json(json!({ + "category": "Music", + })) + .to_request(); + + let resp: serde_json::Value = test::call_and_read_body_json(&app, req).await; + assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 10); +} + +#[serial(queue)] +#[actix_web::test] +async fn test_add_artist() { + let app = test::init_service(build_app((*APP_STATE).clone())).await; + clear_playlist().await; + let req = test::TestRequest::post() + .uri("/queue/add") + .set_json(json!({ + "category": "Music", + "artist": "Alex-Productions", + })) + .to_request(); + + let resp: serde_json::Value = test::call_and_read_body_json(&app, req).await; + + assert_eq!(resp.get("error"), None); + assert_eq!(resp.get("position").unwrap().as_u64().unwrap(), 0); + assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 1); +} + +#[serial(queue)] +#[actix_web::test] +async fn test_add_album() { + let app = test::init_service(build_app((*APP_STATE).clone())).await; + clear_playlist().await; + let req = test::TestRequest::post() + .uri("/queue/add") + .set_json(json!({ + "category": "Music", + "artist": "Bryan Teoh", + "album": "Free", + })) + .to_request(); + + let resp: serde_json::Value = test::call_and_read_body_json(&app, req).await; + + assert_eq!(resp.get("position").unwrap().as_u64().unwrap(), 0); + assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 2); +} + +#[serial(queue)] +#[actix_web::test] +async fn test_add_track() { + let app = test::init_service(build_app((*APP_STATE).clone())).await; + clear_playlist().await; + let req = test::TestRequest::post() + .uri("/queue/add") + .set_json(json!({ + "category": "Music", + "artist": "Bryan Teoh", + "album": "Free", + "track": "01 - Finally See The Light", + })) + .to_request(); + + let resp: serde_json::Value = test::call_and_read_body_json(&app, req).await; + + assert_eq!(resp.get("position").unwrap().as_u64().unwrap(), 0); + assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 1); +} + +#[serial(queue)] +#[actix_web::test] +async fn test_clear() { + let app = test::init_service(build_app((*APP_STATE).clone())).await; + clear_playlist().await; + let req = test::TestRequest::post() + .uri("/queue/add") + .set_json(json!({ + "category": "Music", + })) + .to_request(); + + let resp: serde_json::Value = test::call_and_read_body_json(&app, req).await; + + assert_eq!(resp.get("position").unwrap().as_u64().unwrap(), 0); + assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 5); + + let req = test::TestRequest::post().uri("/queue/clear").to_request(); + + let resp: serde_json::Value = test::call_and_read_body_json(&app, req).await; + + assert_eq!(resp.get("position").unwrap().as_u64().unwrap(), 0); + assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 0); +}