diff --git a/Cargo.lock b/Cargo.lock index b3c9c0e8..aa7a02c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,189 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.8.11", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.98", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.8.11", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -62,6 +245,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -72,9 +270,11 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" name = "analytics" version = "0.1.0" dependencies = [ + "actix-web", "anyhow", "async-nats", "chrono", + "clap", "dotenv", "duckdb", "owo-colors", @@ -100,6 +300,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.96" @@ -467,6 +717,27 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -530,6 +801,15 @@ dependencies = [ "serde", ] +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + [[package]] name = "cast" version = "0.3.0" @@ -614,6 +894,39 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "combine" version = "4.6.7" @@ -685,6 +998,23 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -877,6 +1207,19 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_more" +version = "0.99.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.98", +] + [[package]] name = "digest" version = "0.10.7" @@ -920,6 +1263,7 @@ checksum = "8e2093a18d0c07e411104a9d27ef0097872172552ad5774feba304c2b47f382c" dependencies = [ "arrow", "cast", + "chrono", "fallible-iterator", "fallible-streaming-iterator", "hashlink 0.9.1", @@ -968,6 +1312,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -1261,6 +1614,25 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.4.1" @@ -1369,6 +1741,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.2.0" @@ -1387,7 +1770,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.2.0", ] [[package]] @@ -1398,7 +1781,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http", + "http 1.2.0", "http-body", "pin-project-lite", ] @@ -1409,6 +1792,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.6.0" @@ -1418,7 +1807,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.2.0", "http-body", "httparse", "itoa", @@ -1435,7 +1824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http", + "http 1.2.0", "hyper", "hyper-util", "rustls", @@ -1455,7 +1844,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.2.0", "http-body", "hyper", "pin-project-lite", @@ -1627,6 +2016,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + [[package]] name = "indexmap" version = "2.7.1" @@ -1653,6 +2048,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.14" @@ -1693,6 +2094,12 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1826,6 +2233,23 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + [[package]] name = "lock_api" version = "0.4.12" @@ -1918,6 +2342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2126,6 +2551,12 @@ dependencies = [ "regex", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem" version = "3.0.4" @@ -3016,6 +3447,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -3041,7 +3478,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 1.2.0", "http-body", "http-body-util", "hyper", @@ -3754,7 +4191,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3795,6 +4232,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.25.0" @@ -4100,7 +4543,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "http", + "http 1.2.0", "httparse", "rand", "ring", @@ -4293,6 +4736,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.14.0" diff --git a/crates/analytics/.gitignore b/crates/analytics/.gitignore new file mode 100644 index 00000000..d954f9ab --- /dev/null +++ b/crates/analytics/.gitignore @@ -0,0 +1,3 @@ +.env +*.ddb +*.ddb.wal \ No newline at end of file diff --git a/crates/analytics/Cargo.toml b/crates/analytics/Cargo.toml index 6948aa50..5cac9a6e 100644 --- a/crates/analytics/Cargo.toml +++ b/crates/analytics/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true repository.workspace = true [dependencies] -duckdb = { version = "1.2.0" } +duckdb = { version = "1.2.0", features = ["chrono"] } async-nats = "0.39.0" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.139" @@ -25,3 +25,5 @@ dotenv = "0.15.0" chrono = { version = "0.4.39", features = ["serde"] } anyhow = "1.0.96" polars = "0.46.0" +clap = "4.5.31" +actix-web = "4.9.0" diff --git a/crates/analytics/src/cmd/mod.rs b/crates/analytics/src/cmd/mod.rs new file mode 100644 index 00000000..614a9303 --- /dev/null +++ b/crates/analytics/src/cmd/mod.rs @@ -0,0 +1,2 @@ +pub mod serve; +pub mod sync; diff --git a/crates/analytics/src/cmd/serve.rs b/crates/analytics/src/cmd/serve.rs new file mode 100644 index 00000000..99d99ec6 --- /dev/null +++ b/crates/analytics/src/cmd/serve.rs @@ -0,0 +1,27 @@ +use std::env; + +use actix_web::{get, App, HttpRequest, HttpServer}; +use duckdb::Connection; +use anyhow::Error; +use owo_colors::OwoColorize; + +#[get("/")] +async fn index(_req: HttpRequest) -> String { + "Hello world!".to_owned() +} + +pub async fn serve(_conn: &Connection) -> Result<(), Error> { + let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); + let port = env::var("PORT").unwrap_or_else(|_| "7879".to_string()); + let addr = format!("{}:{}", host, port); + + let url = format!("http://{}", addr); + println!("Listening on {}", url.bright_green()); + + HttpServer::new(|| App::new().service(index)) + .bind(&addr)? + .run() + .await + .map_err(Error::new)?; + Ok(()) +} \ No newline at end of file diff --git a/crates/analytics/src/cmd/sync.rs b/crates/analytics/src/cmd/sync.rs new file mode 100644 index 00000000..9ff53e63 --- /dev/null +++ b/crates/analytics/src/cmd/sync.rs @@ -0,0 +1,20 @@ +use anyhow::Error; +use duckdb::Connection; +use sqlx::{Pool, Postgres}; +use crate::core::*; + +pub async fn sync(conn: &Connection, pool: &Pool) -> Result<(), Error> { + load_tracks(conn, pool).await?; + load_artists(conn, pool).await?; + load_albums(conn, pool).await?; + load_users(conn, pool).await?; + load_scrobbles(conn, pool).await?; + load_album_tracks(conn, pool).await?; + load_loved_tracks(conn, pool).await?; + load_artist_tracks(conn, pool).await?; + load_user_albums(conn, pool).await?; + load_user_artists(conn, pool).await?; + load_user_tracks(conn, pool).await?; + + Ok(()) +} diff --git a/crates/analytics/src/core.rs b/crates/analytics/src/core.rs new file mode 100644 index 00000000..4386920b --- /dev/null +++ b/crates/analytics/src/core.rs @@ -0,0 +1,632 @@ +use duckdb::{params, Connection}; +use anyhow::Error; +use owo_colors::OwoColorize; +use sqlx::{Pool, Postgres}; + +use crate::xata; + + +pub async fn create_tables(conn: &Connection) -> Result<(), Error> { + conn.execute_batch( + "BEGIN; + CREATE TABLE IF NOT EXISTS artists ( + id VARCHAR PRIMARY KEY, + name VARCHAR NOT NULL, + biography TEXT, + born DATE, + born_in VARCHAR, + died DATE, + picture VARCHAR, + sha256 VARCHAR NOT NULL, + spotify_link VARCHAR, + tidal_link VARCHAR, + youtube_link VARCHAR, + apple_music_link VARCHAR, + uri VARCHAR, + ); + CREATE TABLE IF NOT EXISTS albums ( + id VARCHAR PRIMARY KEY, + title VARCHAR NOT NULL, + artist VARCHAR NOT NULL, + release_date DATE, + album_art VARCHAR, + year INTEGER, + spotify_link VARCHAR, + tidal_link VARCHAR, + youtube_link VARCHAR, + apple_music_link VARCHAR, + sha256 VARCHAR NOT NULL, + uri VARCHAR, + artist_uri VARCHAR, + ); + CREATE TABLE IF NOT EXISTS tracks ( + id VARCHAR PRIMARY KEY, + title VARCHAR, + artist VARCHAR, + album_artist VARCHAR, + album_art VARCHAR, + album VARCHAR, + track_number INTEGER, + duration INTEGER, + mb_id VARCHAR, + youtube_link VARCHAR, + spotify_link VARCHAR, + tidal_link VARCHAR, + apple_music_link VARCHAR, + sha256 VARCHAR NOT NULL, + lyrics TEXT, + composer VARCHAR, + genre VARCHAR, + disc_number INTEGER, + copyright_message VARCHAR, + label VARCHAR, + uri VARCHAR, + artist_uri VARCHAR, + album_uri VARCHAR, + created_at TIMESTAMP, + ); + CREATE TABLE IF NOT EXISTS album_tracks ( + id VARCHAR PRIMARY KEY, + album_id VARCHAR, + track_id VARCHAR, + FOREIGN KEY (album_id) REFERENCES albums(id), + FOREIGN KEY (track_id) REFERENCES tracks(id), + ); + CREATE TABLE IF NOT EXISTS users ( + id VARCHAR PRIMARY KEY, + display_name VARCHAR, + did VARCHAR, + handle VARCHAR, + avatar VARCHAR, + ); + CREATE TABLE IF NOT EXISTS playlists ( + id VARCHAR PRIMARY KEY, + name VARCHAR, + description TEXT, + picture VARCHAR, + created_at TIMESTAMP, + updated_at TIMESTAMP, + uri VARCHAR, + created_by VARCHAR NOT NULL, + FOREIGN KEY (created_by) REFERENCES users(id), + ); + CREATE TABLE IF NOT EXISTS playlist_tracks ( + id VARCHAR PRIMARY KEY, + playlist_id VARCHAR, + track_id VARCHAR, + added_by VARCHAR, + created_at TIMESTAMP, + FOREIGN KEY (playlist_id) REFERENCES playlists(id), + FOREIGN KEY (track_id) REFERENCES tracks(id), + ); + CREATE TABLE IF NOT EXISTS user_tracks ( + id VARCHAR PRIMARY KEY, + user_id VARCHAR, + track_id VARCHAR, + created_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (track_id) REFERENCES tracks(id), + ); + CREATE TABLE IF NOT EXISTS user_albums ( + id VARCHAR PRIMARY KEY, + user_id VARCHAR, + album_id VARCHAR, + created_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (album_id) REFERENCES albums(id), + ); + CREATE TABLE IF NOT EXISTS user_artists ( + id VARCHAR PRIMARY KEY, + user_id VARCHAR, + artist_id VARCHAR, + created_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (artist_id) REFERENCES artists(id), + ); + CREATE TABLE IF NOT EXISTS user_playlists ( + id VARCHAR PRIMARY KEY, + user_id VARCHAR, + playlist_id VARCHAR, + created_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (playlist_id) REFERENCES playlists(id), + ); + CREATE TABLE IF NOT EXISTS loved_tracks ( + id VARCHAR PRIMARY KEY, + user_id VARCHAR, + track_id VARCHAR, + created_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (track_id) REFERENCES tracks(id), + ); + CREATE TABLE IF NOT EXISTS artist_tracks ( + id VARCHAR PRIMARY KEY, + artist_id VARCHAR, + track_id VARCHAR, + created_at TIMESTAMP, + FOREIGN KEY (artist_id) REFERENCES artists(id), + FOREIGN KEY (track_id) REFERENCES tracks(id), + ); + CREATE TABLE IF NOT EXISTS album_tracks ( + id VARCHAR PRIMARY KEY, + album_id VARCHAR, + track_id VARCHAR, + FOREIGN KEY (album_id) REFERENCES albums(id), + FOREIGN KEY (track_id) REFERENCES tracks(id), + ); + CREATE TABLE IF NOT EXISTS scrobbles ( + id VARCHAR PRIMARY KEY, + user_id VARCHAR, + track_id VARCHAR, + album_id VARCHAR, + artist_id VARCHAR, + uri VARCHAR, + created_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (track_id) REFERENCES tracks(id), + FOREIGN KEY (album_id) REFERENCES albums(id), + FOREIGN KEY (artist_id) REFERENCES artists(id), + ); + COMMIT; + ", + )?; + + Ok(()) +} + +pub async fn load_tracks(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let tracks: Vec = sqlx::query_as(r#" + SELECT * FROM tracks + "#) + .fetch_all(pool) + .await?; + + for (i, track) in tracks.clone().into_iter().enumerate() { + println!("track {} - {} - {}", i, track.title.bright_green(), track.artist); + match conn.execute( + "INSERT INTO tracks ( + id, + title, + artist, + album_artist, + album_art, + album, + track_number, + duration, + mb_id, + youtube_link, + spotify_link, + tidal_link, + apple_music_link, + sha256, + lyrics, + composer, + genre, + disc_number, + copyright_message, + label, + uri, + artist_uri, + album_uri, + created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + params![ + track.xata_id, + track.title, + track.artist, + track.album_artist, + track.album_art, + track.album, + track.track_number, + track.duration, + track.mb_id, + track.youtube_link, + track.spotify_link, + track.tidal_link, + track.apple_music_link, + track.sha256, + track.lyrics, + track.composer, + track.genre, + track.disc_number, + track.copyright_message, + track.label, + track.uri, + track.artist_uri, + track.album_uri, + track.xata_createdat, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("tracks: {:?}", tracks.len()); + Ok(()) +} + +pub async fn load_artists(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let artists: Vec = sqlx::query_as(r#" + SELECT * FROM artists + "#) + .fetch_all(pool) + .await?; + + for (i, artist) in artists.clone().into_iter().enumerate() { + println!("artist {} - {}", i, artist.name.bright_green()); + match conn.execute( + "INSERT INTO artists ( + id, + name, + biography, + born, + born_in, + died, + picture, + sha256, + spotify_link, + tidal_link, + youtube_link, + apple_music_link, + uri + ) VALUES (?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?)", + params![ + artist.xata_id, + artist.name, + artist.biography, + artist.born, + artist.born_in, + artist.died, + artist.picture, + artist.sha256, + artist.spotify_link, + artist.tidal_link, + artist.youtube_link, + artist.apple_music_link, + artist.uri, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("artists: {:?}", artists.len()); + Ok(()) +} + +pub async fn load_albums(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let albums: Vec = sqlx::query_as(r#" + SELECT * FROM albums + "#) + .fetch_all(pool) + .await?; + + for (i, album) in albums.clone().into_iter().enumerate() { + println!("album {} - {}", i, album.title.bright_green()); + match conn.execute( + "INSERT INTO albums ( + id, + title, + artist, + release_date, + album_art, + year, + spotify_link, + tidal_link, + youtube_link, + apple_music_link, + sha256, + uri, + artist_uri + ) VALUES (?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?)", + params![ + album.xata_id, + album.title, + album.artist, + album.release_date, + album.album_art, + album.year, + album.spotify_link, + album.tidal_link, + album.youtube_link, + album.apple_music_link, + album.sha256, + album.uri, + album.artist_uri, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("albums: {:?}", albums.len()); + Ok(()) +} + +pub async fn load_users(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let users: Vec = sqlx::query_as(r#" + SELECT * FROM users + "#) + .fetch_all(pool) + .await?; + + for (i, user) in users.clone().into_iter().enumerate() { + println!("user {} - {}", i, user.display_name.bright_green()); + match conn.execute( + "INSERT INTO users ( + id, + display_name, + did, + handle, + avatar + ) VALUES (?, + ?, + ?, + ?, + ?)", + params![ + user.xata_id, + user.display_name, + user.did, + user.handle, + user.avatar, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("users: {:?}", users.len()); + Ok(()) +} + +pub async fn load_scrobbles(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let scrobbles: Vec = sqlx::query_as(r#" + SELECT * FROM scrobbles + "#) + .fetch_all(pool) + .await?; + + for (i, scrobble) in scrobbles.clone().into_iter().enumerate() { + println!("scrobble {} - {}", i, + match scrobble.uri.clone() { + Some(uri) => uri.to_string(), + None => "None".to_string(), + }.bright_green() + ); + match conn.execute( + "INSERT INTO scrobbles ( + id, + user_id, + track_id, + album_id, + artist_id, + uri, + created_at + ) VALUES (?, + ?, + ?, + ?, + ?, + ?, + ?)", + params![ + scrobble.xata_id, + scrobble.user_id, + scrobble.track_id, + scrobble.album_id, + scrobble.artist_id, + scrobble.uri, + scrobble.xata_createdat, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("scrobbles: {:?}", scrobbles.len()); + Ok(()) +} + +pub async fn load_album_tracks(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let album_tracks: Vec = sqlx::query_as(r#" + SELECT * FROM album_tracks + "#) + .fetch_all(pool) + .await?; + + for (i, album_track) in album_tracks.clone().into_iter().enumerate() { + println!("album_track {} - {} - {}", i, album_track.album_id.bright_green(), album_track.track_id); + match conn.execute( + "INSERT INTO album_tracks ( + id, + album_id, + track_id + ) VALUES (?, + ?, + ?)", + params![ + album_track.xata_id, + album_track.album_id, + album_track.track_id, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + println!("album_tracks: {:?}", album_tracks.len()); + Ok(()) +} + +pub async fn load_loved_tracks(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let loved_tracks: Vec = sqlx::query_as(r#" + SELECT * FROM loved_tracks + "#) + .fetch_all(pool) + .await?; + + for (i, loved_track) in loved_tracks.clone().into_iter().enumerate() { + println!("loved_track {} - {} - {}", i, loved_track.user_id.bright_green(), loved_track.track_id); + match conn.execute( + "INSERT INTO loved_tracks ( + id, + user_id, + track_id, + created_at + ) VALUES (?, + ?, + ?, + ?)", + params![ + loved_track.xata_id, + loved_track.user_id, + loved_track.track_id, + loved_track.xata_createdat, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("loved_tracks: {:?}", loved_tracks.len()); + Ok(()) +} + +pub async fn load_artist_tracks(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let artist_tracks: Vec = sqlx::query_as(r#" + SELECT * FROM artist_tracks + "#) + .fetch_all(pool) + .await?; + + for (i, artist_track) in artist_tracks.clone().into_iter().enumerate() { + println!("artist_track {} - {} - {}", i, artist_track.artist_id.bright_green(), artist_track.track_id); + match conn.execute( + "INSERT INTO artist_tracks (id, artist_id, track_id, created_at) VALUES (?, ?, ?, ?)", + params![ + artist_track.xata_id, + artist_track.artist_id, + artist_track.track_id, + artist_track.xata_createdat, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("artist_tracks: {:?}", artist_tracks.len()); + Ok(()) +} + +pub async fn load_user_albums(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let user_albums: Vec = sqlx::query_as(r#" + SELECT * FROM user_albums + "#) + .fetch_all(pool) + .await?; + + for (i, user_album) in user_albums.clone().into_iter().enumerate() { + println!("user_album {} - {} - {}", i, user_album.user_id.bright_green(), user_album.album_id); + match conn.execute( + "INSERT INTO user_albums (id, user_id, album_id, created_at) VALUES (?, ?, ?, ?)", + params![ + user_album.xata_id, + user_album.user_id, + user_album.album_id, + user_album.xata_createdat, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("user_albums: {:?}", user_albums.len()); + Ok(()) +} + +pub async fn load_user_artists(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let user_artists: Vec = sqlx::query_as(r#" + SELECT * FROM user_artists + "#) + .fetch_all(pool) + .await?; + + for (i, user_artist) in user_artists.clone().into_iter().enumerate() { + println!("user_artist {} - {} - {}", i, user_artist.user_id.bright_green(), user_artist.artist_id); + match conn.execute( + "INSERT INTO user_artists (id, user_id, artist_id, created_at) VALUES (?, ?, ?, ?)", + params![ + user_artist.xata_id, + user_artist.user_id, + user_artist.artist_id, + user_artist.xata_createdat, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("user_artists: {:?}", user_artists.len()); + Ok(()) +} + +pub async fn load_user_tracks(conn: &Connection, pool: &Pool) -> Result<(), Error> { + let user_tracks: Vec = sqlx::query_as(r#" + SELECT * FROM user_tracks + "#) + .fetch_all(pool) + .await?; + + for (i, user_track) in user_tracks.clone().into_iter().enumerate() { + println!("user_track {} - {} - {}", i, user_track.user_id.bright_green(), user_track.track_id); + match conn.execute( + "INSERT INTO user_tracks (id, user_id, track_id, created_at) VALUES (?, ?, ?, ?)", + params![ + user_track.xata_id, + user_track.user_id, + user_track.track_id, + user_track.xata_createdat, + ], + ) { + Ok(_) => (), + Err(e) => println!("error: {}", e), + } + } + + println!("user_tracks: {:?}", user_tracks.len()); + Ok(()) +} \ No newline at end of file diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 8b137891..20cf7aa5 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -1 +1,2 @@ - +pub mod types; +pub mod xata; diff --git a/crates/analytics/src/main.rs b/crates/analytics/src/main.rs index 1ff15b6c..7e4c099f 100644 --- a/crates/analytics/src/main.rs +++ b/crates/analytics/src/main.rs @@ -1,8 +1,48 @@ +use core::create_tables; +use std::env; + +use clap::Command; +use cmd::{serve::serve, sync::sync}; use duckdb::Connection; +use sqlx::postgres::PgPoolOptions; +use dotenv::dotenv; + +pub mod types; +pub mod xata; +pub mod cmd; +pub mod core; + +fn cli() -> Command { + Command::new("analytics") + .version(env!("CARGO_PKG_VERSION")) + .about("Rocksky Analytics CLI built with Rust and DuckDB") + .subcommand( + Command::new("sync") + .about("Sync data from Xata to DuckDB") + ) + .subcommand( + Command::new("serve") + .about("Serve the Rocksky Analytics API") + ) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenv().ok(); + + + let pool= PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; + let conn = Connection::open("./rocksky-analytics.ddb")?; + + create_tables(&conn).await?; + + let args = cli().get_matches(); + + match args.subcommand() { + Some(("sync", _)) => sync(&conn, &pool).await?, + Some(("serve", _)) => serve(&conn).await?, + _ => serve(&conn).await?, + } -fn main() -> Result<(), Box> { - let conn = Connection::open_in_memory()?; - conn.execute_batch("CREATE TABLE items (name STRING, value INTEGER);")?; - println!("Hello, world!"); Ok(()) } diff --git a/crates/analytics/src/types/album.rs b/crates/analytics/src/types/album.rs new file mode 100644 index 00000000..fe97a74c --- /dev/null +++ b/crates/analytics/src/types/album.rs @@ -0,0 +1,32 @@ +use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Album { + pub id: String, + pub title: String, + pub artist: String, + pub release_date: Option, + pub album_art: Option, + pub year: Option, + pub spotify_link: Option, + pub tidal_link: Option, + pub youtube_link: Option, + pub apple_music_link: Option, + pub sha256: String, + pub uri: Option, + pub artist_uri: Option, +} + +impl Responder for Album { + type Body = BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + let body = serde_json::to_string(&self).unwrap(); + + // Create response and set content type + HttpResponse::Ok() + .content_type(ContentType::json()) + .body(body) + } +} diff --git a/crates/analytics/src/types/album_track.rs b/crates/analytics/src/types/album_track.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/types/artist.rs b/crates/analytics/src/types/artist.rs new file mode 100644 index 00000000..f4fcd2bd --- /dev/null +++ b/crates/analytics/src/types/artist.rs @@ -0,0 +1,33 @@ +use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; +use chrono::NaiveDate; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Artist { + pub id: String, + pub name: String, + pub biography: Option, + pub born: Option, + pub born_in: Option, + pub died: Option, + pub picture: Option, + pub sha256: String, + pub spotify_link: Option, + pub tidal_link: Option, + pub youtube_link: Option, + pub apple_music_link: Option, + pub uri: Option, +} + +impl Responder for Artist { + type Body = BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + let body = serde_json::to_string(&self).unwrap(); + + // Create response and set content type + HttpResponse::Ok() + .content_type(ContentType::json()) + .body(body) + } +} diff --git a/crates/analytics/src/types/artist_album.rs b/crates/analytics/src/types/artist_album.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/types/artist_track.rs b/crates/analytics/src/types/artist_track.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/types/loved_track.rs b/crates/analytics/src/types/loved_track.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/types/mod.rs b/crates/analytics/src/types/mod.rs new file mode 100644 index 00000000..c7f2208b --- /dev/null +++ b/crates/analytics/src/types/mod.rs @@ -0,0 +1,12 @@ +pub mod album; +pub mod album_track; +pub mod artist; +pub mod artist_track; +pub mod playlist; +pub mod playlist_track; +pub mod scrobble; +pub mod track; +pub mod user; +pub mod user_album; +pub mod user_artist; +pub mod user_playlist; diff --git a/crates/analytics/src/types/playlist.rs b/crates/analytics/src/types/playlist.rs new file mode 100644 index 00000000..ecf44d99 --- /dev/null +++ b/crates/analytics/src/types/playlist.rs @@ -0,0 +1,28 @@ +use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Playlist { + pub id: String, + pub name: String, + pub description: Option, + pub picture: Option, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub uri: Option, + pub created_by: String, +} + +impl Responder for Playlist { + type Body = BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + let body = serde_json::to_string(&self).unwrap(); + + // Create response and set content type + HttpResponse::Ok() + .content_type(ContentType::json()) + .body(body) + } +} diff --git a/crates/analytics/src/types/playlist_track.rs b/crates/analytics/src/types/playlist_track.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/types/scrobble.rs b/crates/analytics/src/types/scrobble.rs new file mode 100644 index 00000000..99d92eb3 --- /dev/null +++ b/crates/analytics/src/types/scrobble.rs @@ -0,0 +1,13 @@ +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Scrobble { + pub id: String, + pub user_id: String, + pub track_id: String, + pub album_id: String, + pub artist_id: String, + pub uri: Option, + pub created_at: NaiveDateTime, +} diff --git a/crates/analytics/src/types/track.rs b/crates/analytics/src/types/track.rs new file mode 100644 index 00000000..1dd8aebc --- /dev/null +++ b/crates/analytics/src/types/track.rs @@ -0,0 +1,44 @@ +use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Track { + pub id: String, + pub title: String, + pub artist: String, + pub album_artist: String, + pub album_art: Option, + pub album: String, + pub track_number: i32, + pub duration: i32, + pub mb_id: Option, + pub youtube_link: Option, + pub spotify_link: Option, + pub tidal_link: Option, + pub apple_music_link: Option, + pub sha256: String, + pub lyrics: Option, + pub composer: Option, + pub genre: Option, + pub disc_number: i32, + pub copyright_message: Option, + pub label: Option, + pub uri: Option, + pub artist_uri: Option, + pub album_uri: Option, + pub created_at: NaiveDateTime, +} + +impl Responder for Track { + type Body = BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + let body = serde_json::to_string(&self).unwrap(); + + // Create response and set content type + HttpResponse::Ok() + .content_type(ContentType::json()) + .body(body) + } +} diff --git a/crates/analytics/src/types/user.rs b/crates/analytics/src/types/user.rs new file mode 100644 index 00000000..6c5cc786 --- /dev/null +++ b/crates/analytics/src/types/user.rs @@ -0,0 +1,24 @@ +use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + pub id: String, + pub display_name: String, + pub did: String, + pub handle: String, + pub avatar: String, +} + +impl Responder for User { + type Body = BoxBody; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + let body = serde_json::to_string(&self).unwrap(); + + // Create response and set content type + HttpResponse::Ok() + .content_type(ContentType::json()) + .body(body) + } +} diff --git a/crates/analytics/src/types/user_album.rs b/crates/analytics/src/types/user_album.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/types/user_artist.rs b/crates/analytics/src/types/user_artist.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/types/user_playlist.rs b/crates/analytics/src/types/user_playlist.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/types/user_track.rs b/crates/analytics/src/types/user_track.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/analytics/src/xata/album.rs b/crates/analytics/src/xata/album.rs new file mode 100644 index 00000000..fca2adf2 --- /dev/null +++ b/crates/analytics/src/xata/album.rs @@ -0,0 +1,21 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct Album { + pub xata_id: String, + pub title: String, + pub artist: String, + pub release_date: Option, + pub album_art: Option, + pub year: Option, + pub spotify_link: Option, + pub tidal_link: Option, + pub youtube_link: Option, + pub apple_music_link: Option, + pub sha256: String, + pub uri: Option, + pub artist_uri: Option, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, +} diff --git a/crates/analytics/src/xata/album_track.rs b/crates/analytics/src/xata/album_track.rs new file mode 100644 index 00000000..124075aa --- /dev/null +++ b/crates/analytics/src/xata/album_track.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct AlbumTrack { + pub xata_id: String, + pub album_id: String, + pub track_id: String, +} diff --git a/crates/analytics/src/xata/artist.rs b/crates/analytics/src/xata/artist.rs new file mode 100644 index 00000000..25187ec5 --- /dev/null +++ b/crates/analytics/src/xata/artist.rs @@ -0,0 +1,23 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct Artist { + pub xata_id: String, + pub name: String, + pub biography: Option, + #[serde(with = "chrono::serde::ts_seconds_option")] + pub born: Option>, + pub born_in: Option, + #[serde(with = "chrono::serde::ts_seconds_option")] + pub died: Option>, + pub picture: Option, + pub sha256: String, + pub spotify_link: Option, + pub tidal_link: Option, + pub youtube_link: Option, + pub apple_music_link: Option, + pub uri: Option, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, +} diff --git a/crates/analytics/src/xata/artist_album.rs b/crates/analytics/src/xata/artist_album.rs new file mode 100644 index 00000000..2e30ef11 --- /dev/null +++ b/crates/analytics/src/xata/artist_album.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct ArtistAlbum { + pub xata_id: String, + pub artist_id: String, + pub album_id: String, +} diff --git a/crates/analytics/src/xata/artist_track.rs b/crates/analytics/src/xata/artist_track.rs new file mode 100644 index 00000000..6fa911f4 --- /dev/null +++ b/crates/analytics/src/xata/artist_track.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct ArtistTrack { + pub xata_id: String, + pub artist_id: String, + pub track_id: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: chrono::DateTime, +} diff --git a/crates/analytics/src/xata/loved_track.rs b/crates/analytics/src/xata/loved_track.rs new file mode 100644 index 00000000..4515e80f --- /dev/null +++ b/crates/analytics/src/xata/loved_track.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct LovedTrack { + pub xata_id: String, + pub user_id: String, + pub track_id: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: chrono::DateTime, +} diff --git a/crates/analytics/src/xata/mod.rs b/crates/analytics/src/xata/mod.rs new file mode 100644 index 00000000..2c959adf --- /dev/null +++ b/crates/analytics/src/xata/mod.rs @@ -0,0 +1,15 @@ +pub mod album; +pub mod album_track; +pub mod artist; +pub mod artist_album; +pub mod artist_track; +pub mod loved_track; +pub mod playlist; +pub mod playlist_track; +pub mod scrobble; +pub mod track; +pub mod user; +pub mod user_album; +pub mod user_artist; +pub mod user_playlist; +pub mod user_track; diff --git a/crates/analytics/src/xata/playlist.rs b/crates/analytics/src/xata/playlist.rs new file mode 100644 index 00000000..25f6de51 --- /dev/null +++ b/crates/analytics/src/xata/playlist.rs @@ -0,0 +1,16 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct Playlist { + pub xata_id: String, + pub name: String, + pub description: Option, + pub picture: Option, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_updatedat: DateTime, + pub uri: Option, + pub created_by: String, +} diff --git a/crates/analytics/src/xata/playlist_track.rs b/crates/analytics/src/xata/playlist_track.rs new file mode 100644 index 00000000..b045a3f8 --- /dev/null +++ b/crates/analytics/src/xata/playlist_track.rs @@ -0,0 +1,11 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct PlaylistTrack { + pub xata_id: String, + pub playlist_id: String, + pub track_id: String, + pub added_by: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: chrono::DateTime, +} diff --git a/crates/analytics/src/xata/scrobble.rs b/crates/analytics/src/xata/scrobble.rs new file mode 100644 index 00000000..3ca5f387 --- /dev/null +++ b/crates/analytics/src/xata/scrobble.rs @@ -0,0 +1,14 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct Scrobble { + pub xata_id: String, + pub user_id: String, + pub track_id: String, + pub album_id: Option, + pub artist_id: Option, + pub uri: Option, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, +} diff --git a/crates/analytics/src/xata/track.rs b/crates/analytics/src/xata/track.rs new file mode 100644 index 00000000..cd36b095 --- /dev/null +++ b/crates/analytics/src/xata/track.rs @@ -0,0 +1,31 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct Track { + pub xata_id: String, + pub title: String, + pub artist: String, + pub album_artist: String, + pub album_art: Option, + pub album: String, + pub track_number: i32, + pub duration: i32, + pub mb_id: Option, + pub youtube_link: Option, + pub spotify_link: Option, + pub tidal_link: Option, + pub apple_music_link: Option, + pub sha256: String, + pub lyrics: Option, + pub composer: Option, + pub genre: Option, + pub disc_number: i32, + pub copyright_message: Option, + pub label: Option, + pub uri: Option, + pub artist_uri: Option, + pub album_uri: Option, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, +} diff --git a/crates/analytics/src/xata/user.rs b/crates/analytics/src/xata/user.rs new file mode 100644 index 00000000..967749a9 --- /dev/null +++ b/crates/analytics/src/xata/user.rs @@ -0,0 +1,13 @@ +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct User { + pub xata_id: String, + pub display_name: String, + pub did: String, + pub handle: String, + pub avatar: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: DateTime, +} diff --git a/crates/analytics/src/xata/user_album.rs b/crates/analytics/src/xata/user_album.rs new file mode 100644 index 00000000..67bad286 --- /dev/null +++ b/crates/analytics/src/xata/user_album.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct UserAlbum { + pub xata_id: String, + pub user_id: String, + pub album_id: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: chrono::DateTime, +} diff --git a/crates/analytics/src/xata/user_artist.rs b/crates/analytics/src/xata/user_artist.rs new file mode 100644 index 00000000..469e6a4d --- /dev/null +++ b/crates/analytics/src/xata/user_artist.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct UserArtist { + pub xata_id: String, + pub user_id: String, + pub artist_id: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: chrono::DateTime, +} diff --git a/crates/analytics/src/xata/user_playlist.rs b/crates/analytics/src/xata/user_playlist.rs new file mode 100644 index 00000000..af8f705d --- /dev/null +++ b/crates/analytics/src/xata/user_playlist.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct UserPlaylist { + pub xata_id: String, + pub user_id: String, + pub playlist_id: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: chrono::DateTime, +} diff --git a/crates/analytics/src/xata/user_track.rs b/crates/analytics/src/xata/user_track.rs new file mode 100644 index 00000000..0f9be606 --- /dev/null +++ b/crates/analytics/src/xata/user_track.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, sqlx::FromRow, Deserialize, Clone)] +pub struct UserTrack { + pub xata_id: String, + pub user_id: String, + pub track_id: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub xata_createdat: chrono::DateTime, +}