A decentralized music tracking and discovery platform built on AT Protocol 🎵

setup analytics server

+1596 -14
+457 -8
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "actix-codec" 7 + version = "0.5.2" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" 10 + dependencies = [ 11 + "bitflags", 12 + "bytes", 13 + "futures-core", 14 + "futures-sink", 15 + "memchr", 16 + "pin-project-lite", 17 + "tokio", 18 + "tokio-util", 19 + "tracing", 20 + ] 21 + 22 + [[package]] 23 + name = "actix-http" 24 + version = "3.9.0" 25 + source = "registry+https://github.com/rust-lang/crates.io-index" 26 + checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" 27 + dependencies = [ 28 + "actix-codec", 29 + "actix-rt", 30 + "actix-service", 31 + "actix-utils", 32 + "ahash 0.8.11", 33 + "base64", 34 + "bitflags", 35 + "brotli", 36 + "bytes", 37 + "bytestring", 38 + "derive_more", 39 + "encoding_rs", 40 + "flate2", 41 + "futures-core", 42 + "h2", 43 + "http 0.2.12", 44 + "httparse", 45 + "httpdate", 46 + "itoa", 47 + "language-tags", 48 + "local-channel", 49 + "mime", 50 + "percent-encoding", 51 + "pin-project-lite", 52 + "rand", 53 + "sha1", 54 + "smallvec", 55 + "tokio", 56 + "tokio-util", 57 + "tracing", 58 + "zstd", 59 + ] 60 + 61 + [[package]] 62 + name = "actix-macros" 63 + version = "0.2.4" 64 + source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" 66 + dependencies = [ 67 + "quote", 68 + "syn 2.0.98", 69 + ] 70 + 71 + [[package]] 72 + name = "actix-router" 73 + version = "0.5.3" 74 + source = "registry+https://github.com/rust-lang/crates.io-index" 75 + checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" 76 + dependencies = [ 77 + "bytestring", 78 + "cfg-if", 79 + "http 0.2.12", 80 + "regex", 81 + "regex-lite", 82 + "serde", 83 + "tracing", 84 + ] 85 + 86 + [[package]] 87 + name = "actix-rt" 88 + version = "2.10.0" 89 + source = "registry+https://github.com/rust-lang/crates.io-index" 90 + checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" 91 + dependencies = [ 92 + "futures-core", 93 + "tokio", 94 + ] 95 + 96 + [[package]] 97 + name = "actix-server" 98 + version = "2.5.0" 99 + source = "registry+https://github.com/rust-lang/crates.io-index" 100 + checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" 101 + dependencies = [ 102 + "actix-rt", 103 + "actix-service", 104 + "actix-utils", 105 + "futures-core", 106 + "futures-util", 107 + "mio", 108 + "socket2", 109 + "tokio", 110 + "tracing", 111 + ] 112 + 113 + [[package]] 114 + name = "actix-service" 115 + version = "2.0.2" 116 + source = "registry+https://github.com/rust-lang/crates.io-index" 117 + checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" 118 + dependencies = [ 119 + "futures-core", 120 + "paste", 121 + "pin-project-lite", 122 + ] 123 + 124 + [[package]] 125 + name = "actix-utils" 126 + version = "3.0.1" 127 + source = "registry+https://github.com/rust-lang/crates.io-index" 128 + checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" 129 + dependencies = [ 130 + "local-waker", 131 + "pin-project-lite", 132 + ] 133 + 134 + [[package]] 135 + name = "actix-web" 136 + version = "4.9.0" 137 + source = "registry+https://github.com/rust-lang/crates.io-index" 138 + checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" 139 + dependencies = [ 140 + "actix-codec", 141 + "actix-http", 142 + "actix-macros", 143 + "actix-router", 144 + "actix-rt", 145 + "actix-server", 146 + "actix-service", 147 + "actix-utils", 148 + "actix-web-codegen", 149 + "ahash 0.8.11", 150 + "bytes", 151 + "bytestring", 152 + "cfg-if", 153 + "cookie", 154 + "derive_more", 155 + "encoding_rs", 156 + "futures-core", 157 + "futures-util", 158 + "impl-more", 159 + "itoa", 160 + "language-tags", 161 + "log", 162 + "mime", 163 + "once_cell", 164 + "pin-project-lite", 165 + "regex", 166 + "regex-lite", 167 + "serde", 168 + "serde_json", 169 + "serde_urlencoded", 170 + "smallvec", 171 + "socket2", 172 + "time", 173 + "url", 174 + ] 175 + 176 + [[package]] 177 + name = "actix-web-codegen" 178 + version = "4.3.0" 179 + source = "registry+https://github.com/rust-lang/crates.io-index" 180 + checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" 181 + dependencies = [ 182 + "actix-router", 183 + "proc-macro2", 184 + "quote", 185 + "syn 2.0.98", 186 + ] 187 + 188 + [[package]] 6 189 name = "addr2line" 7 190 version = "0.24.2" 8 191 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 63 246 ] 64 247 65 248 [[package]] 249 + name = "alloc-no-stdlib" 250 + version = "2.0.4" 251 + source = "registry+https://github.com/rust-lang/crates.io-index" 252 + checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 253 + 254 + [[package]] 255 + name = "alloc-stdlib" 256 + version = "0.2.2" 257 + source = "registry+https://github.com/rust-lang/crates.io-index" 258 + checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 259 + dependencies = [ 260 + "alloc-no-stdlib", 261 + ] 262 + 263 + [[package]] 66 264 name = "allocator-api2" 67 265 version = "0.2.21" 68 266 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 72 270 name = "analytics" 73 271 version = "0.1.0" 74 272 dependencies = [ 273 + "actix-web", 75 274 "anyhow", 76 275 "async-nats", 77 276 "chrono", 277 + "clap", 78 278 "dotenv", 79 279 "duckdb", 80 280 "owo-colors", ··· 101 301 ] 102 302 103 303 [[package]] 304 + name = "anstream" 305 + version = "0.6.18" 306 + source = "registry+https://github.com/rust-lang/crates.io-index" 307 + checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 308 + dependencies = [ 309 + "anstyle", 310 + "anstyle-parse", 311 + "anstyle-query", 312 + "anstyle-wincon", 313 + "colorchoice", 314 + "is_terminal_polyfill", 315 + "utf8parse", 316 + ] 317 + 318 + [[package]] 319 + name = "anstyle" 320 + version = "1.0.10" 321 + source = "registry+https://github.com/rust-lang/crates.io-index" 322 + checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 323 + 324 + [[package]] 325 + name = "anstyle-parse" 326 + version = "0.2.6" 327 + source = "registry+https://github.com/rust-lang/crates.io-index" 328 + checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 329 + dependencies = [ 330 + "utf8parse", 331 + ] 332 + 333 + [[package]] 334 + name = "anstyle-query" 335 + version = "1.1.2" 336 + source = "registry+https://github.com/rust-lang/crates.io-index" 337 + checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 338 + dependencies = [ 339 + "windows-sys 0.59.0", 340 + ] 341 + 342 + [[package]] 343 + name = "anstyle-wincon" 344 + version = "3.0.7" 345 + source = "registry+https://github.com/rust-lang/crates.io-index" 346 + checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 347 + dependencies = [ 348 + "anstyle", 349 + "once_cell", 350 + "windows-sys 0.59.0", 351 + ] 352 + 353 + [[package]] 104 354 name = "anyhow" 105 355 version = "1.0.96" 106 356 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 468 718 ] 469 719 470 720 [[package]] 721 + name = "brotli" 722 + version = "6.0.0" 723 + source = "registry+https://github.com/rust-lang/crates.io-index" 724 + checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" 725 + dependencies = [ 726 + "alloc-no-stdlib", 727 + "alloc-stdlib", 728 + "brotli-decompressor", 729 + ] 730 + 731 + [[package]] 732 + name = "brotli-decompressor" 733 + version = "4.0.2" 734 + source = "registry+https://github.com/rust-lang/crates.io-index" 735 + checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" 736 + dependencies = [ 737 + "alloc-no-stdlib", 738 + "alloc-stdlib", 739 + ] 740 + 741 + [[package]] 471 742 name = "bumpalo" 472 743 version = "3.17.0" 473 744 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 531 802 ] 532 803 533 804 [[package]] 805 + name = "bytestring" 806 + version = "1.4.0" 807 + source = "registry+https://github.com/rust-lang/crates.io-index" 808 + checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" 809 + dependencies = [ 810 + "bytes", 811 + ] 812 + 813 + [[package]] 534 814 name = "cast" 535 815 version = "0.3.0" 536 816 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 615 895 ] 616 896 617 897 [[package]] 898 + name = "clap" 899 + version = "4.5.31" 900 + source = "registry+https://github.com/rust-lang/crates.io-index" 901 + checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" 902 + dependencies = [ 903 + "clap_builder", 904 + ] 905 + 906 + [[package]] 907 + name = "clap_builder" 908 + version = "4.5.31" 909 + source = "registry+https://github.com/rust-lang/crates.io-index" 910 + checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" 911 + dependencies = [ 912 + "anstream", 913 + "anstyle", 914 + "clap_lex", 915 + "strsim", 916 + ] 917 + 918 + [[package]] 919 + name = "clap_lex" 920 + version = "0.7.4" 921 + source = "registry+https://github.com/rust-lang/crates.io-index" 922 + checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 923 + 924 + [[package]] 925 + name = "colorchoice" 926 + version = "1.0.3" 927 + source = "registry+https://github.com/rust-lang/crates.io-index" 928 + checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 929 + 930 + [[package]] 618 931 name = "combine" 619 932 version = "4.6.7" 620 933 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 686 999 ] 687 1000 688 1001 [[package]] 1002 + name = "convert_case" 1003 + version = "0.4.0" 1004 + source = "registry+https://github.com/rust-lang/crates.io-index" 1005 + checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 1006 + 1007 + [[package]] 1008 + name = "cookie" 1009 + version = "0.16.2" 1010 + source = "registry+https://github.com/rust-lang/crates.io-index" 1011 + checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 1012 + dependencies = [ 1013 + "percent-encoding", 1014 + "time", 1015 + "version_check", 1016 + ] 1017 + 1018 + [[package]] 689 1019 name = "core-foundation" 690 1020 version = "0.9.4" 691 1021 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 878 1208 ] 879 1209 880 1210 [[package]] 1211 + name = "derive_more" 1212 + version = "0.99.19" 1213 + source = "registry+https://github.com/rust-lang/crates.io-index" 1214 + checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" 1215 + dependencies = [ 1216 + "convert_case", 1217 + "proc-macro2", 1218 + "quote", 1219 + "rustc_version", 1220 + "syn 2.0.98", 1221 + ] 1222 + 1223 + [[package]] 881 1224 name = "digest" 882 1225 version = "0.10.7" 883 1226 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 920 1263 dependencies = [ 921 1264 "arrow", 922 1265 "cast", 1266 + "chrono", 923 1267 "fallible-iterator", 924 1268 "fallible-streaming-iterator", 925 1269 "hashlink 0.9.1", ··· 969 1313 ] 970 1314 971 1315 [[package]] 1316 + name = "encoding_rs" 1317 + version = "0.8.35" 1318 + source = "registry+https://github.com/rust-lang/crates.io-index" 1319 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 1320 + dependencies = [ 1321 + "cfg-if", 1322 + ] 1323 + 1324 + [[package]] 972 1325 name = "enum_dispatch" 973 1326 version = "0.3.13" 974 1327 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1262 1615 checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 1263 1616 1264 1617 [[package]] 1618 + name = "h2" 1619 + version = "0.3.26" 1620 + source = "registry+https://github.com/rust-lang/crates.io-index" 1621 + checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 1622 + dependencies = [ 1623 + "bytes", 1624 + "fnv", 1625 + "futures-core", 1626 + "futures-sink", 1627 + "futures-util", 1628 + "http 0.2.12", 1629 + "indexmap", 1630 + "slab", 1631 + "tokio", 1632 + "tokio-util", 1633 + "tracing", 1634 + ] 1635 + 1636 + [[package]] 1265 1637 name = "half" 1266 1638 version = "2.4.1" 1267 1639 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1371 1743 1372 1744 [[package]] 1373 1745 name = "http" 1746 + version = "0.2.12" 1747 + source = "registry+https://github.com/rust-lang/crates.io-index" 1748 + checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 1749 + dependencies = [ 1750 + "bytes", 1751 + "fnv", 1752 + "itoa", 1753 + ] 1754 + 1755 + [[package]] 1756 + name = "http" 1374 1757 version = "1.2.0" 1375 1758 source = "registry+https://github.com/rust-lang/crates.io-index" 1376 1759 checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" ··· 1387 1770 checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 1388 1771 dependencies = [ 1389 1772 "bytes", 1390 - "http", 1773 + "http 1.2.0", 1391 1774 ] 1392 1775 1393 1776 [[package]] ··· 1398 1781 dependencies = [ 1399 1782 "bytes", 1400 1783 "futures-util", 1401 - "http", 1784 + "http 1.2.0", 1402 1785 "http-body", 1403 1786 "pin-project-lite", 1404 1787 ] ··· 1410 1793 checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" 1411 1794 1412 1795 [[package]] 1796 + name = "httpdate" 1797 + version = "1.0.3" 1798 + source = "registry+https://github.com/rust-lang/crates.io-index" 1799 + checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1800 + 1801 + [[package]] 1413 1802 name = "hyper" 1414 1803 version = "1.6.0" 1415 1804 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1418 1807 "bytes", 1419 1808 "futures-channel", 1420 1809 "futures-util", 1421 - "http", 1810 + "http 1.2.0", 1422 1811 "http-body", 1423 1812 "httparse", 1424 1813 "itoa", ··· 1435 1824 checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 1436 1825 dependencies = [ 1437 1826 "futures-util", 1438 - "http", 1827 + "http 1.2.0", 1439 1828 "hyper", 1440 1829 "hyper-util", 1441 1830 "rustls", ··· 1455 1844 "bytes", 1456 1845 "futures-channel", 1457 1846 "futures-util", 1458 - "http", 1847 + "http 1.2.0", 1459 1848 "http-body", 1460 1849 "hyper", 1461 1850 "pin-project-lite", ··· 1628 2017 ] 1629 2018 1630 2019 [[package]] 2020 + name = "impl-more" 2021 + version = "0.1.9" 2022 + source = "registry+https://github.com/rust-lang/crates.io-index" 2023 + checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" 2024 + 2025 + [[package]] 1631 2026 name = "indexmap" 1632 2027 version = "2.7.1" 1633 2028 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1654 2049 checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1655 2050 1656 2051 [[package]] 2052 + name = "is_terminal_polyfill" 2053 + version = "1.70.1" 2054 + source = "registry+https://github.com/rust-lang/crates.io-index" 2055 + checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 2056 + 2057 + [[package]] 1657 2058 name = "itoa" 1658 2059 version = "1.0.14" 1659 2060 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1692 2093 "serde_json", 1693 2094 "simple_asn1", 1694 2095 ] 2096 + 2097 + [[package]] 2098 + name = "language-tags" 2099 + version = "0.3.2" 2100 + source = "registry+https://github.com/rust-lang/crates.io-index" 2101 + checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" 1695 2102 1696 2103 [[package]] 1697 2104 name = "lazy_static" ··· 1827 2234 checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 1828 2235 1829 2236 [[package]] 2237 + name = "local-channel" 2238 + version = "0.1.5" 2239 + source = "registry+https://github.com/rust-lang/crates.io-index" 2240 + checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" 2241 + dependencies = [ 2242 + "futures-core", 2243 + "futures-sink", 2244 + "local-waker", 2245 + ] 2246 + 2247 + [[package]] 2248 + name = "local-waker" 2249 + version = "0.1.4" 2250 + source = "registry+https://github.com/rust-lang/crates.io-index" 2251 + checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" 2252 + 2253 + [[package]] 1830 2254 name = "lock_api" 1831 2255 version = "0.4.12" 1832 2256 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1918 2342 checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1919 2343 dependencies = [ 1920 2344 "libc", 2345 + "log", 1921 2346 "wasi 0.11.0+wasi-snapshot-preview1", 1922 2347 "windows-sys 0.52.0", 1923 2348 ] ··· 2127 2552 ] 2128 2553 2129 2554 [[package]] 2555 + name = "paste" 2556 + version = "1.0.15" 2557 + source = "registry+https://github.com/rust-lang/crates.io-index" 2558 + checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 2559 + 2560 + [[package]] 2130 2561 name = "pem" 2131 2562 version = "3.0.4" 2132 2563 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3017 3448 ] 3018 3449 3019 3450 [[package]] 3451 + name = "regex-lite" 3452 + version = "0.1.6" 3453 + source = "registry+https://github.com/rust-lang/crates.io-index" 3454 + checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" 3455 + 3456 + [[package]] 3020 3457 name = "regex-syntax" 3021 3458 version = "0.8.5" 3022 3459 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3041 3478 "bytes", 3042 3479 "futures-core", 3043 3480 "futures-util", 3044 - "http", 3481 + "http 1.2.0", 3045 3482 "http-body", 3046 3483 "http-body-util", 3047 3484 "hyper", ··· 3754 4191 "cfg-if", 3755 4192 "libc", 3756 4193 "psm", 3757 - "windows-sys 0.52.0", 4194 + "windows-sys 0.59.0", 3758 4195 ] 3759 4196 3760 4197 [[package]] ··· 3796 4233 ] 3797 4234 3798 4235 [[package]] 4236 + name = "strsim" 4237 + version = "0.11.1" 4238 + source = "registry+https://github.com/rust-lang/crates.io-index" 4239 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 4240 + 4241 + [[package]] 3799 4242 name = "strum" 3800 4243 version = "0.25.0" 3801 4244 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4100 4543 "bytes", 4101 4544 "futures-core", 4102 4545 "futures-sink", 4103 - "http", 4546 + "http 1.2.0", 4104 4547 "httparse", 4105 4548 "rand", 4106 4549 "ring", ··· 4292 4735 version = "1.0.4" 4293 4736 source = "registry+https://github.com/rust-lang/crates.io-index" 4294 4737 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 4738 + 4739 + [[package]] 4740 + name = "utf8parse" 4741 + version = "0.2.2" 4742 + source = "registry+https://github.com/rust-lang/crates.io-index" 4743 + checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 4295 4744 4296 4745 [[package]] 4297 4746 name = "uuid"
+3
crates/analytics/.gitignore
··· 1 + .env 2 + *.ddb 3 + *.ddb.wal
+3 -1
crates/analytics/Cargo.toml
··· 7 7 repository.workspace = true 8 8 9 9 [dependencies] 10 - duckdb = { version = "1.2.0" } 10 + duckdb = { version = "1.2.0", features = ["chrono"] } 11 11 async-nats = "0.39.0" 12 12 serde = { version = "1.0.217", features = ["derive"] } 13 13 serde_json = "1.0.139" ··· 25 25 chrono = { version = "0.4.39", features = ["serde"] } 26 26 anyhow = "1.0.96" 27 27 polars = "0.46.0" 28 + clap = "4.5.31" 29 + actix-web = "4.9.0"
+2
crates/analytics/src/cmd/mod.rs
··· 1 + pub mod serve; 2 + pub mod sync;
+27
crates/analytics/src/cmd/serve.rs
··· 1 + use std::env; 2 + 3 + use actix_web::{get, App, HttpRequest, HttpServer}; 4 + use duckdb::Connection; 5 + use anyhow::Error; 6 + use owo_colors::OwoColorize; 7 + 8 + #[get("/")] 9 + async fn index(_req: HttpRequest) -> String { 10 + "Hello world!".to_owned() 11 + } 12 + 13 + pub async fn serve(_conn: &Connection) -> Result<(), Error> { 14 + let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 15 + let port = env::var("PORT").unwrap_or_else(|_| "7879".to_string()); 16 + let addr = format!("{}:{}", host, port); 17 + 18 + let url = format!("http://{}", addr); 19 + println!("Listening on {}", url.bright_green()); 20 + 21 + HttpServer::new(|| App::new().service(index)) 22 + .bind(&addr)? 23 + .run() 24 + .await 25 + .map_err(Error::new)?; 26 + Ok(()) 27 + }
+20
crates/analytics/src/cmd/sync.rs
··· 1 + use anyhow::Error; 2 + use duckdb::Connection; 3 + use sqlx::{Pool, Postgres}; 4 + use crate::core::*; 5 + 6 + pub async fn sync(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 7 + load_tracks(conn, pool).await?; 8 + load_artists(conn, pool).await?; 9 + load_albums(conn, pool).await?; 10 + load_users(conn, pool).await?; 11 + load_scrobbles(conn, pool).await?; 12 + load_album_tracks(conn, pool).await?; 13 + load_loved_tracks(conn, pool).await?; 14 + load_artist_tracks(conn, pool).await?; 15 + load_user_albums(conn, pool).await?; 16 + load_user_artists(conn, pool).await?; 17 + load_user_tracks(conn, pool).await?; 18 + 19 + Ok(()) 20 + }
+632
crates/analytics/src/core.rs
··· 1 + use duckdb::{params, Connection}; 2 + use anyhow::Error; 3 + use owo_colors::OwoColorize; 4 + use sqlx::{Pool, Postgres}; 5 + 6 + use crate::xata; 7 + 8 + 9 + pub async fn create_tables(conn: &Connection) -> Result<(), Error> { 10 + conn.execute_batch( 11 + "BEGIN; 12 + CREATE TABLE IF NOT EXISTS artists ( 13 + id VARCHAR PRIMARY KEY, 14 + name VARCHAR NOT NULL, 15 + biography TEXT, 16 + born DATE, 17 + born_in VARCHAR, 18 + died DATE, 19 + picture VARCHAR, 20 + sha256 VARCHAR NOT NULL, 21 + spotify_link VARCHAR, 22 + tidal_link VARCHAR, 23 + youtube_link VARCHAR, 24 + apple_music_link VARCHAR, 25 + uri VARCHAR, 26 + ); 27 + CREATE TABLE IF NOT EXISTS albums ( 28 + id VARCHAR PRIMARY KEY, 29 + title VARCHAR NOT NULL, 30 + artist VARCHAR NOT NULL, 31 + release_date DATE, 32 + album_art VARCHAR, 33 + year INTEGER, 34 + spotify_link VARCHAR, 35 + tidal_link VARCHAR, 36 + youtube_link VARCHAR, 37 + apple_music_link VARCHAR, 38 + sha256 VARCHAR NOT NULL, 39 + uri VARCHAR, 40 + artist_uri VARCHAR, 41 + ); 42 + CREATE TABLE IF NOT EXISTS tracks ( 43 + id VARCHAR PRIMARY KEY, 44 + title VARCHAR, 45 + artist VARCHAR, 46 + album_artist VARCHAR, 47 + album_art VARCHAR, 48 + album VARCHAR, 49 + track_number INTEGER, 50 + duration INTEGER, 51 + mb_id VARCHAR, 52 + youtube_link VARCHAR, 53 + spotify_link VARCHAR, 54 + tidal_link VARCHAR, 55 + apple_music_link VARCHAR, 56 + sha256 VARCHAR NOT NULL, 57 + lyrics TEXT, 58 + composer VARCHAR, 59 + genre VARCHAR, 60 + disc_number INTEGER, 61 + copyright_message VARCHAR, 62 + label VARCHAR, 63 + uri VARCHAR, 64 + artist_uri VARCHAR, 65 + album_uri VARCHAR, 66 + created_at TIMESTAMP, 67 + ); 68 + CREATE TABLE IF NOT EXISTS album_tracks ( 69 + id VARCHAR PRIMARY KEY, 70 + album_id VARCHAR, 71 + track_id VARCHAR, 72 + FOREIGN KEY (album_id) REFERENCES albums(id), 73 + FOREIGN KEY (track_id) REFERENCES tracks(id), 74 + ); 75 + CREATE TABLE IF NOT EXISTS users ( 76 + id VARCHAR PRIMARY KEY, 77 + display_name VARCHAR, 78 + did VARCHAR, 79 + handle VARCHAR, 80 + avatar VARCHAR, 81 + ); 82 + CREATE TABLE IF NOT EXISTS playlists ( 83 + id VARCHAR PRIMARY KEY, 84 + name VARCHAR, 85 + description TEXT, 86 + picture VARCHAR, 87 + created_at TIMESTAMP, 88 + updated_at TIMESTAMP, 89 + uri VARCHAR, 90 + created_by VARCHAR NOT NULL, 91 + FOREIGN KEY (created_by) REFERENCES users(id), 92 + ); 93 + CREATE TABLE IF NOT EXISTS playlist_tracks ( 94 + id VARCHAR PRIMARY KEY, 95 + playlist_id VARCHAR, 96 + track_id VARCHAR, 97 + added_by VARCHAR, 98 + created_at TIMESTAMP, 99 + FOREIGN KEY (playlist_id) REFERENCES playlists(id), 100 + FOREIGN KEY (track_id) REFERENCES tracks(id), 101 + ); 102 + CREATE TABLE IF NOT EXISTS user_tracks ( 103 + id VARCHAR PRIMARY KEY, 104 + user_id VARCHAR, 105 + track_id VARCHAR, 106 + created_at TIMESTAMP, 107 + FOREIGN KEY (user_id) REFERENCES users(id), 108 + FOREIGN KEY (track_id) REFERENCES tracks(id), 109 + ); 110 + CREATE TABLE IF NOT EXISTS user_albums ( 111 + id VARCHAR PRIMARY KEY, 112 + user_id VARCHAR, 113 + album_id VARCHAR, 114 + created_at TIMESTAMP, 115 + FOREIGN KEY (user_id) REFERENCES users(id), 116 + FOREIGN KEY (album_id) REFERENCES albums(id), 117 + ); 118 + CREATE TABLE IF NOT EXISTS user_artists ( 119 + id VARCHAR PRIMARY KEY, 120 + user_id VARCHAR, 121 + artist_id VARCHAR, 122 + created_at TIMESTAMP, 123 + FOREIGN KEY (user_id) REFERENCES users(id), 124 + FOREIGN KEY (artist_id) REFERENCES artists(id), 125 + ); 126 + CREATE TABLE IF NOT EXISTS user_playlists ( 127 + id VARCHAR PRIMARY KEY, 128 + user_id VARCHAR, 129 + playlist_id VARCHAR, 130 + created_at TIMESTAMP, 131 + FOREIGN KEY (user_id) REFERENCES users(id), 132 + FOREIGN KEY (playlist_id) REFERENCES playlists(id), 133 + ); 134 + CREATE TABLE IF NOT EXISTS loved_tracks ( 135 + id VARCHAR PRIMARY KEY, 136 + user_id VARCHAR, 137 + track_id VARCHAR, 138 + created_at TIMESTAMP, 139 + FOREIGN KEY (user_id) REFERENCES users(id), 140 + FOREIGN KEY (track_id) REFERENCES tracks(id), 141 + ); 142 + CREATE TABLE IF NOT EXISTS artist_tracks ( 143 + id VARCHAR PRIMARY KEY, 144 + artist_id VARCHAR, 145 + track_id VARCHAR, 146 + created_at TIMESTAMP, 147 + FOREIGN KEY (artist_id) REFERENCES artists(id), 148 + FOREIGN KEY (track_id) REFERENCES tracks(id), 149 + ); 150 + CREATE TABLE IF NOT EXISTS album_tracks ( 151 + id VARCHAR PRIMARY KEY, 152 + album_id VARCHAR, 153 + track_id VARCHAR, 154 + FOREIGN KEY (album_id) REFERENCES albums(id), 155 + FOREIGN KEY (track_id) REFERENCES tracks(id), 156 + ); 157 + CREATE TABLE IF NOT EXISTS scrobbles ( 158 + id VARCHAR PRIMARY KEY, 159 + user_id VARCHAR, 160 + track_id VARCHAR, 161 + album_id VARCHAR, 162 + artist_id VARCHAR, 163 + uri VARCHAR, 164 + created_at TIMESTAMP, 165 + FOREIGN KEY (user_id) REFERENCES users(id), 166 + FOREIGN KEY (track_id) REFERENCES tracks(id), 167 + FOREIGN KEY (album_id) REFERENCES albums(id), 168 + FOREIGN KEY (artist_id) REFERENCES artists(id), 169 + ); 170 + COMMIT; 171 + ", 172 + )?; 173 + 174 + Ok(()) 175 + } 176 + 177 + pub async fn load_tracks(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 178 + let tracks: Vec<xata::track::Track> = sqlx::query_as(r#" 179 + SELECT * FROM tracks 180 + "#) 181 + .fetch_all(pool) 182 + .await?; 183 + 184 + for (i, track) in tracks.clone().into_iter().enumerate() { 185 + println!("track {} - {} - {}", i, track.title.bright_green(), track.artist); 186 + match conn.execute( 187 + "INSERT INTO tracks ( 188 + id, 189 + title, 190 + artist, 191 + album_artist, 192 + album_art, 193 + album, 194 + track_number, 195 + duration, 196 + mb_id, 197 + youtube_link, 198 + spotify_link, 199 + tidal_link, 200 + apple_music_link, 201 + sha256, 202 + lyrics, 203 + composer, 204 + genre, 205 + disc_number, 206 + copyright_message, 207 + label, 208 + uri, 209 + artist_uri, 210 + album_uri, 211 + created_at 212 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 213 + params![ 214 + track.xata_id, 215 + track.title, 216 + track.artist, 217 + track.album_artist, 218 + track.album_art, 219 + track.album, 220 + track.track_number, 221 + track.duration, 222 + track.mb_id, 223 + track.youtube_link, 224 + track.spotify_link, 225 + track.tidal_link, 226 + track.apple_music_link, 227 + track.sha256, 228 + track.lyrics, 229 + track.composer, 230 + track.genre, 231 + track.disc_number, 232 + track.copyright_message, 233 + track.label, 234 + track.uri, 235 + track.artist_uri, 236 + track.album_uri, 237 + track.xata_createdat, 238 + ], 239 + ) { 240 + Ok(_) => (), 241 + Err(e) => println!("error: {}", e), 242 + } 243 + } 244 + 245 + println!("tracks: {:?}", tracks.len()); 246 + Ok(()) 247 + } 248 + 249 + pub async fn load_artists(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 250 + let artists: Vec<xata::artist::Artist> = sqlx::query_as(r#" 251 + SELECT * FROM artists 252 + "#) 253 + .fetch_all(pool) 254 + .await?; 255 + 256 + for (i, artist) in artists.clone().into_iter().enumerate() { 257 + println!("artist {} - {}", i, artist.name.bright_green()); 258 + match conn.execute( 259 + "INSERT INTO artists ( 260 + id, 261 + name, 262 + biography, 263 + born, 264 + born_in, 265 + died, 266 + picture, 267 + sha256, 268 + spotify_link, 269 + tidal_link, 270 + youtube_link, 271 + apple_music_link, 272 + uri 273 + ) VALUES (?, 274 + ?, 275 + ?, 276 + ?, 277 + ?, 278 + ?, 279 + ?, 280 + ?, 281 + ?, 282 + ?, 283 + ?, 284 + ?, 285 + ?)", 286 + params![ 287 + artist.xata_id, 288 + artist.name, 289 + artist.biography, 290 + artist.born, 291 + artist.born_in, 292 + artist.died, 293 + artist.picture, 294 + artist.sha256, 295 + artist.spotify_link, 296 + artist.tidal_link, 297 + artist.youtube_link, 298 + artist.apple_music_link, 299 + artist.uri, 300 + ], 301 + ) { 302 + Ok(_) => (), 303 + Err(e) => println!("error: {}", e), 304 + } 305 + } 306 + 307 + println!("artists: {:?}", artists.len()); 308 + Ok(()) 309 + } 310 + 311 + pub async fn load_albums(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 312 + let albums: Vec<xata::album::Album> = sqlx::query_as(r#" 313 + SELECT * FROM albums 314 + "#) 315 + .fetch_all(pool) 316 + .await?; 317 + 318 + for (i, album) in albums.clone().into_iter().enumerate() { 319 + println!("album {} - {}", i, album.title.bright_green()); 320 + match conn.execute( 321 + "INSERT INTO albums ( 322 + id, 323 + title, 324 + artist, 325 + release_date, 326 + album_art, 327 + year, 328 + spotify_link, 329 + tidal_link, 330 + youtube_link, 331 + apple_music_link, 332 + sha256, 333 + uri, 334 + artist_uri 335 + ) VALUES (?, 336 + ?, 337 + ?, 338 + ?, 339 + ?, 340 + ?, 341 + ?, 342 + ?, 343 + ?, 344 + ?, 345 + ?, 346 + ?, 347 + ?)", 348 + params![ 349 + album.xata_id, 350 + album.title, 351 + album.artist, 352 + album.release_date, 353 + album.album_art, 354 + album.year, 355 + album.spotify_link, 356 + album.tidal_link, 357 + album.youtube_link, 358 + album.apple_music_link, 359 + album.sha256, 360 + album.uri, 361 + album.artist_uri, 362 + ], 363 + ) { 364 + Ok(_) => (), 365 + Err(e) => println!("error: {}", e), 366 + } 367 + } 368 + 369 + println!("albums: {:?}", albums.len()); 370 + Ok(()) 371 + } 372 + 373 + pub async fn load_users(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 374 + let users: Vec<xata::user::User> = sqlx::query_as(r#" 375 + SELECT * FROM users 376 + "#) 377 + .fetch_all(pool) 378 + .await?; 379 + 380 + for (i, user) in users.clone().into_iter().enumerate() { 381 + println!("user {} - {}", i, user.display_name.bright_green()); 382 + match conn.execute( 383 + "INSERT INTO users ( 384 + id, 385 + display_name, 386 + did, 387 + handle, 388 + avatar 389 + ) VALUES (?, 390 + ?, 391 + ?, 392 + ?, 393 + ?)", 394 + params![ 395 + user.xata_id, 396 + user.display_name, 397 + user.did, 398 + user.handle, 399 + user.avatar, 400 + ], 401 + ) { 402 + Ok(_) => (), 403 + Err(e) => println!("error: {}", e), 404 + } 405 + } 406 + 407 + println!("users: {:?}", users.len()); 408 + Ok(()) 409 + } 410 + 411 + pub async fn load_scrobbles(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 412 + let scrobbles: Vec<xata::scrobble::Scrobble> = sqlx::query_as(r#" 413 + SELECT * FROM scrobbles 414 + "#) 415 + .fetch_all(pool) 416 + .await?; 417 + 418 + for (i, scrobble) in scrobbles.clone().into_iter().enumerate() { 419 + println!("scrobble {} - {}", i, 420 + match scrobble.uri.clone() { 421 + Some(uri) => uri.to_string(), 422 + None => "None".to_string(), 423 + }.bright_green() 424 + ); 425 + match conn.execute( 426 + "INSERT INTO scrobbles ( 427 + id, 428 + user_id, 429 + track_id, 430 + album_id, 431 + artist_id, 432 + uri, 433 + created_at 434 + ) VALUES (?, 435 + ?, 436 + ?, 437 + ?, 438 + ?, 439 + ?, 440 + ?)", 441 + params![ 442 + scrobble.xata_id, 443 + scrobble.user_id, 444 + scrobble.track_id, 445 + scrobble.album_id, 446 + scrobble.artist_id, 447 + scrobble.uri, 448 + scrobble.xata_createdat, 449 + ], 450 + ) { 451 + Ok(_) => (), 452 + Err(e) => println!("error: {}", e), 453 + } 454 + } 455 + 456 + println!("scrobbles: {:?}", scrobbles.len()); 457 + Ok(()) 458 + } 459 + 460 + pub async fn load_album_tracks(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 461 + let album_tracks: Vec<xata::album_track::AlbumTrack> = sqlx::query_as(r#" 462 + SELECT * FROM album_tracks 463 + "#) 464 + .fetch_all(pool) 465 + .await?; 466 + 467 + for (i, album_track) in album_tracks.clone().into_iter().enumerate() { 468 + println!("album_track {} - {} - {}", i, album_track.album_id.bright_green(), album_track.track_id); 469 + match conn.execute( 470 + "INSERT INTO album_tracks ( 471 + id, 472 + album_id, 473 + track_id 474 + ) VALUES (?, 475 + ?, 476 + ?)", 477 + params![ 478 + album_track.xata_id, 479 + album_track.album_id, 480 + album_track.track_id, 481 + ], 482 + ) { 483 + Ok(_) => (), 484 + Err(e) => println!("error: {}", e), 485 + } 486 + } 487 + println!("album_tracks: {:?}", album_tracks.len()); 488 + Ok(()) 489 + } 490 + 491 + pub async fn load_loved_tracks(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 492 + let loved_tracks: Vec<xata::user_track::UserTrack> = sqlx::query_as(r#" 493 + SELECT * FROM loved_tracks 494 + "#) 495 + .fetch_all(pool) 496 + .await?; 497 + 498 + for (i, loved_track) in loved_tracks.clone().into_iter().enumerate() { 499 + println!("loved_track {} - {} - {}", i, loved_track.user_id.bright_green(), loved_track.track_id); 500 + match conn.execute( 501 + "INSERT INTO loved_tracks ( 502 + id, 503 + user_id, 504 + track_id, 505 + created_at 506 + ) VALUES (?, 507 + ?, 508 + ?, 509 + ?)", 510 + params![ 511 + loved_track.xata_id, 512 + loved_track.user_id, 513 + loved_track.track_id, 514 + loved_track.xata_createdat, 515 + ], 516 + ) { 517 + Ok(_) => (), 518 + Err(e) => println!("error: {}", e), 519 + } 520 + } 521 + 522 + println!("loved_tracks: {:?}", loved_tracks.len()); 523 + Ok(()) 524 + } 525 + 526 + pub async fn load_artist_tracks(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 527 + let artist_tracks: Vec<xata::artist_track::ArtistTrack> = sqlx::query_as(r#" 528 + SELECT * FROM artist_tracks 529 + "#) 530 + .fetch_all(pool) 531 + .await?; 532 + 533 + for (i, artist_track) in artist_tracks.clone().into_iter().enumerate() { 534 + println!("artist_track {} - {} - {}", i, artist_track.artist_id.bright_green(), artist_track.track_id); 535 + match conn.execute( 536 + "INSERT INTO artist_tracks (id, artist_id, track_id, created_at) VALUES (?, ?, ?, ?)", 537 + params![ 538 + artist_track.xata_id, 539 + artist_track.artist_id, 540 + artist_track.track_id, 541 + artist_track.xata_createdat, 542 + ], 543 + ) { 544 + Ok(_) => (), 545 + Err(e) => println!("error: {}", e), 546 + } 547 + } 548 + 549 + println!("artist_tracks: {:?}", artist_tracks.len()); 550 + Ok(()) 551 + } 552 + 553 + pub async fn load_user_albums(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 554 + let user_albums: Vec<xata::user_album::UserAlbum> = sqlx::query_as(r#" 555 + SELECT * FROM user_albums 556 + "#) 557 + .fetch_all(pool) 558 + .await?; 559 + 560 + for (i, user_album) in user_albums.clone().into_iter().enumerate() { 561 + println!("user_album {} - {} - {}", i, user_album.user_id.bright_green(), user_album.album_id); 562 + match conn.execute( 563 + "INSERT INTO user_albums (id, user_id, album_id, created_at) VALUES (?, ?, ?, ?)", 564 + params![ 565 + user_album.xata_id, 566 + user_album.user_id, 567 + user_album.album_id, 568 + user_album.xata_createdat, 569 + ], 570 + ) { 571 + Ok(_) => (), 572 + Err(e) => println!("error: {}", e), 573 + } 574 + } 575 + 576 + println!("user_albums: {:?}", user_albums.len()); 577 + Ok(()) 578 + } 579 + 580 + pub async fn load_user_artists(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 581 + let user_artists: Vec<xata::user_artist::UserArtist> = sqlx::query_as(r#" 582 + SELECT * FROM user_artists 583 + "#) 584 + .fetch_all(pool) 585 + .await?; 586 + 587 + for (i, user_artist) in user_artists.clone().into_iter().enumerate() { 588 + println!("user_artist {} - {} - {}", i, user_artist.user_id.bright_green(), user_artist.artist_id); 589 + match conn.execute( 590 + "INSERT INTO user_artists (id, user_id, artist_id, created_at) VALUES (?, ?, ?, ?)", 591 + params![ 592 + user_artist.xata_id, 593 + user_artist.user_id, 594 + user_artist.artist_id, 595 + user_artist.xata_createdat, 596 + ], 597 + ) { 598 + Ok(_) => (), 599 + Err(e) => println!("error: {}", e), 600 + } 601 + } 602 + 603 + println!("user_artists: {:?}", user_artists.len()); 604 + Ok(()) 605 + } 606 + 607 + pub async fn load_user_tracks(conn: &Connection, pool: &Pool<Postgres>) -> Result<(), Error> { 608 + let user_tracks: Vec<xata::user_track::UserTrack> = sqlx::query_as(r#" 609 + SELECT * FROM user_tracks 610 + "#) 611 + .fetch_all(pool) 612 + .await?; 613 + 614 + for (i, user_track) in user_tracks.clone().into_iter().enumerate() { 615 + println!("user_track {} - {} - {}", i, user_track.user_id.bright_green(), user_track.track_id); 616 + match conn.execute( 617 + "INSERT INTO user_tracks (id, user_id, track_id, created_at) VALUES (?, ?, ?, ?)", 618 + params![ 619 + user_track.xata_id, 620 + user_track.user_id, 621 + user_track.track_id, 622 + user_track.xata_createdat, 623 + ], 624 + ) { 625 + Ok(_) => (), 626 + Err(e) => println!("error: {}", e), 627 + } 628 + } 629 + 630 + println!("user_tracks: {:?}", user_tracks.len()); 631 + Ok(()) 632 + }
+2 -1
crates/analytics/src/lib.rs
··· 1 - 1 + pub mod types; 2 + pub mod xata;
+44 -4
crates/analytics/src/main.rs
··· 1 + use core::create_tables; 2 + use std::env; 3 + 4 + use clap::Command; 5 + use cmd::{serve::serve, sync::sync}; 1 6 use duckdb::Connection; 7 + use sqlx::postgres::PgPoolOptions; 8 + use dotenv::dotenv; 2 9 3 - fn main() -> Result<(), Box<dyn std::error::Error>> { 4 - let conn = Connection::open_in_memory()?; 5 - conn.execute_batch("CREATE TABLE items (name STRING, value INTEGER);")?; 6 - println!("Hello, world!"); 10 + pub mod types; 11 + pub mod xata; 12 + pub mod cmd; 13 + pub mod core; 14 + 15 + fn cli() -> Command { 16 + Command::new("analytics") 17 + .version(env!("CARGO_PKG_VERSION")) 18 + .about("Rocksky Analytics CLI built with Rust and DuckDB") 19 + .subcommand( 20 + Command::new("sync") 21 + .about("Sync data from Xata to DuckDB") 22 + ) 23 + .subcommand( 24 + Command::new("serve") 25 + .about("Serve the Rocksky Analytics API") 26 + ) 27 + } 28 + 29 + #[tokio::main] 30 + async fn main() -> Result<(), Box<dyn std::error::Error>> { 31 + dotenv().ok(); 32 + 33 + 34 + let pool= PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; 35 + let conn = Connection::open("./rocksky-analytics.ddb")?; 36 + 37 + create_tables(&conn).await?; 38 + 39 + let args = cli().get_matches(); 40 + 41 + match args.subcommand() { 42 + Some(("sync", _)) => sync(&conn, &pool).await?, 43 + Some(("serve", _)) => serve(&conn).await?, 44 + _ => serve(&conn).await?, 45 + } 46 + 7 47 Ok(()) 8 48 }
+32
crates/analytics/src/types/album.rs
··· 1 + use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + #[derive(Debug, Serialize, Deserialize)] 5 + pub struct Album { 6 + pub id: String, 7 + pub title: String, 8 + pub artist: String, 9 + pub release_date: Option<String>, 10 + pub album_art: Option<String>, 11 + pub year: Option<i32>, 12 + pub spotify_link: Option<String>, 13 + pub tidal_link: Option<String>, 14 + pub youtube_link: Option<String>, 15 + pub apple_music_link: Option<String>, 16 + pub sha256: String, 17 + pub uri: Option<String>, 18 + pub artist_uri: Option<String>, 19 + } 20 + 21 + impl Responder for Album { 22 + type Body = BoxBody; 23 + 24 + fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> { 25 + let body = serde_json::to_string(&self).unwrap(); 26 + 27 + // Create response and set content type 28 + HttpResponse::Ok() 29 + .content_type(ContentType::json()) 30 + .body(body) 31 + } 32 + }
crates/analytics/src/types/album_track.rs

This is a binary file and will not be displayed.

+33
crates/analytics/src/types/artist.rs
··· 1 + use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; 2 + use chrono::NaiveDate; 3 + use serde::{Deserialize, Serialize}; 4 + 5 + #[derive(Debug, Serialize, Deserialize)] 6 + pub struct Artist { 7 + pub id: String, 8 + pub name: String, 9 + pub biography: Option<String>, 10 + pub born: Option<NaiveDate>, 11 + pub born_in: Option<String>, 12 + pub died: Option<NaiveDate>, 13 + pub picture: Option<String>, 14 + pub sha256: String, 15 + pub spotify_link: Option<String>, 16 + pub tidal_link: Option<String>, 17 + pub youtube_link: Option<String>, 18 + pub apple_music_link: Option<String>, 19 + pub uri: Option<String>, 20 + } 21 + 22 + impl Responder for Artist { 23 + type Body = BoxBody; 24 + 25 + fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> { 26 + let body = serde_json::to_string(&self).unwrap(); 27 + 28 + // Create response and set content type 29 + HttpResponse::Ok() 30 + .content_type(ContentType::json()) 31 + .body(body) 32 + } 33 + }
crates/analytics/src/types/artist_album.rs

This is a binary file and will not be displayed.

crates/analytics/src/types/artist_track.rs

This is a binary file and will not be displayed.

crates/analytics/src/types/loved_track.rs

This is a binary file and will not be displayed.

+12
crates/analytics/src/types/mod.rs
··· 1 + pub mod album; 2 + pub mod album_track; 3 + pub mod artist; 4 + pub mod artist_track; 5 + pub mod playlist; 6 + pub mod playlist_track; 7 + pub mod scrobble; 8 + pub mod track; 9 + pub mod user; 10 + pub mod user_album; 11 + pub mod user_artist; 12 + pub mod user_playlist;
+28
crates/analytics/src/types/playlist.rs
··· 1 + use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; 2 + use chrono::NaiveDateTime; 3 + use serde::{Deserialize, Serialize}; 4 + 5 + #[derive(Debug, Serialize, Deserialize)] 6 + pub struct Playlist { 7 + pub id: String, 8 + pub name: String, 9 + pub description: Option<String>, 10 + pub picture: Option<String>, 11 + pub created_at: NaiveDateTime, 12 + pub updated_at: NaiveDateTime, 13 + pub uri: Option<String>, 14 + pub created_by: String, 15 + } 16 + 17 + impl Responder for Playlist { 18 + type Body = BoxBody; 19 + 20 + fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> { 21 + let body = serde_json::to_string(&self).unwrap(); 22 + 23 + // Create response and set content type 24 + HttpResponse::Ok() 25 + .content_type(ContentType::json()) 26 + .body(body) 27 + } 28 + }
crates/analytics/src/types/playlist_track.rs

This is a binary file and will not be displayed.

+13
crates/analytics/src/types/scrobble.rs
··· 1 + use chrono::NaiveDateTime; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + #[derive(Debug, Serialize, Deserialize)] 5 + pub struct Scrobble { 6 + pub id: String, 7 + pub user_id: String, 8 + pub track_id: String, 9 + pub album_id: String, 10 + pub artist_id: String, 11 + pub uri: Option<String>, 12 + pub created_at: NaiveDateTime, 13 + }
+44
crates/analytics/src/types/track.rs
··· 1 + use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; 2 + use chrono::NaiveDateTime; 3 + use serde::{Deserialize, Serialize}; 4 + 5 + #[derive(Debug, Serialize, Deserialize)] 6 + pub struct Track { 7 + pub id: String, 8 + pub title: String, 9 + pub artist: String, 10 + pub album_artist: String, 11 + pub album_art: Option<String>, 12 + pub album: String, 13 + pub track_number: i32, 14 + pub duration: i32, 15 + pub mb_id: Option<String>, 16 + pub youtube_link: Option<String>, 17 + pub spotify_link: Option<String>, 18 + pub tidal_link: Option<String>, 19 + pub apple_music_link: Option<String>, 20 + pub sha256: String, 21 + pub lyrics: Option<String>, 22 + pub composer: Option<String>, 23 + pub genre: Option<String>, 24 + pub disc_number: i32, 25 + pub copyright_message: Option<String>, 26 + pub label: Option<String>, 27 + pub uri: Option<String>, 28 + pub artist_uri: Option<String>, 29 + pub album_uri: Option<String>, 30 + pub created_at: NaiveDateTime, 31 + } 32 + 33 + impl Responder for Track { 34 + type Body = BoxBody; 35 + 36 + fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> { 37 + let body = serde_json::to_string(&self).unwrap(); 38 + 39 + // Create response and set content type 40 + HttpResponse::Ok() 41 + .content_type(ContentType::json()) 42 + .body(body) 43 + } 44 + }
+24
crates/analytics/src/types/user.rs
··· 1 + use actix_web::{body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder}; 2 + use serde::{Deserialize, Serialize}; 3 + 4 + #[derive(Debug, Serialize, Deserialize)] 5 + pub struct User { 6 + pub id: String, 7 + pub display_name: String, 8 + pub did: String, 9 + pub handle: String, 10 + pub avatar: String, 11 + } 12 + 13 + impl Responder for User { 14 + type Body = BoxBody; 15 + 16 + fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> { 17 + let body = serde_json::to_string(&self).unwrap(); 18 + 19 + // Create response and set content type 20 + HttpResponse::Ok() 21 + .content_type(ContentType::json()) 22 + .body(body) 23 + } 24 + }
crates/analytics/src/types/user_album.rs

This is a binary file and will not be displayed.

crates/analytics/src/types/user_artist.rs

This is a binary file and will not be displayed.

crates/analytics/src/types/user_playlist.rs

This is a binary file and will not be displayed.

crates/analytics/src/types/user_track.rs

This is a binary file and will not be displayed.

+21
crates/analytics/src/xata/album.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 5 + pub struct Album { 6 + pub xata_id: String, 7 + pub title: String, 8 + pub artist: String, 9 + pub release_date: Option<String>, 10 + pub album_art: Option<String>, 11 + pub year: Option<i32>, 12 + pub spotify_link: Option<String>, 13 + pub tidal_link: Option<String>, 14 + pub youtube_link: Option<String>, 15 + pub apple_music_link: Option<String>, 16 + pub sha256: String, 17 + pub uri: Option<String>, 18 + pub artist_uri: Option<String>, 19 + #[serde(with = "chrono::serde::ts_seconds")] 20 + pub xata_createdat: DateTime<Utc>, 21 + }
+8
crates/analytics/src/xata/album_track.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct AlbumTrack { 5 + pub xata_id: String, 6 + pub album_id: String, 7 + pub track_id: String, 8 + }
+23
crates/analytics/src/xata/artist.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 5 + pub struct Artist { 6 + pub xata_id: String, 7 + pub name: String, 8 + pub biography: Option<String>, 9 + #[serde(with = "chrono::serde::ts_seconds_option")] 10 + pub born: Option<DateTime<Utc>>, 11 + pub born_in: Option<String>, 12 + #[serde(with = "chrono::serde::ts_seconds_option")] 13 + pub died: Option<DateTime<Utc>>, 14 + pub picture: Option<String>, 15 + pub sha256: String, 16 + pub spotify_link: Option<String>, 17 + pub tidal_link: Option<String>, 18 + pub youtube_link: Option<String>, 19 + pub apple_music_link: Option<String>, 20 + pub uri: Option<String>, 21 + #[serde(with = "chrono::serde::ts_seconds")] 22 + pub xata_createdat: DateTime<Utc>, 23 + }
+8
crates/analytics/src/xata/artist_album.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct ArtistAlbum { 5 + pub xata_id: String, 6 + pub artist_id: String, 7 + pub album_id: String, 8 + }
+10
crates/analytics/src/xata/artist_track.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct ArtistTrack { 5 + pub xata_id: String, 6 + pub artist_id: String, 7 + pub track_id: String, 8 + #[serde(with = "chrono::serde::ts_seconds")] 9 + pub xata_createdat: chrono::DateTime<chrono::Utc>, 10 + }
+10
crates/analytics/src/xata/loved_track.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct LovedTrack { 5 + pub xata_id: String, 6 + pub user_id: String, 7 + pub track_id: String, 8 + #[serde(with = "chrono::serde::ts_seconds")] 9 + pub xata_createdat: chrono::DateTime<chrono::Utc>, 10 + }
+15
crates/analytics/src/xata/mod.rs
··· 1 + pub mod album; 2 + pub mod album_track; 3 + pub mod artist; 4 + pub mod artist_album; 5 + pub mod artist_track; 6 + pub mod loved_track; 7 + pub mod playlist; 8 + pub mod playlist_track; 9 + pub mod scrobble; 10 + pub mod track; 11 + pub mod user; 12 + pub mod user_album; 13 + pub mod user_artist; 14 + pub mod user_playlist; 15 + pub mod user_track;
+16
crates/analytics/src/xata/playlist.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 5 + pub struct Playlist { 6 + pub xata_id: String, 7 + pub name: String, 8 + pub description: Option<String>, 9 + pub picture: Option<String>, 10 + #[serde(with = "chrono::serde::ts_seconds")] 11 + pub xata_createdat: DateTime<Utc>, 12 + #[serde(with = "chrono::serde::ts_seconds")] 13 + pub xata_updatedat: DateTime<Utc>, 14 + pub uri: Option<String>, 15 + pub created_by: String, 16 + }
+11
crates/analytics/src/xata/playlist_track.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct PlaylistTrack { 5 + pub xata_id: String, 6 + pub playlist_id: String, 7 + pub track_id: String, 8 + pub added_by: String, 9 + #[serde(with = "chrono::serde::ts_seconds")] 10 + pub xata_createdat: chrono::DateTime<chrono::Utc>, 11 + }
+14
crates/analytics/src/xata/scrobble.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 5 + pub struct Scrobble { 6 + pub xata_id: String, 7 + pub user_id: String, 8 + pub track_id: String, 9 + pub album_id: Option<String>, 10 + pub artist_id: Option<String>, 11 + pub uri: Option<String>, 12 + #[serde(with = "chrono::serde::ts_seconds")] 13 + pub xata_createdat: DateTime<Utc>, 14 + }
+31
crates/analytics/src/xata/track.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 5 + pub struct Track { 6 + pub xata_id: String, 7 + pub title: String, 8 + pub artist: String, 9 + pub album_artist: String, 10 + pub album_art: Option<String>, 11 + pub album: String, 12 + pub track_number: i32, 13 + pub duration: i32, 14 + pub mb_id: Option<String>, 15 + pub youtube_link: Option<String>, 16 + pub spotify_link: Option<String>, 17 + pub tidal_link: Option<String>, 18 + pub apple_music_link: Option<String>, 19 + pub sha256: String, 20 + pub lyrics: Option<String>, 21 + pub composer: Option<String>, 22 + pub genre: Option<String>, 23 + pub disc_number: i32, 24 + pub copyright_message: Option<String>, 25 + pub label: Option<String>, 26 + pub uri: Option<String>, 27 + pub artist_uri: Option<String>, 28 + pub album_uri: Option<String>, 29 + #[serde(with = "chrono::serde::ts_seconds")] 30 + pub xata_createdat: DateTime<Utc>, 31 + }
+13
crates/analytics/src/xata/user.rs
··· 1 + use chrono::{DateTime, Utc}; 2 + use serde::Deserialize; 3 + 4 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 5 + pub struct User { 6 + pub xata_id: String, 7 + pub display_name: String, 8 + pub did: String, 9 + pub handle: String, 10 + pub avatar: String, 11 + #[serde(with = "chrono::serde::ts_seconds")] 12 + pub xata_createdat: DateTime<Utc>, 13 + }
+10
crates/analytics/src/xata/user_album.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct UserAlbum { 5 + pub xata_id: String, 6 + pub user_id: String, 7 + pub album_id: String, 8 + #[serde(with = "chrono::serde::ts_seconds")] 9 + pub xata_createdat: chrono::DateTime<chrono::Utc>, 10 + }
+10
crates/analytics/src/xata/user_artist.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct UserArtist { 5 + pub xata_id: String, 6 + pub user_id: String, 7 + pub artist_id: String, 8 + #[serde(with = "chrono::serde::ts_seconds")] 9 + pub xata_createdat: chrono::DateTime<chrono::Utc>, 10 + }
+10
crates/analytics/src/xata/user_playlist.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct UserPlaylist { 5 + pub xata_id: String, 6 + pub user_id: String, 7 + pub playlist_id: String, 8 + #[serde(with = "chrono::serde::ts_seconds")] 9 + pub xata_createdat: chrono::DateTime<chrono::Utc>, 10 + }
+10
crates/analytics/src/xata/user_track.rs
··· 1 + use serde::Deserialize; 2 + 3 + #[derive(Debug, sqlx::FromRow, Deserialize, Clone)] 4 + pub struct UserTrack { 5 + pub xata_id: String, 6 + pub user_id: String, 7 + pub track_id: String, 8 + #[serde(with = "chrono::serde::ts_seconds")] 9 + pub xata_createdat: chrono::DateTime<chrono::Utc>, 10 + }