From dbea13e676b9f743e3f77a70acb6043832671c37 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 3 Dec 2021 16:13:19 -0500 Subject: [PATCH] Create initial language functionality and framework --- .gitignore | 5 + Cargo.lock | 1248 ++++++++++++++++++++ Cargo.toml | 17 + mps-interpreter/Cargo.toml | 15 + mps-interpreter/src/context.rs | 29 + mps-interpreter/src/interpretor.rs | 140 +++ mps-interpreter/src/lang/db_items.rs | 287 +++++ mps-interpreter/src/lang/dictionary.rs | 42 + mps-interpreter/src/lang/error.rs | 41 + mps-interpreter/src/lang/mod.rs | 29 + mps-interpreter/src/lang/operation.rs | 31 + mps-interpreter/src/lang/sql_query.rs | 166 +++ mps-interpreter/src/lang/statement.rs | 40 + mps-interpreter/src/lang/utility.rs | 41 + mps-interpreter/src/lib.rs | 16 + mps-interpreter/src/music/build_library.rs | 9 + mps-interpreter/src/music/library.rs | 156 +++ mps-interpreter/src/music/mod.rs | 6 + mps-interpreter/src/music/tag.rs | 145 +++ mps-interpreter/src/music_item.rs | 17 + mps-interpreter/src/runner.rs | 70 ++ mps-interpreter/src/tokens/error.rs | 39 + mps-interpreter/src/tokens/mod.rs | 7 + mps-interpreter/src/tokens/token_enum.rs | 59 + mps-interpreter/src/tokens/tokenizer.rs | 249 ++++ mps-interpreter/tests/music_lib.rs | 11 + mps-interpreter/tests/single_line.rs | 57 + mps-player/Cargo.toml | 11 + mps-player/src/errors.rs | 20 + mps-player/src/lib.rs | 8 + mps-player/src/player.rs | 135 +++ mps-player/src/utility.rs | 15 + src/main.rs | 15 + 33 files changed, 3176 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 mps-interpreter/Cargo.toml create mode 100644 mps-interpreter/src/context.rs create mode 100644 mps-interpreter/src/interpretor.rs create mode 100644 mps-interpreter/src/lang/db_items.rs create mode 100644 mps-interpreter/src/lang/dictionary.rs create mode 100644 mps-interpreter/src/lang/error.rs create mode 100644 mps-interpreter/src/lang/mod.rs create mode 100644 mps-interpreter/src/lang/operation.rs create mode 100644 mps-interpreter/src/lang/sql_query.rs create mode 100644 mps-interpreter/src/lang/statement.rs create mode 100644 mps-interpreter/src/lang/utility.rs create mode 100644 mps-interpreter/src/lib.rs create mode 100644 mps-interpreter/src/music/build_library.rs create mode 100644 mps-interpreter/src/music/library.rs create mode 100644 mps-interpreter/src/music/mod.rs create mode 100644 mps-interpreter/src/music/tag.rs create mode 100644 mps-interpreter/src/music_item.rs create mode 100644 mps-interpreter/src/runner.rs create mode 100644 mps-interpreter/src/tokens/error.rs create mode 100644 mps-interpreter/src/tokens/mod.rs create mode 100644 mps-interpreter/src/tokens/token_enum.rs create mode 100644 mps-interpreter/src/tokens/tokenizer.rs create mode 100644 mps-interpreter/tests/music_lib.rs create mode 100644 mps-interpreter/tests/single_line.rs create mode 100644 mps-player/Cargo.toml create mode 100644 mps-player/src/errors.rs create mode 100644 mps-player/src/lib.rs create mode 100644 mps-player/src/player.rs create mode 100644 mps-player/src/utility.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6ce6db --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +**/target +/*/metadata.mps.sqlite +metadata.mps.sqlite +**.m3u8 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..adf1fcd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1248 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "alsa" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" + +[[package]] +name = "bytemuck" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom 5.1.2", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + +[[package]] +name = "combine" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "coreaudio-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" +dependencies = [ + "bitflags", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "jni", + "js-sys", + "lazy_static", + "libc", + "mach", + "ndk 0.3.0", + "ndk-glue 0.3.0", + "nix", + "oboe", + "parking_lot", + "stdweb", + "thiserror", + "web-sys", + "winapi", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "encoding_rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "hound" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +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.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" + +[[package]] +name = "libloading" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd5850c449b40bacb498b2bbdfaff648b1b055630073ba8db499caf2d0ea9f2" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "m3u8-rs" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50fe05791a7f418b59d6cddebdc293d77c9c1f652adbff855c071d4507cd883b" +dependencies = [ + "nom 7.1.0", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "minimp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" +dependencies = [ + "minimp3-sys", + "slice-deque", + "thiserror", +] + +[[package]] +name = "minimp3-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" +dependencies = [ + "cc", +] + +[[package]] +name = "mps" +version = "0.1.0" +dependencies = [ + "mps-interpreter", + "mps-player", +] + +[[package]] +name = "mps-interpreter" +version = "0.1.0" +dependencies = [ + "dirs", + "rusqlite", + "symphonia", +] + +[[package]] +name = "mps-player" +version = "0.1.0" +dependencies = [ + "m3u8-rs", + "mps-interpreter", + "rodio", +] + +[[package]] +name = "ndk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.3.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-glue" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.4.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +dependencies = [ + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "oboe" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" +dependencies = [ + "jni", + "ndk 0.4.0", + "ndk-glue 0.4.0", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc" +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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pkg-config" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rodio" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "minimp3", +] + +[[package]] +name = "rusqlite" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a82b0b91fad72160c56bf8da7a549b25d7c31109f52cc1437eac4c0ad2550a7" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "slice-deque" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" +dependencies = [ + "libc", + "mach", + "winapi", +] + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "symphonia" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e5f38aa07e792f4eebb0faa93cee088ec82c48222dd332897aae1569d9a4b7" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-ogg", + "symphonia-format-wav", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "116e5412f5fb4e5d07efd6628d50d6fcd7a61ebef43d98f5012f3cf763b25d02" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4d97c4a61ece4651751dddb393ebecb7579169d9e758ae808fe507a5250790" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd3d7ab37eb9b7df16ddedd7adb7cc382afe708ff078e525a14dc9b05e57558f" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1d54738758993546107e3a4be2c1da827f2d4489fcffee0fa47867254e44c7" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a29ed6748078effb35a05064a451493a78038918981dc1a76bdf5a2752d441fa" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa135e97be0f4a666c31dfe5ef4c75435ba3d355fd6a73d2100aa79b14c104c9" +dependencies = [ + "arrayvec", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feee3a7711e7ec1b7540756f3868bbb3cbb0d1195569b9bc26471a24a02f57b5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b2357288a79adfec532cfd86049696cfa5c58efeff83bd51687a528f18a519" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-wav" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3586e944a951f3ff19ae14d3f46643c063784f119bffb091fc536102909575" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5260599daba18d8fe905ca3eb3b42ba210529a6276886632412cc74984e79b1a" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37026c6948ff842e0bf94b4008579cc71ab16ed0ff9ca70a331f60f4f1e1e9" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..541f4a3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mps" +version = "0.1.0" +edition = "2021" +authors = ["NGnius (Graham) "] +description = "Music Playlist Scripting language (MPS)" + +[workspace] +members = [ + "mps-interpreter", + "mps-player" +] + +[dependencies] +# local +mps-interpreter = { path = "./mps-interpreter" } +mps-player = { path = "./mps-player" } diff --git a/mps-interpreter/Cargo.toml b/mps-interpreter/Cargo.toml new file mode 100644 index 0000000..f5def9b --- /dev/null +++ b/mps-interpreter/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "mps-interpreter" +version = "0.1.0" +edition = "2021" + +[dependencies] +rusqlite = { version = "0.26.1" } +symphonia = { version = "0.4.0", optional = true, features = [ + "aac", "flac", "mp3", "pcm", "vorbis", "isomp4", "ogg", "wav" +] } +dirs = { version = "4.0.0", optional = true} + +[features] +default = [ "music_library" ] +music_library = [ "symphonia", "dirs" ] # song metadata parsing and database auto-population diff --git a/mps-interpreter/src/context.rs b/mps-interpreter/src/context.rs new file mode 100644 index 0000000..9b2f38b --- /dev/null +++ b/mps-interpreter/src/context.rs @@ -0,0 +1,29 @@ +use std::fmt::{Debug, Display, Formatter, Error}; + +#[derive(Debug)] +pub struct MpsContext { + pub sqlite_connection: Option, +} + +impl Default for MpsContext { + fn default() -> Self { + Self { + sqlite_connection: None, // initialized by first SQL statement instead + } + } +} + +impl std::clone::Clone for MpsContext { + fn clone(&self) -> Self { + Self { + sqlite_connection: None, + } + } +} + +impl Display for MpsContext { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "MpsContext")?; + Ok(()) + } +} diff --git a/mps-interpreter/src/interpretor.rs b/mps-interpreter/src/interpretor.rs new file mode 100644 index 0000000..da9131e --- /dev/null +++ b/mps-interpreter/src/interpretor.rs @@ -0,0 +1,140 @@ +use std::iter::Iterator; +use std::collections::VecDeque; +use std::path::Path; +use std::fs::File; + +use super::MpsMusicItem; +use super::MpsContext; +use super::tokens::MpsToken; +use super::lang::{MpsOp, MpsLanguageError, MpsLanguageDictionary}; + +pub struct MpsInterpretor where T: crate::tokens::MpsTokenReader { + tokenizer: T, + buffer: VecDeque, + current_stmt: Option>, + vocabulary: MpsLanguageDictionary, + context: Option, +} + +pub fn interpretor(stream: R) -> MpsInterpretor> { + let tokenizer = crate::tokens::MpsTokenizer::new(stream); + MpsInterpretor::with_standard_vocab(tokenizer) +} + +impl MpsInterpretor + where T: crate::tokens::MpsTokenReader +{ + pub fn with_vocab(tokenizer: T, vocab: MpsLanguageDictionary) -> Self { + Self { + tokenizer: tokenizer, + buffer: VecDeque::new(), + current_stmt: None, + vocabulary: vocab, + context: None, + } + } + + pub fn with_standard_vocab(tokenizer: T) -> Self { + let mut result = Self { + tokenizer: tokenizer, + buffer: VecDeque::new(), + current_stmt: None, + vocabulary: MpsLanguageDictionary::default(), + context: None, + }; + standard_vocab(&mut result.vocabulary); + result + } + + pub fn context(&mut self, ctx: MpsContext) { + self.context = Some(ctx) + } + + pub fn is_done(&self) -> bool { + self.tokenizer.end_of_file() && self.current_stmt.is_none() + } +} + +impl MpsInterpretor> { + pub fn standard_file>(path: P) -> std::io::Result { + let file = File::open(path)?; + let tokenizer = crate::tokens::MpsTokenizer::new(file); + let mut result = Self { + tokenizer: tokenizer, + buffer: VecDeque::new(), + current_stmt: None, + vocabulary: MpsLanguageDictionary::default(), + context: None, + }; + standard_vocab(&mut result.vocabulary); + Ok(result) + } +} + +impl Iterator for MpsInterpretor + where T: crate::tokens::MpsTokenReader +{ + type Item = Result>; + + fn next(&mut self) -> Option { + let mut is_stmt_done = false; + let result = if let Some(stmt) = &mut self.current_stmt { + let next_item = stmt.next(); + if next_item.is_none() { + is_stmt_done = true; + } + match next_item { + Some(item) => Some(item.map_err(|e| box_error_with_ctx( + e, self.tokenizer.current_line() + ))), + None => None + } + } else { + if self.tokenizer.end_of_file() { return None; } + // build new statement + let token_result = self.tokenizer.next_statements(1, &mut self.buffer) + .map_err(|e| box_error_with_ctx(e, self.tokenizer.current_line())); + match token_result { + Ok(_) => {}, + Err(x) => return Some(Err(x)) + } + if self.tokenizer.end_of_file() && self.buffer.len() == 0 { return None; } + let stmt = self.vocabulary.try_build_statement(&mut self.buffer); + match stmt { + Ok(mut stmt) => { + stmt.enter(self.context.take().unwrap_or_else(|| MpsContext::default())); + self.current_stmt = Some(stmt); + let next_item = self.current_stmt.as_mut().unwrap().next(); + if next_item.is_none() { + is_stmt_done = true; + } + match next_item { + Some(item) => Some(item.map_err(|e| box_error_with_ctx( + e, + self.tokenizer.current_line() + ))), + None => None + } + }, + Err(e) => Some(Err(e).map_err(|e| box_error_with_ctx( + e, + self.tokenizer.current_line() + ))) + } + }; + if is_stmt_done { + self.context = Some(self.current_stmt.take().unwrap().escape()); + } + result + } +} + +fn box_error_with_ctx(mut error: E, line: usize) -> Box { + error.set_line(line); + Box::new(error) as Box +} + +pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) { + vocabulary + .add(crate::lang::vocabulary::SqlStatementFactory); +} diff --git a/mps-interpreter/src/lang/db_items.rs b/mps-interpreter/src/lang/db_items.rs new file mode 100644 index 0000000..c24b602 --- /dev/null +++ b/mps-interpreter/src/lang/db_items.rs @@ -0,0 +1,287 @@ +pub const DEFAULT_SQLITE_FILEPATH: &str = "metadata.mps.sqlite"; + +pub trait DatabaseObj: Sized { + fn map_row(row: &rusqlite::Row) -> rusqlite::Result; + + fn to_params(&self) -> Vec<&'_ dyn rusqlite::ToSql>; + + fn id(&self) -> u64; +} + +pub fn generate_default_db() -> rusqlite::Result { + let db_exists = std::path::Path::new(DEFAULT_SQLITE_FILEPATH).exists(); + let conn = rusqlite::Connection::open(DEFAULT_SQLITE_FILEPATH)?; + // skip db building if SQLite file already exists + // TODO do a more exhaustive db check to make sure it's actually the correct file + if db_exists {return Ok(conn);} + // build db tables + conn.execute_batch( + "BEGIN; + CREATE TABLE IF NOT EXISTS songs ( + song_id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + artist INTEGER NOT NULL, + album INTEGER, + filename TEXT, + metadata INTEGER NOT NULL, + genre INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS artists ( + artist_id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + genre INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS albums ( + album_id INTEGER NOT NULL PRIMARY KEY, + title TEXT, + metadata INTEGER NOT NULL, + artist INTEGER NOT NULL, + genre INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS metadata ( + meta_id INTEGER NOT NULL PRIMARY KEY, + plays INTEGER NOT NULL DEFAULT 0, + track INTEGER NOT NULL DEFAULT 1, + disc INTEGER NOT NULL DEFAULT 1, + duration INTEGER, + date INTEGER + ); + CREATE TABLE IF NOT EXISTS genres ( + genre_id INTEGER NOT NULL PRIMARY KEY, + title TEXT + ); + COMMIT;" + )?; + // generate data and store in db + #[cfg(feature = "music_library")] + { + let music_path = super::utility::music_folder(); + match crate::music::build_library(&music_path) { + Ok(lib) => { + let mut song_insert = conn.prepare( + "INSERT OR REPLACE INTO songs ( + song_id, + title, + artist, + album, + filename, + metadata, + genre + ) VALUES (?, ?, ?, ?, ?, ?, ?)" + )?; + for song in lib.all_songs() { + song_insert.execute(song.to_params().as_slice())?; + } + + let mut metadata_insert = conn.prepare( + "INSERT OR REPLACE INTO metadata ( + meta_id, + plays, + track, + disc, + duration, + date + ) VALUES (?, ?, ?, ?, ?, ?)" + )?; + for meta in lib.all_metadata() { + metadata_insert.execute(meta.to_params().as_slice())?; + } + + let mut artist_insert = conn.prepare( + "INSERT OR REPLACE INTO artists ( + artist_id, + name, + genre + ) VALUES (?, ?, ?)" + )?; + for artist in lib.all_artists() { + artist_insert.execute(artist.to_params().as_slice())?; + } + + let mut album_insert = conn.prepare( + "INSERT OR REPLACE INTO albums ( + album_id, + title, + metadata, + artist, + genre + ) VALUES (?, ?, ?, ?, ?)" + )?; + for album in lib.all_albums() { + album_insert.execute(album.to_params().as_slice())?; + } + + let mut genre_insert = conn.prepare( + "INSERT OR REPLACE INTO genres ( + genre_id, + title + ) VALUES (?, ?)" + )?; + for genre in lib.all_genres() { + genre_insert.execute(genre.to_params().as_slice())?; + } + }, + Err(e) => println!("Unable to load music from {}: {}", music_path.display(), e) + } + } + Ok(conn) +} + +#[derive(Clone, Debug)] +pub struct DbMusicItem { + pub song_id: u64, + pub title: String, + pub artist: u64, + pub album: Option, + pub filename: String, + pub metadata: u64, + pub genre: u64, +} + +impl DatabaseObj for DbMusicItem { + fn map_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(Self{ + song_id: row.get(0)?, + title: row.get(1)?, + artist: row.get(2)?, + album: row.get(3)?, + filename: row.get(4)?, + metadata: row.get(5)?, + genre: row.get(6)?, + }) + } + + fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { + vec![ + &self.song_id, + &self.title, + &self.artist, + &self.album, + &self.filename, + &self.metadata, + &self.genre, + ] + } + + fn id(&self) -> u64 {self.song_id} +} + +#[derive(Clone, Debug)] +pub struct DbMetaItem { + pub meta_id: u64, + pub plays: u64, + pub track: u64, + pub disc: u64, + pub duration: u64, // seconds + pub date: u64, // year +} + +impl DatabaseObj for DbMetaItem { + fn map_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(Self{ + meta_id: row.get(0)?, + plays: row.get(1)?, + track: row.get(2)?, + disc: row.get(3)?, + duration: row.get(4)?, + date: row.get(5)?, + }) + } + + fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { + vec![ + &self.meta_id, + &self.plays, + &self.track, + &self.disc, + &self.duration, + &self.date, + ] + } + + fn id(&self) -> u64 {self.meta_id} +} + +#[derive(Clone, Debug)] +pub struct DbArtistItem { + pub artist_id: u64, + pub name: String, + pub genre: u64, +} + +impl DatabaseObj for DbArtistItem { + fn map_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(Self{ + artist_id: row.get(0)?, + name: row.get(1)?, + genre: row.get(2)?, + }) + } + + fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { + vec![ + &self.artist_id, + &self.name, + &self.genre, + ] + } + + fn id(&self) -> u64 {self.artist_id} +} + +#[derive(Clone, Debug)] +pub struct DbAlbumItem { + pub album_id: u64, + pub title: String, + pub metadata: u64, + pub artist: u64, + pub genre: u64, +} + +impl DatabaseObj for DbAlbumItem { + fn map_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(Self{ + album_id: row.get(0)?, + title: row.get(1)?, + metadata: row.get(2)?, + artist: row.get(3)?, + genre: row.get(4)?, + }) + } + + fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { + vec![ + &self.album_id, + &self.title, + &self.metadata, + &self.artist, + &self.genre, + ] + } + + fn id(&self) -> u64 {self.album_id} +} + +#[derive(Clone, Debug)] +pub struct DbGenreItem { + pub genre_id: u64, + pub title: String, +} + +impl DatabaseObj for DbGenreItem { + fn map_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(Self{ + genre_id: row.get(0)?, + title: row.get(1)?, + }) + } + + fn to_params(&self) -> Vec<&dyn rusqlite::ToSql> { + vec![ + &self.genre_id, + &self.title, + ] + } + + fn id(&self) -> u64 {self.genre_id} +} diff --git a/mps-interpreter/src/lang/dictionary.rs b/mps-interpreter/src/lang/dictionary.rs new file mode 100644 index 0000000..54314f1 --- /dev/null +++ b/mps-interpreter/src/lang/dictionary.rs @@ -0,0 +1,42 @@ +use std::collections::VecDeque; + +use crate::tokens::MpsToken; +use super::{BoxedMpsOpFactory, MpsOp}; +use super::SyntaxError; + +pub struct MpsLanguageDictionary { + vocabulary: Vec> +} + +impl MpsLanguageDictionary { + pub fn add(&mut self, factory: T) -> &mut Self { + self.vocabulary.push(Box::new(factory) as Box); + self + } + + pub fn try_build_statement(&self, tokens: &mut VecDeque) -> Result, SyntaxError> { + for factory in &self.vocabulary { + if factory.is_op_boxed(tokens) { + return factory.build_op_boxed(tokens); + } + } + Err(SyntaxError { + line: 0, + token: tokens.pop_front().unwrap() + }) + } + + pub fn new() -> Self { + Self { + vocabulary: Vec::new(), + } + } +} + +impl Default for MpsLanguageDictionary { + fn default() -> Self { + Self { + vocabulary: Vec::new(), + } + } +} diff --git a/mps-interpreter/src/lang/error.rs b/mps-interpreter/src/lang/error.rs new file mode 100644 index 0000000..474037f --- /dev/null +++ b/mps-interpreter/src/lang/error.rs @@ -0,0 +1,41 @@ +use std::fmt::{Debug, Display, Formatter, Error}; + +use crate::tokens::MpsToken; +use super::MpsOp; + +#[derive(Debug)] +pub struct SyntaxError { + pub line: usize, + pub token: MpsToken, +} + +impl Display for SyntaxError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "SyntaxError (line {}): Unexpected {}", &self.line, &self.token) + } +} + +impl MpsLanguageError for SyntaxError { + fn set_line(&mut self, line: usize) {self.line = line} +} + +#[derive(Debug)] +pub struct RuntimeError { + pub line: usize, + pub op: Box, + pub msg: String, +} + +impl Display for RuntimeError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "{} (line {}): {}", &self.msg, &self.line, &self.op) + } +} + +impl MpsLanguageError for RuntimeError { + fn set_line(&mut self, line: usize) {self.line = line} +} + +pub trait MpsLanguageError: Display + Debug { + fn set_line(&mut self, line: usize); +} diff --git a/mps-interpreter/src/lang/mod.rs b/mps-interpreter/src/lang/mod.rs new file mode 100644 index 0000000..ec88b0d --- /dev/null +++ b/mps-interpreter/src/lang/mod.rs @@ -0,0 +1,29 @@ +mod db_items; +mod dictionary; +mod error; +mod operation; +mod sql_query; +//mod statement; +pub(crate) mod utility; + +pub use dictionary::MpsLanguageDictionary; +pub use error::{SyntaxError, RuntimeError, MpsLanguageError}; +pub use operation::{MpsOp, MpsOpFactory, BoxedMpsOpFactory}; +//pub(crate) use statement::MpsStatement; + +pub mod vocabulary { + pub use super::sql_query::{SqlStatement, SqlStatementFactory}; +} + +pub mod db { + pub use super::db_items::{DEFAULT_SQLITE_FILEPATH, generate_default_db, DatabaseObj, DbMusicItem, DbAlbumItem, DbArtistItem, DbMetaItem, DbGenreItem}; +} + +#[cfg(test)] +mod tests { + #[test] + fn db_build_test() -> rusqlite::Result<()> { + super::db::generate_default_db()?; + Ok(()) + } +} diff --git a/mps-interpreter/src/lang/operation.rs b/mps-interpreter/src/lang/operation.rs new file mode 100644 index 0000000..f6cb8f8 --- /dev/null +++ b/mps-interpreter/src/lang/operation.rs @@ -0,0 +1,31 @@ +use std::iter::Iterator; +use std::collections::VecDeque; +use std::fmt::{Debug, Display}; + +use crate::MpsMusicItem; +use crate::MpsContext; +use crate::tokens::MpsToken; +use super::{SyntaxError, RuntimeError}; + +pub trait MpsOpFactory { + fn is_op(&self, tokens: &VecDeque) -> bool; + + fn build_op(&self, tokens: &mut VecDeque) -> Result; + + #[inline] + fn build_box(&self, tokens: &mut VecDeque) -> Result, SyntaxError> { + Ok(Box::new(self.build_op(tokens)?)) + } +} + +pub trait BoxedMpsOpFactory { + fn build_op_boxed(&self, tokens: &mut VecDeque) -> Result, SyntaxError>; + + fn is_op_boxed(&self, tokens: &VecDeque) -> bool; +} + +pub trait MpsOp: Iterator> + Debug + Display { + fn enter(&mut self, ctx: MpsContext); + + fn escape(&mut self) -> MpsContext; +} diff --git a/mps-interpreter/src/lang/sql_query.rs b/mps-interpreter/src/lang/sql_query.rs new file mode 100644 index 0000000..727b21d --- /dev/null +++ b/mps-interpreter/src/lang/sql_query.rs @@ -0,0 +1,166 @@ +use std::iter::Iterator; +use std::collections::VecDeque; +use std::fmt::{Debug, Display, Formatter, Error}; + +use crate::MpsContext; +use crate::MpsMusicItem; +use crate::tokens::MpsToken; +use super::{MpsOp, MpsOpFactory, BoxedMpsOpFactory}; +use super::{SyntaxError, RuntimeError}; +use super::utility::{assert_token, assert_token_raw}; +use super::db::*; + +#[derive(Debug)] +pub struct SqlStatement { + query: String, + context: Option, + rows: Option>>, + current: usize, +} + +impl SqlStatement { + fn map_item(&mut self, increment: bool) -> Option> { + if let Some(rows) = &self.rows { + if increment { + if self.current == rows.len() { + return None + } + self.current += 1; + } + if self.current >= rows.len() { + None + } else { + match &rows[self.current] { + Ok(item) => Some(Ok(item.clone())), + Err(e) => Some(Err(RuntimeError { + line: 0, + op: Box::new(self.clone()), + msg: format!("SQL music item mapping error: {}", e).into(), + })) + } + } + } else { + Some(Err(RuntimeError { + line: 0, + op: Box::new(self.clone()), + msg: format!("Context error: rows is None").into(), + })) + } + + } +} + +impl MpsOp for SqlStatement { + fn enter(&mut self, ctx: MpsContext){ + self.context = Some(ctx) + } + + fn escape(&mut self) -> MpsContext { + self.context.take().unwrap() + } +} + +impl std::clone::Clone for SqlStatement { + fn clone(&self) -> Self { + Self { + query: self.query.clone(), + context: self.context.clone(), + rows: None, // TODO use different Result type so this is cloneable + current: self.current, + } + } +} + +impl Iterator for SqlStatement { + type Item = Result; + + fn next(&mut self) -> Option { + if self.rows.is_some() { + // query has executed, return another result + self.map_item(true) + } else { + let ctx = self.context.as_mut().unwrap(); + // query has not been executed yet + if let None = ctx.sqlite_connection { + // connection needs to be created + match generate_default_db() { + Ok(conn) => ctx.sqlite_connection = Some(conn), + Err(e) => return Some(Err(RuntimeError{ + line: 0, + op: Box::new(self.clone()), + msg: format!("SQL connection error: {}", e).into() + })) + } + } + let conn = ctx.sqlite_connection.as_mut().unwrap(); + // execute query + match perform_query(conn, &self.query) { + Ok(items) => { + self.rows = Some(items); + self.map_item(false) + } + Err(e) => Some(Err(RuntimeError{ + line: 0, + op: Box::new(self.clone()), + msg: e + })) + } + } + } +} + +impl Display for SqlStatement { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "sql(`{}`)", &self.query) + } +} + +pub struct SqlStatementFactory; + +impl MpsOpFactory for SqlStatementFactory { + #[inline] + fn is_op(&self, tokens: &VecDeque) -> bool { + tokens.len() > 3 && tokens[0].is_sql() + } + + #[inline] + fn build_op(&self, tokens: &mut VecDeque) -> Result { + // sql ( `some query` ) + assert_token_raw(MpsToken::Sql, tokens)?; + assert_token_raw(MpsToken::OpenBracket, tokens)?; + let literal = assert_token(|t| { + match t { + MpsToken::Literal(query) => Some(query), + _ => None + } + }, MpsToken::Literal("".into()), tokens)?; + assert_token_raw(MpsToken::CloseBracket, tokens)?; + Ok(SqlStatement { + query: literal, + context: None, + current: 0, + rows: None, + }) + } +} + +impl BoxedMpsOpFactory for SqlStatementFactory { + fn build_op_boxed(&self, tokens: &mut VecDeque) -> Result, SyntaxError> { + self.build_box(tokens) + } + + fn is_op_boxed(&self, tokens: &VecDeque) -> bool { + self.is_op(tokens) + } +} + +fn perform_query( + conn: &mut rusqlite::Connection, + query: &str +) -> Result>, String> { + let mut stmt = conn.prepare(query) + .map_err(|e| format!("SQLite query error: {}", e))?; + let iter = stmt.query_map([], MpsMusicItem::map_row) + .map_err(|e| format!("SQLite item mapping error: {}", e))?; + Ok(iter.collect()) +} diff --git a/mps-interpreter/src/lang/statement.rs b/mps-interpreter/src/lang/statement.rs new file mode 100644 index 0000000..b8ec249 --- /dev/null +++ b/mps-interpreter/src/lang/statement.rs @@ -0,0 +1,40 @@ +use std::iter::Iterator; +use std::fmt::{Debug, Display, Formatter, Error}; + +use std::collections::VecDeque; + +use crate::tokens::MpsToken; +use crate::MpsMusicItem; + +use super::SqlStatement; +use super::{SyntaxError, RuntimeError}; +use super::MpsLanguageDictionary; + +#[derive(Debug)] +pub enum MpsStatement { + Sql(SqlStatement), +} + +impl MpsStatement { + pub fn eat_some(tokens: &mut VecDeque, vocab: MpsLanguageDictionary) -> Result { + vocab.try_build_statement(tokens) + } +} + +impl Iterator for MpsStatement { + type Item = Result; + + fn next(&mut self) -> Option { + match self { + MpsStatement::Sql(s) => s.next(), + } + } +} + +impl Display for MpsStatement { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + Self::Sql(s) => write!(f, "{}", s), + } + } +} diff --git a/mps-interpreter/src/lang/utility.rs b/mps-interpreter/src/lang/utility.rs new file mode 100644 index 0000000..d509091 --- /dev/null +++ b/mps-interpreter/src/lang/utility.rs @@ -0,0 +1,41 @@ +use std::collections::VecDeque; +#[cfg(feature = "music_library")] +use std::path::PathBuf; + +use crate::tokens::MpsToken; +use super::SyntaxError; + +pub fn assert_token Option>( + caster: F, + token: MpsToken, + tokens: &mut VecDeque +) -> Result { + if let Some(out) = caster(tokens.pop_front().unwrap()) { + Ok(out) + } else { + Err(SyntaxError{ + line: 0, + token: token, + }) + } +} + +pub fn assert_token_raw( + token: MpsToken, + tokens: &mut VecDeque +) -> Result { + let result = tokens.pop_front().unwrap(); + if std::mem::discriminant(&token) == std::mem::discriminant(&result) { + Ok(result) + } else { + Err(SyntaxError { + line: 0, + token: token, + }) + } +} + +#[cfg(feature = "music_library")] +pub fn music_folder() -> PathBuf { + dirs::home_dir().unwrap_or_else(|| PathBuf::from("./")).join("Music") +} diff --git a/mps-interpreter/src/lib.rs b/mps-interpreter/src/lib.rs new file mode 100644 index 0000000..dbd65ad --- /dev/null +++ b/mps-interpreter/src/lib.rs @@ -0,0 +1,16 @@ +mod context; +mod interpretor; +mod runner; +mod music_item; +pub mod lang; +#[cfg(feature = "music_library")] +pub mod music; +pub mod tokens; + +pub use context::MpsContext; +pub use interpretor::{MpsInterpretor, interpretor}; +pub use runner::MpsRunner; +pub use music_item::MpsMusicItem; + +#[cfg(test)] +mod tests {} diff --git a/mps-interpreter/src/music/build_library.rs b/mps-interpreter/src/music/build_library.rs new file mode 100644 index 0000000..740ddf6 --- /dev/null +++ b/mps-interpreter/src/music/build_library.rs @@ -0,0 +1,9 @@ +use std::path::Path; + +use super::MpsLibrary; + +pub fn build_library>(path: P) -> std::io::Result { + let mut result = MpsLibrary::new(); + result.read_path(path, 10)?; + Ok(result) +} diff --git a/mps-interpreter/src/music/library.rs b/mps-interpreter/src/music/library.rs new file mode 100644 index 0000000..ca427c9 --- /dev/null +++ b/mps-interpreter/src/music/library.rs @@ -0,0 +1,156 @@ +use std::collections::HashMap; +use std::path::Path; + +use symphonia::core::io::MediaSourceStream; +use symphonia::core::probe::Hint; + +use crate::lang::db::*; +use super::tag::Tags; + +#[derive(Clone, Default)] +pub struct MpsLibrary { + songs: HashMap, + metadata: HashMap, + artists: HashMap, + albums: HashMap, + genres: HashMap, +} + +impl MpsLibrary { + pub fn new() -> Self { + Self { + songs: HashMap::new(), + metadata: HashMap::new(), + artists: HashMap::new(), + albums: HashMap::new(), + genres: HashMap::new(), + } + } + + pub fn len(&self) -> usize { + self.songs.len() + } + + pub fn all_songs<'a>(&'a self) -> Vec<&'a DbMusicItem> { + self.songs.values().collect() + } + + pub fn all_metadata<'a>(&'a self) -> Vec<&'a DbMetaItem> { + self.metadata.values().collect() + } + + pub fn all_artists<'a>(&'a self) -> Vec<&'a DbArtistItem> { + self.artists.values().collect() + } + + pub fn all_albums<'a>(&'a self) -> Vec<&'a DbAlbumItem> { + self.albums.values().collect() + } + + pub fn all_genres<'a>(&'a self) -> Vec<&'a DbGenreItem> { + self.genres.values().collect() + } + + pub fn read_path>(&mut self, path: P, depth: usize) -> std::io::Result<()> { + let path = path.as_ref(); + if path.is_dir() && depth != 0 { + for entry in path.read_dir()? { + self.read_path(entry?.path(), depth-1)?; + } + } else if path.is_file() { + self.read_file(path)?; + } + Ok(()) + } + + fn read_file>(&mut self, path: P) -> std::io::Result<()> { + let path = path.as_ref(); + let file = Box::new(std::fs::File::open(path)?); + // use symphonia to get metadata + let mss = MediaSourceStream::new(file, Default::default() /* options */); + let probed = symphonia::default::get_probe().format( + &Hint::new(), + mss, + &Default::default(), + &Default::default() + ); + // process audio file, ignoring any processing errors (skip file on error) + if let Ok(mut probed) = probed { + let mut tags = Tags::new(path); + // collect metadata + if let Some(metadata) = probed.metadata.get() { + if let Some(rev) = metadata.current() { + for tag in rev.tags() { + //println!("(pre) metadata tag ({},{})", tag.key, tag.value); + tags.add(tag.key.clone(), &tag.value); + } + } + } + if let Some(rev) = probed.format.metadata().current() { + for tag in rev.tags() { + //println!("(post) metadata tag ({},{})", tag.key, tag.value); + tags.add(tag.key.clone(), &tag.value); + } + } + self.generate_entries(&tags); + } + Ok(()) + } + + /// generate data structures and links + fn generate_entries(&mut self, tags: &Tags) { + if tags.len() == 0 { return; } // probably not a valid song, let's skip it + let song_id = self.songs.len() as u64; // guaranteed to be created + let meta_id = self.metadata.len() as u64; // guaranteed to be created + self.metadata.insert(meta_id, tags.meta(meta_id)); // definitely necessary + // genre has no links to others, so find that first + let mut genre = tags.genre(0); + genre.genre_id = Self::find_or_gen_id(&self.genres, &genre.title); + if genre.genre_id == self.genres.len() as u64 { + self.genres.insert(Self::sanitise_key(&genre.title), genre.clone()); + } + // artist only links to genre, so that can be next + let mut artist = tags.artist(0, genre.genre_id); + artist.artist_id = Self::find_or_gen_id(&self.artists, &artist.name); + if artist.artist_id == self.artists.len() as u64 { + self.artists.insert(Self::sanitise_key(&artist.name), artist.clone()); + } + // same with album artist + let mut album_artist = tags.album_artist(0, genre.genre_id); + album_artist.artist_id = Self::find_or_gen_id(&self.artists, &album_artist.name); + if album_artist.artist_id == self.artists.len() as u64 { + self.artists.insert(Self::sanitise_key(&album_artist.name), album_artist.clone()); + } + // album now has all links ready + let mut album = tags.album(0, 0, album_artist.artist_id, genre.genre_id); + album.album_id = Self::find_or_gen_id(&self.albums, &album.title); + if album.album_id == self.albums.len() as u64 { + let album_meta = tags.album_meta(self.metadata.len() as u64); + album.metadata = album_meta.meta_id; + self.albums.insert(Self::sanitise_key(&album.title), album.clone()); + self.metadata.insert(album_meta.meta_id, album_meta); + } + //let meta_album_id = self.metadata.len() as u64; + //let album = tags.album(album_id, meta_album_id); + self.songs.insert(song_id, tags.song(song_id, artist.artist_id, Some(album.album_id), meta_id, genre.genre_id)); + } + + #[inline] + fn find_or_gen_id<'a, D: DatabaseObj>(map: &'a HashMap, key: &str) -> u64 { + if let Some(obj) = Self::find_by_key(map, key) { + obj.id() + } else { + map.len() as u64 + } + } + + #[inline(always)] + fn find_by_key<'a, D: DatabaseObj>(map: &'a HashMap, key: &str) -> Option<&'a D> { + map.get(&Self::sanitise_key(key)) + } + + #[inline(always)] + fn sanitise_key(key: &str) -> String { + key.trim().to_lowercase() + } +} diff --git a/mps-interpreter/src/music/mod.rs b/mps-interpreter/src/music/mod.rs new file mode 100644 index 0000000..ce19fdb --- /dev/null +++ b/mps-interpreter/src/music/mod.rs @@ -0,0 +1,6 @@ +mod build_library; +mod library; +mod tag; + +pub use build_library::build_library; +pub use library::MpsLibrary; diff --git a/mps-interpreter/src/music/tag.rs b/mps-interpreter/src/music/tag.rs new file mode 100644 index 0000000..8da66b0 --- /dev/null +++ b/mps-interpreter/src/music/tag.rs @@ -0,0 +1,145 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use symphonia::core::meta::Value; + +use crate::lang::db::*; + +pub struct Tags { + data: HashMap, + filename: PathBuf, +} + +impl Tags { + pub fn new>(path: P) -> Self { + Self { + data: HashMap::new(), + filename: path.as_ref().canonicalize().unwrap(), + } + } + + pub fn add(&mut self, key: String, value: &Value) { + if let Some(tag_type) = TagType::from_symphonia_value(value) { + self.data.insert(key.trim().to_uppercase(), tag_type); + } + } + + pub fn len(&self) -> usize {self.data.len()} + + pub fn song(&self, id: u64, artist_id: u64, album_id: Option, meta_id: u64, genre_id: u64) -> DbMusicItem { + let default_title = || { + let extension = self.filename.extension().and_then(|ext| ext.to_str()).unwrap_or(""); + self.filename.file_name() + .and_then(|file| file.to_str()) + .and_then(|file| Some(file.replacen(extension, "", 1))) + .unwrap_or("Unknown Title".into()) + }; + DbMusicItem { + song_id: id, + title: self.data.get("TITLE") + .unwrap_or(&TagType::Unknown).str() + .and_then(|s| Some(s.to_string())) + .unwrap_or_else(default_title), + artist: artist_id, + album: album_id, + filename: self.filename.to_str().unwrap_or("").into(), + metadata: meta_id, + genre: genre_id, + } + } + + pub fn meta(&self, id: u64) -> DbMetaItem { + DbMetaItem { + meta_id: id, + plays: self.data.get("PLAYS").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), + track: self.data.get("TRACKNUMBER").unwrap_or(&TagType::Unknown).uint().unwrap_or(id), + disc: self.data.get("DISCNUMBER").unwrap_or(&TagType::Unknown).uint().unwrap_or(1), + duration: self.data.get("DURATION").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), + date: self.data.get("DATE").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), + } + } + + pub fn artist(&self, id: u64, genre_id: u64) -> DbArtistItem { + DbArtistItem { + artist_id: id, + name: self.data.get("ARTIST").unwrap_or(&TagType::Unknown).str().unwrap_or("Unknown Artist").into(), + genre: genre_id, + } + } + + pub fn album_artist(&self, id: u64, genre_id: u64) -> DbArtistItem { + DbArtistItem { + artist_id: id, + name: self.data.get("ALBUMARTIST").unwrap_or(&TagType::Unknown).str().unwrap_or("Unknown Artist").into(), + genre: genre_id, + } + } + + pub fn album(&self, id: u64, meta_id: u64, artist_id: u64, genre_id: u64) -> DbAlbumItem { + DbAlbumItem { + album_id: id, + title: self.data.get("ALBUM").unwrap_or(&TagType::Unknown).str().unwrap_or("Unknown Album").into(), + metadata: meta_id, + artist: artist_id, + genre: genre_id, + } + } + + pub fn album_meta(&self, id: u64) -> DbMetaItem { + DbMetaItem { + meta_id: id, + plays: self.data.get("PLAYS").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), + track: self.data.get("TRACKTOTAL").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), + disc: self.data.get("DISCTOTAL").unwrap_or(&TagType::Unknown).uint().unwrap_or(1), + duration: 0, + date: self.data.get("DATE").unwrap_or(&TagType::Unknown).uint().unwrap_or(0), + } + } + + pub fn genre(&self, id: u64) -> DbGenreItem { + DbGenreItem { + genre_id: id, + title: self.data.get("GENRE").unwrap_or(&TagType::Unknown).str().unwrap_or("Unknown Genre").into(), + } + } +} + +#[derive(Clone)] +enum TagType { + Boolean(bool), + Flag, + I64(i64), + U64(u64), + Str(String), + Unknown +} + +impl TagType { + fn from_symphonia_value(value: &Value) -> Option { + match value { + Value::Binary(_val) => None, + Value::Boolean(b) => Some(Self::Boolean(*b)), + Value::Flag => Some(Self::Flag), + Value::Float(_val) => None, + Value::SignedInt(i) => Some(Self::I64(*i)), + Value::String(s) => Some(Self::Str(s.clone())), + Value::UnsignedInt(u) => Some(Self::U64(*u)), + } + } + + fn str(&self) -> Option<&str> { + match self { + Self::Str(s) => Some(&s), + _ => None + } + } + + fn uint(&self) -> Option { + match self { + Self::I64(i) => (*i).try_into().ok(), + Self::U64(u) => Some(*u), + Self::Str(s) => s.parse::().ok(), + _ => None + } + } +} diff --git a/mps-interpreter/src/music_item.rs b/mps-interpreter/src/music_item.rs new file mode 100644 index 0000000..0fb8f86 --- /dev/null +++ b/mps-interpreter/src/music_item.rs @@ -0,0 +1,17 @@ +use super::lang::db::{DatabaseObj, DbMusicItem}; + +#[derive(Clone, Debug)] +pub struct MpsMusicItem { + pub title: String, + pub filename: String, +} + +impl MpsMusicItem { + pub fn map_row(row: &rusqlite::Row) -> rusqlite::Result { + let item = DbMusicItem::map_row(row)?; + Ok(Self{ + title: item.title, + filename: item.filename, + }) + } +} diff --git a/mps-interpreter/src/runner.rs b/mps-interpreter/src/runner.rs new file mode 100644 index 0000000..00d0d16 --- /dev/null +++ b/mps-interpreter/src/runner.rs @@ -0,0 +1,70 @@ +use std::iter::Iterator; +use std::io::Read; + +use super::{MpsInterpretor, MpsContext, MpsMusicItem}; +use super::tokens::{MpsTokenReader, MpsTokenizer}; +use super::lang::{MpsLanguageDictionary, MpsLanguageError}; + +pub struct MpsRunnerSettings { + pub vocabulary: MpsLanguageDictionary, + pub tokenizer: T, + pub context: Option, +} + +impl MpsRunnerSettings { + pub fn default_with_tokenizer(token_reader: T) -> Self { + let mut vocab = MpsLanguageDictionary::default(); + super::interpretor::standard_vocab(&mut vocab); + Self { + vocabulary: vocab, + tokenizer: token_reader, + context: None, + } + } +} + +pub struct MpsRunner { + interpretor: MpsInterpretor, + new_statement: bool, +} + +impl MpsRunner { + pub fn with_settings(settings: MpsRunnerSettings) -> Self { + let mut interpretor = MpsInterpretor::with_vocab(settings.tokenizer, settings.vocabulary); + if let Some(ctx) = settings.context { + interpretor.context(ctx); + } + Self { + interpretor: interpretor, + new_statement: true, + } + } + + pub fn is_new_statement(&self) -> bool { + self.new_statement + } +} + +impl MpsRunner> { + pub fn with_stream(stream: R) -> Self { + let tokenizer = MpsTokenizer::new(stream); + Self { + interpretor: MpsInterpretor::with_standard_vocab(tokenizer), + new_statement: true, + } + } +} + +impl Iterator for MpsRunner { + type Item = Result>; + + fn next(&mut self) -> Option { + let mut item = self.interpretor.next(); + self.new_statement = false; + while item.is_none() && !self.interpretor.is_done() { + item = self.interpretor.next(); + self.new_statement = true; + } + item + } +} diff --git a/mps-interpreter/src/tokens/error.rs b/mps-interpreter/src/tokens/error.rs new file mode 100644 index 0000000..b3c9af3 --- /dev/null +++ b/mps-interpreter/src/tokens/error.rs @@ -0,0 +1,39 @@ +use std::fmt::{Debug, Display, Formatter, Error}; + +use crate::lang::MpsLanguageError; + +#[derive(Debug)] +pub struct ParseError { + pub line: usize, + pub column: usize, + pub item: String, +} + +impl Display for ParseError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "ParseError (line {}, column {}): Unexpected {}", &self.line, &self.column, &self.item) + } +} + +impl MpsTokenError for ParseError { + fn set_line(&mut self, line: usize) {self.line = line} + + fn set_column(&mut self, column: usize) {self.column = column} +} + +pub trait MpsTokenError: Display + Debug { + fn set_line(&mut self, line: usize); + + fn set_column(&mut self, column: usize); + + fn set_location(&mut self, line: usize, column: usize) { + self.set_line(line); + self.set_column(column); + } +} + +impl MpsLanguageError for T { + fn set_line(&mut self, line: usize) { + (self as &mut dyn MpsTokenError).set_line(line); + } +} diff --git a/mps-interpreter/src/tokens/mod.rs b/mps-interpreter/src/tokens/mod.rs new file mode 100644 index 0000000..8fc6cdf --- /dev/null +++ b/mps-interpreter/src/tokens/mod.rs @@ -0,0 +1,7 @@ +mod error; +mod token_enum; +mod tokenizer; + +pub use error::{ParseError, MpsTokenError}; +pub use token_enum::MpsToken; +pub use tokenizer::{MpsTokenizer, MpsTokenReader}; diff --git a/mps-interpreter/src/tokens/token_enum.rs b/mps-interpreter/src/tokens/token_enum.rs new file mode 100644 index 0000000..362860f --- /dev/null +++ b/mps-interpreter/src/tokens/token_enum.rs @@ -0,0 +1,59 @@ +use std::fmt::{Debug, Display, Formatter, Error}; + +#[derive(Debug, Eq, PartialEq)] +pub enum MpsToken { + Sql, + OpenBracket, + CloseBracket, + Literal(String), +} + +impl MpsToken { + pub fn parse_from_string(s: String) -> Result { + match &s as &str { + "sql" => Ok(Self::Sql), + "(" => Ok(Self::OpenBracket), + ")" => Ok(Self::CloseBracket), + _ => Err(s), + } + } + + pub fn is_sql(&self) -> bool { + match self { + Self::Sql => true, + _ => false + } + } + + pub fn is_open_bracket(&self) -> bool { + match self { + Self::OpenBracket => true, + _ => false + } + } + + pub fn is_close_bracket(&self) -> bool { + match self { + Self::CloseBracket => true, + _ => false + } + } + + pub fn is_literal(&self) -> bool { + match self { + Self::Literal(_) => true, + _ => false + } + } +} + +impl Display for MpsToken { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + match self { + Self::Sql => write!(f, "sql"), + Self::OpenBracket => write!(f, "("), + Self::CloseBracket => write!(f, ")"), + Self::Literal(s) => write!(f, "\"{}\"", s), + } + } +} diff --git a/mps-interpreter/src/tokens/tokenizer.rs b/mps-interpreter/src/tokens/tokenizer.rs new file mode 100644 index 0000000..57867cf --- /dev/null +++ b/mps-interpreter/src/tokens/tokenizer.rs @@ -0,0 +1,249 @@ +use std::collections::VecDeque; + +use super::ParseError; +use super::MpsToken; + +pub trait MpsTokenReader { + fn current_line(&self) -> usize; + + fn current_column(&self) -> usize; + + fn next_statements(&mut self, count: usize, token_buffer: &mut VecDeque) -> Result<(), ParseError>; + + fn end_of_file(&self) -> bool; +} + +pub struct MpsTokenizer where R: std::io::Read { + reader: R, + fsm: ReaderStateMachine, + line: usize, + column: usize, +} + +impl MpsTokenizer where R: std::io::Read { + pub fn new(reader: R) -> Self { + Self { + reader: reader, + fsm: ReaderStateMachine::Start{}, + line: 0, + column: 0, + } + } + + pub fn read_line(&mut self, buf: &mut VecDeque) -> Result<(), ParseError> { + let mut byte_buf = [0_u8]; + // first read special case + // always read before checking if end of statement + // since FSM could be from previous (already ended) statement + if self.reader.read(&mut byte_buf).map_err(|e| self.error(format!("IO read error: {}", e)))? == 0 { + byte_buf[0] = 0; // clear to null char (nothing read is assumed to mean end of file) + } + self.do_tracking(byte_buf[0]); + self.fsm = self.fsm.next_state(byte_buf[0]); + let mut bigger_buf: Vec = Vec::new(); + while !(self.fsm.is_end_statement() || self.fsm.is_end_of_file()) { + // keep token's bytes + if let Some(out) = self.fsm.output() { + bigger_buf.push(out); + } + // handle parse endings + match self.fsm { + ReaderStateMachine::EndLiteral{} => { + let literal = String::from_utf8(bigger_buf.clone()) + .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; + buf.push_back(MpsToken::Literal(literal)); + bigger_buf.clear(); + }, + ReaderStateMachine::EndToken{} => { + let token = String::from_utf8(bigger_buf.clone()) + .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; + buf.push_back( + MpsToken::parse_from_string(token) + .map_err(|e| self.error(format!("Invalid token {}", e)))? + ); + bigger_buf.clear(); + }, + ReaderStateMachine::Bracket{..} => { + let out = bigger_buf.pop().unwrap(); // bracket token + if bigger_buf.len() != 0 { // bracket tokens can be beside other tokens, without separator + let token = String::from_utf8(bigger_buf.clone()) + .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; + buf.push_back( + MpsToken::parse_from_string(token) + .map_err(|e| self.error(format!("Invalid token {}", e)))? + ); + bigger_buf.clear(); + } + // process bracket token + bigger_buf.push(out); + let token = String::from_utf8(bigger_buf.clone()) + .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; + buf.push_back( + MpsToken::parse_from_string(token) + .map_err(|e| self.error(format!("Invalid token {}", e)))? + ); + bigger_buf.clear(); + }, + ReaderStateMachine::EndStatement{} => { + // unnecessary; loop will have already exited + }, + ReaderStateMachine::EndOfFile{} => { + // unnecessary; loop will have already exited + }, + _ => {}, + } + if self.reader.read(&mut byte_buf).map_err(|e| self.error(format!("IO read error: {}", e)))? == 0 { + byte_buf[0] = 0; // clear to null char (nothing read is assumed to mean end of file) + } + self.do_tracking(byte_buf[0]); + self.fsm = self.fsm.next_state(byte_buf[0]); + } + // handle end statement + if bigger_buf.len() != 0 { // also end of token + // note: never also end of literal, since those have explicit closing characters + let token = String::from_utf8(bigger_buf.clone()) + .map_err(|e| self.error(format!("UTF-8 encoding error: {}", e)))?; + buf.push_back( + MpsToken::parse_from_string(token) + .map_err(|e| self.error(format!("Invalid token {}", e)))? + ); + bigger_buf.clear(); + } + Ok(()) + } + + /// track line and column locations + fn do_tracking(&mut self, input: u8) { + if input as char == '\n' { + self.line += 1; + } + self.column += 1; // TODO correctly track columns with utf-8 characters longer than one byte + } + + /// error factory (for ergonomics/DRY) + fn error(&self, item: String) -> ParseError { + ParseError { + line: self.current_line(), + column: self.current_column(), + item: item, + } + } +} + +impl MpsTokenReader for MpsTokenizer +where + R: std::io::Read +{ + fn current_line(&self) -> usize { + self.line + } + + fn current_column(&self) -> usize { + self.column + } + + fn next_statements(&mut self, count: usize, buf: &mut VecDeque) -> Result<(), ParseError> { + for _ in 0..count { + self.read_line(buf)?; + } + Ok(()) + } + + fn end_of_file(&self) -> bool { + self.fsm.is_end_of_file() + } +} + +#[derive(Copy, Clone)] +enum ReaderStateMachine { + Start{}, // beginning of machine, no parsing has occured + Regular{ + out: u8, + }, // standard + Escaped{ + inside: char, // literal + }, // escape character; applied to next character + StartTickLiteral{}, + StartQuoteLiteral{}, + InsideTickLiteral{ + out: u8, + }, + InsideQuoteLiteral{ + out: u8, + }, + Bracket { + out: u8, + }, + EndLiteral{}, + EndToken{}, + EndStatement{}, + EndOfFile{}, +} + +impl ReaderStateMachine { + pub fn next_state(self, input: u8) -> Self { + let input_char = input as char; + match self { + Self::Start{} + | Self::Regular{..} + | Self::Bracket{..} + | Self::EndLiteral{} + | Self::EndToken{} + | Self::EndStatement{} => + match input_char { + '\\' => Self::Escaped{inside: '_'}, + '`' => Self::StartTickLiteral{}, + '"' => Self::StartQuoteLiteral{}, + ' ' => Self::EndToken{}, + '\n' | '\r' | ';' => Self::EndStatement{}, + '\0' => Self::EndOfFile{}, + '(' | ')' => Self::Bracket{out: input}, + _ => Self::Regular{out: input}, + }, + Self::Escaped{inside} => match inside { + '`' => Self::InsideTickLiteral{out: input}, + '"' => Self::InsideQuoteLiteral{out: input}, + '_' | _ => Self::Regular{out: input} + }, + Self::StartTickLiteral{} + | Self::InsideTickLiteral{..} => + match input_char { + '\\' => Self::Escaped{inside: '`'}, + '`' => Self::EndLiteral{}, + _ => Self::InsideTickLiteral{out: input}, + }, + Self::StartQuoteLiteral{} + | Self::InsideQuoteLiteral{..} => + match input_char { + '\\' => Self::Escaped{inside: '"'}, + '"' => Self::EndLiteral{}, + _ => Self::InsideQuoteLiteral{out: input}, + }, + Self::EndOfFile{} => Self::EndOfFile{}, + } + } + + pub fn is_end_statement(&self) -> bool { + match self { + Self::EndStatement{} => true, + _ => false + } + } + + pub fn is_end_of_file(&self) -> bool { + match self { + Self::EndOfFile{} => true, + _ => false + } + } + + pub fn output(&self) -> Option { + match self { + Self::Regular{ out, ..} + | Self::Bracket{ out, ..} + | Self::InsideTickLiteral{ out, ..} + | Self::InsideQuoteLiteral{ out, ..} => Some(*out), + _ => None + } + } +} diff --git a/mps-interpreter/tests/music_lib.rs b/mps-interpreter/tests/music_lib.rs new file mode 100644 index 0000000..2c5cee9 --- /dev/null +++ b/mps-interpreter/tests/music_lib.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "music_library")] +mod music_lib_test { + use mps_interpreter::music::*; + + #[test] + fn generate_library() { + let mut lib = MpsLibrary::new(); + lib.read_path("/home/ngnius/Music", 10).unwrap(); + println!("generated library size: {}", lib.len()); + } +} diff --git a/mps-interpreter/tests/single_line.rs b/mps-interpreter/tests/single_line.rs new file mode 100644 index 0000000..354d3b6 --- /dev/null +++ b/mps-interpreter/tests/single_line.rs @@ -0,0 +1,57 @@ +use std::io::Cursor; +use std::collections::VecDeque; +use mps_interpreter::*; +use mps_interpreter::lang::MpsLanguageError; +use mps_interpreter::tokens::{ParseError, MpsToken, MpsTokenizer}; + +#[test] +fn parse_line() -> Result<(), ParseError> { + let cursor = Cursor::new("sql(`SELECT * FROM songs;`)"); + let correct_tokens: Vec = vec![ + MpsToken::Sql, + MpsToken::OpenBracket, + MpsToken::Literal("SELECT * FROM songs;".into()), + MpsToken::CloseBracket, + ]; + + let mut tokenizer = MpsTokenizer::new(cursor); + let mut buf = VecDeque::::new(); + tokenizer.read_line(&mut buf)?; // operation being tested + + // debug output + println!("Token buffer:"); + for i in 0..buf.len() { + println!(" Token #{}: {}", i, &buf[i]); + } + + // validity tests + assert_eq!(buf.len(), correct_tokens.len()); + for i in 0..buf.len() { + assert_eq!(buf[i], correct_tokens[i]); + } + + tokenizer.read_line(&mut buf)?; // this should immediately return + Ok(()) +} + +#[test] +fn execute_line() -> Result<(), Box> { + let cursor = Cursor::new("sql(`SELECT * FROM songs ORDER BY artist;`)"); + + let tokenizer = MpsTokenizer::new(cursor); + let interpreter = MpsInterpretor::with_standard_vocab(tokenizer); + + let mut count = 0; + for result in interpreter { + if let Ok(item) = result { + count +=1; + if count > 100 {continue;} // no need to spam the rest of the songs + println!("Got song `{}` (file: `{}`)", item.title, item.filename); + } else { + println!("Got error while iterating (executing)"); + result?; + } + } + assert_ne!(count, 0); // database is populated + Ok(()) +} diff --git a/mps-player/Cargo.toml b/mps-player/Cargo.toml new file mode 100644 index 0000000..aa0be5b --- /dev/null +++ b/mps-player/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mps-player" +version = "0.1.0" +edition = "2021" + +[dependencies] +rodio = { version = "^0.14"} +m3u8-rs = { version = "^3.0.0" } + +# local +mps-interpreter = { path = "../mps-interpreter" } diff --git a/mps-player/src/errors.rs b/mps-player/src/errors.rs new file mode 100644 index 0000000..585961e --- /dev/null +++ b/mps-player/src/errors.rs @@ -0,0 +1,20 @@ +use std::fmt::{Debug, Display, Formatter, Error}; + +#[derive(Debug)] +pub struct PlaybackError { + pub(crate) msg: String +} + +impl PlaybackError { + pub fn from_err(err: E) -> Self { + Self { + msg: format!("{}", err), + } + } +} + +impl Display for PlaybackError { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "PlaybackError: {}", &self.msg) + } +} diff --git a/mps-player/src/lib.rs b/mps-player/src/lib.rs new file mode 100644 index 0000000..bcafa19 --- /dev/null +++ b/mps-player/src/lib.rs @@ -0,0 +1,8 @@ +mod errors; +mod player; + +pub use errors::PlaybackError; +pub use player::MpsPlayer; + +#[cfg(test)] +mod tests {} diff --git a/mps-player/src/player.rs b/mps-player/src/player.rs new file mode 100644 index 0000000..78164ba --- /dev/null +++ b/mps-player/src/player.rs @@ -0,0 +1,135 @@ +use std::io; +use std::fs; + +use rodio::{decoder::Decoder, OutputStream, Sink}; + +use m3u8_rs::{MediaPlaylist, MediaSegment}; + +use mps_interpreter::{MpsRunner, tokens::MpsTokenReader}; + +use super::PlaybackError; + +pub struct MpsPlayer { + runner: MpsRunner, + sink: Sink, + #[allow(dead_code)] + output_stream: OutputStream, // this is required for playback, so it must live as long as this struct instance +} + +impl MpsPlayer { + pub fn new(runner: MpsRunner) -> Result { + let (stream, output_handle) = OutputStream::try_default().map_err(PlaybackError::from_err)?; + Ok(Self{ + runner: runner, + sink: Sink::try_new(&output_handle).map_err(PlaybackError::from_err)?, + output_stream: stream, + }) + } + + pub fn play_all(&mut self) -> Result<(), PlaybackError> { + for item in &mut self.runner { + self.sink.sleep_until_end(); + match item { + Ok(music) => { + let file = fs::File::open(music.filename).map_err(PlaybackError::from_err)?; + let stream = io::BufReader::new(file); + let source = Decoder::new(stream).map_err(PlaybackError::from_err)?; + self.sink.append(source); + //self.sink.play(); // idk if this is necessary + Ok(()) + }, + Err(e) => Err(PlaybackError::from_err(e)) + }?; + } + self.sink.sleep_until_end(); + Ok(()) + } + + pub fn enqueue_all(&mut self) -> Result<(), PlaybackError> { + for item in &mut self.runner { + match item { + Ok(music) => { + let file = fs::File::open(music.filename).map_err(PlaybackError::from_err)?; + let stream = io::BufReader::new(file); + let source = Decoder::new(stream).map_err(PlaybackError::from_err)?; + self.sink.append(source); + self.sink.play(); // idk if this is necessary + Ok(()) + }, + Err(e) => Err(PlaybackError::from_err(e)) + }?; + } + Ok(()) + } + + pub fn resume(&self) { + self.sink.play() + } + + pub fn pause(&self) { + self.sink.pause() + } + + pub fn stop(&self) { + self.sink.stop() + } + + pub fn sleep_until_end(&self) { + self.sink.sleep_until_end() + } + + pub fn queue_len(&self) -> usize { + self.sink.len() + } + + pub fn save_m3u8(&mut self, w: &mut W) -> Result<(), PlaybackError> { + let mut playlist = MediaPlaylist { + version: 6, + ..Default::default() + }; + // generate + for item in &mut self.runner { + match item { + Ok(music) => { + playlist.segments.push( + MediaSegment { + uri: music.filename, + title: Some(music.title), + ..Default::default() + } + ); + Ok(()) + }, + Err(e) => Err(PlaybackError::from_err(e)) + }?; + } + playlist.write_to(w).map_err(PlaybackError::from_err) + } +} + +#[cfg(test)] +mod tests { + use std::io; + use mps_interpreter::MpsRunner; + use super::*; + + #[allow(dead_code)] + #[test] + fn play_cursor() -> Result<(), PlaybackError> { + let cursor = io::Cursor::new("sql(`SELECT * FROM songs JOIN artists ON songs.artist = artists.artist_id WHERE artists.name like 'thundercat'`);"); + let runner = MpsRunner::with_stream(cursor); + let mut player = MpsPlayer::new(runner)?; + player.play_all() + } + + #[test] + fn playlist() -> Result<(), PlaybackError> { + let cursor = io::Cursor::new("sql(`SELECT * FROM songs JOIN artists ON songs.artist = artists.artist_id WHERE artists.name like 'thundercat'`);"); + let runner = MpsRunner::with_stream(cursor); + let mut player = MpsPlayer::new(runner)?; + + let output_file = std::fs::File::create("playlist.m3u8").unwrap(); + let mut buffer = std::io::BufWriter::new(output_file); + player.save_m3u8(&mut buffer) + } +} diff --git a/mps-player/src/utility.rs b/mps-player/src/utility.rs new file mode 100644 index 0000000..ad54c4e --- /dev/null +++ b/mps-player/src/utility.rs @@ -0,0 +1,15 @@ +use std::path::Path; +use std::io; +use std::fs; + +use mps_interpreter::{MpsRunner, tokens::MpsTokenReader}; + +use super::{MpsPlayer, PlaybackError}; + +pub fn play_script>(p: P) -> Result<(), PlaybackError> { + let file = fs::File::open(music.filename).map_err(PlaybackError::from_err)?; + let stream = io::BufReader::new(file); + let runner = MpsRunner::with_stream(stream); + let mut player = MpsPlayer::new(runner); + player.play_all() +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..36876ef --- /dev/null +++ b/src/main.rs @@ -0,0 +1,15 @@ +use std::io; +use mps_interpreter::MpsRunner; +use mps_player::{MpsPlayer, PlaybackError}; + +#[allow(dead_code)] +fn play_cursor() -> Result<(), PlaybackError> { + let cursor = io::Cursor::new("sql(`SELECT * FROM songs JOIN artists ON songs.artist = artists.artist_id WHERE artists.name like 'thundercat'`);"); + let runner = MpsRunner::with_stream(cursor); + let mut player = MpsPlayer::new(runner)?; + player.play_all() +} + +fn main() { + play_cursor().unwrap(); +}