A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz

[connect] initialize rocksky connect

+7705 -4976
+1174 -572
Cargo.lock
··· 8 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 9 checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" 10 10 dependencies = [ 11 - "bitflags 2.8.0", 11 + "bitflags 2.9.1", 12 12 "bytes", 13 13 "futures-core", 14 14 "futures-sink", ··· 21 21 22 22 [[package]] 23 23 name = "actix-http" 24 - version = "3.9.0" 24 + version = "3.11.0" 25 25 source = "registry+https://github.com/rust-lang/crates.io-index" 26 - checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" 26 + checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2" 27 27 dependencies = [ 28 28 "actix-codec", 29 29 "actix-rt", 30 30 "actix-service", 31 31 "actix-utils", 32 - "ahash 0.8.11", 33 32 "base64 0.22.1", 34 - "bitflags 2.8.0", 33 + "bitflags 2.9.1", 35 34 "brotli", 36 35 "bytes", 37 36 "bytestring", 38 - "derive_more 0.99.19", 37 + "derive_more 2.0.1", 39 38 "encoding_rs", 40 39 "flate2", 40 + "foldhash", 41 41 "futures-core", 42 - "h2", 42 + "h2 0.3.26", 43 43 "http 0.2.12", 44 44 "httparse", 45 45 "httpdate", ··· 49 49 "mime", 50 50 "percent-encoding", 51 51 "pin-project-lite", 52 - "rand 0.8.5", 52 + "rand 0.9.1", 53 53 "sha1", 54 54 "smallvec", 55 55 "tokio", ··· 68 68 "actix-utils", 69 69 "actix-web", 70 70 "chrono", 71 - "derive_more 0.99.19", 71 + "derive_more 0.99.20", 72 72 "log", 73 73 "redis 0.23.3", 74 74 "time", ··· 81 81 checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" 82 82 dependencies = [ 83 83 "quote", 84 - "syn 2.0.98", 84 + "syn 2.0.101", 85 85 ] 86 86 87 87 [[package]] ··· 111 111 112 112 [[package]] 113 113 name = "actix-server" 114 - version = "2.5.0" 114 + version = "2.6.0" 115 115 source = "registry+https://github.com/rust-lang/crates.io-index" 116 - checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" 116 + checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" 117 117 dependencies = [ 118 118 "actix-rt", 119 119 "actix-service", ··· 128 128 129 129 [[package]] 130 130 name = "actix-service" 131 - version = "2.0.2" 131 + version = "2.0.3" 132 132 source = "registry+https://github.com/rust-lang/crates.io-index" 133 - checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" 133 + checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" 134 134 dependencies = [ 135 135 "futures-core", 136 - "paste", 137 136 "pin-project-lite", 138 137 ] 139 138 ··· 148 147 "actix-web", 149 148 "anyhow", 150 149 "async-trait", 151 - "derive_more 0.99.19", 150 + "derive_more 0.99.20", 152 151 "serde", 153 152 "serde_json", 154 153 "tracing", ··· 183 182 184 183 [[package]] 185 184 name = "actix-web" 186 - version = "4.9.0" 185 + version = "4.11.0" 187 186 source = "registry+https://github.com/rust-lang/crates.io-index" 188 - checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" 187 + checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" 189 188 dependencies = [ 190 189 "actix-codec", 191 190 "actix-http", ··· 196 195 "actix-service", 197 196 "actix-utils", 198 197 "actix-web-codegen", 199 - "ahash 0.8.11", 200 198 "bytes", 201 199 "bytestring", 202 200 "cfg-if", 203 201 "cookie", 204 - "derive_more 0.99.19", 202 + "derive_more 2.0.1", 205 203 "encoding_rs", 204 + "foldhash", 206 205 "futures-core", 207 206 "futures-util", 208 207 "impl-more", ··· 220 219 "smallvec", 221 220 "socket2", 222 221 "time", 222 + "tracing", 223 223 "url", 224 224 ] 225 225 ··· 232 232 "actix-router", 233 233 "proc-macro2", 234 234 "quote", 235 - "syn 2.0.98", 235 + "syn 2.0.101", 236 236 ] 237 237 238 238 [[package]] ··· 291 291 source = "registry+https://github.com/rust-lang/crates.io-index" 292 292 checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" 293 293 dependencies = [ 294 - "getrandom 0.2.15", 294 + "getrandom 0.2.16", 295 295 "once_cell", 296 296 "version_check", 297 297 ] 298 298 299 299 [[package]] 300 300 name = "ahash" 301 - version = "0.8.11" 301 + version = "0.8.12" 302 302 source = "registry+https://github.com/rust-lang/crates.io-index" 303 - checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 303 + checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 304 304 dependencies = [ 305 305 "cfg-if", 306 306 "const-random", 307 - "getrandom 0.2.15", 307 + "getrandom 0.3.3", 308 308 "once_cell", 309 309 "version_check", 310 - "zerocopy 0.7.35", 310 + "zerocopy", 311 311 ] 312 312 313 313 [[package]] ··· 416 416 417 417 [[package]] 418 418 name = "anstyle-wincon" 419 - version = "3.0.7" 419 + version = "3.0.8" 420 420 source = "registry+https://github.com/rust-lang/crates.io-index" 421 - checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 421 + checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 422 422 dependencies = [ 423 423 "anstyle", 424 - "once_cell", 424 + "once_cell_polyfill", 425 425 "windows-sys 0.59.0", 426 426 ] 427 427 ··· 439 439 440 440 [[package]] 441 441 name = "argminmax" 442 - version = "0.6.2" 442 + version = "0.6.3" 443 443 source = "registry+https://github.com/rust-lang/crates.io-index" 444 - checksum = "52424b59d69d69d5056d508b260553afd91c57e21849579cd1f50ee8b8b88eaa" 444 + checksum = "70f13d10a41ac8d2ec79ee34178d61e6f47a29c2edfe7ef1721c7383b0359e65" 445 445 dependencies = [ 446 446 "num-traits", 447 447 ] 448 448 449 449 [[package]] 450 450 name = "array-init-cursor" 451 - version = "0.2.0" 451 + version = "0.2.1" 452 452 source = "registry+https://github.com/rust-lang/crates.io-index" 453 - checksum = "bf7d0a018de4f6aa429b9d33d69edf69072b1c5b1cb8d3e4a5f7ef898fc3eb76" 453 + checksum = "ed51fe0f224d1d4ea768be38c51f9f831dee9d05c163c11fba0b8c44387b1fc3" 454 454 455 455 [[package]] 456 456 name = "arrayvec" ··· 460 460 461 461 [[package]] 462 462 name = "arrow" 463 - version = "54.2.0" 463 + version = "54.2.1" 464 464 source = "registry+https://github.com/rust-lang/crates.io-index" 465 - checksum = "755b6da235ac356a869393c23668c663720b8749dd6f15e52b6c214b4b964cc7" 465 + checksum = "dc208515aa0151028e464cc94a692156e945ce5126abd3537bb7fd6ba2143ed1" 466 466 dependencies = [ 467 467 "arrow-arith", 468 468 "arrow-array", ··· 478 478 479 479 [[package]] 480 480 name = "arrow-arith" 481 - version = "54.2.0" 481 + version = "54.2.1" 482 482 source = "registry+https://github.com/rust-lang/crates.io-index" 483 - checksum = "64656a1e0b13ca766f8440752e9a93e11014eec7b67909986f83ed0ab1fe37b8" 483 + checksum = "e07e726e2b3f7816a85c6a45b6ec118eeeabf0b2a8c208122ad949437181f49a" 484 484 dependencies = [ 485 485 "arrow-array", 486 486 "arrow-buffer", ··· 492 492 493 493 [[package]] 494 494 name = "arrow-array" 495 - version = "54.2.0" 495 + version = "54.2.1" 496 496 source = "registry+https://github.com/rust-lang/crates.io-index" 497 - checksum = "57a4a6d2896083cfbdf84a71a863b22460d0708f8206a8373c52e326cc72ea1a" 497 + checksum = "a2262eba4f16c78496adfd559a29fe4b24df6088efc9985a873d58e92be022d5" 498 498 dependencies = [ 499 - "ahash 0.8.11", 499 + "ahash 0.8.12", 500 500 "arrow-buffer", 501 501 "arrow-data", 502 502 "arrow-schema", 503 503 "chrono", 504 504 "half", 505 - "hashbrown 0.15.2", 505 + "hashbrown 0.15.3", 506 506 "num", 507 507 ] 508 508 509 509 [[package]] 510 510 name = "arrow-buffer" 511 - version = "54.2.0" 511 + version = "54.3.1" 512 512 source = "registry+https://github.com/rust-lang/crates.io-index" 513 - checksum = "cef870583ce5e4f3b123c181706f2002fb134960f9a911900f64ba4830c7a43a" 513 + checksum = "263f4801ff1839ef53ebd06f99a56cecd1dbaf314ec893d93168e2e860e0291c" 514 514 dependencies = [ 515 515 "bytes", 516 516 "half", ··· 519 519 520 520 [[package]] 521 521 name = "arrow-cast" 522 - version = "54.2.0" 522 + version = "54.2.1" 523 523 source = "registry+https://github.com/rust-lang/crates.io-index" 524 - checksum = "1ac7eba5a987f8b4a7d9629206ba48e19a1991762795bbe5d08497b7736017ee" 524 + checksum = "4103d88c5b441525ed4ac23153be7458494c2b0c9a11115848fdb9b81f6f886a" 525 525 dependencies = [ 526 526 "arrow-array", 527 527 "arrow-buffer", ··· 540 540 541 541 [[package]] 542 542 name = "arrow-data" 543 - version = "54.2.0" 543 + version = "54.3.1" 544 544 source = "registry+https://github.com/rust-lang/crates.io-index" 545 - checksum = "b095e8a4f3c309544935d53e04c3bfe4eea4e71c3de6fe0416d1f08bb4441a83" 545 + checksum = "61cfdd7d99b4ff618f167e548b2411e5dd2c98c0ddebedd7df433d34c20a4429" 546 546 dependencies = [ 547 547 "arrow-buffer", 548 548 "arrow-schema", ··· 552 552 553 553 [[package]] 554 554 name = "arrow-ord" 555 - version = "54.2.0" 555 + version = "54.2.1" 556 556 source = "registry+https://github.com/rust-lang/crates.io-index" 557 - checksum = "6c07223476f8219d1ace8cd8d85fa18c4ebd8d945013f25ef5c72e85085ca4ee" 557 + checksum = "f841bfcc1997ef6ac48ee0305c4dfceb1f7c786fe31e67c1186edf775e1f1160" 558 558 dependencies = [ 559 559 "arrow-array", 560 560 "arrow-buffer", ··· 565 565 566 566 [[package]] 567 567 name = "arrow-row" 568 - version = "54.2.0" 568 + version = "54.2.1" 569 569 source = "registry+https://github.com/rust-lang/crates.io-index" 570 - checksum = "91b194b38bfd89feabc23e798238989c6648b2506ad639be42ec8eb1658d82c4" 570 + checksum = "1eeb55b0a0a83851aa01f2ca5ee5648f607e8506ba6802577afdda9d75cdedcd" 571 571 dependencies = [ 572 572 "arrow-array", 573 573 "arrow-buffer", ··· 578 578 579 579 [[package]] 580 580 name = "arrow-schema" 581 - version = "54.2.0" 581 + version = "54.3.1" 582 582 source = "registry+https://github.com/rust-lang/crates.io-index" 583 - checksum = "0f40f6be8f78af1ab610db7d9b236e21d587b7168e368a36275d2e5670096735" 583 + checksum = "39cfaf5e440be44db5413b75b72c2a87c1f8f0627117d110264048f2969b99e9" 584 584 dependencies = [ 585 - "bitflags 2.8.0", 585 + "bitflags 2.9.1", 586 586 ] 587 587 588 588 [[package]] 589 589 name = "arrow-select" 590 - version = "54.2.0" 590 + version = "54.2.1" 591 591 source = "registry+https://github.com/rust-lang/crates.io-index" 592 - checksum = "ac265273864a820c4a179fc67182ccc41ea9151b97024e1be956f0f2369c2539" 592 + checksum = "7e2932aece2d0c869dd2125feb9bd1709ef5c445daa3838ac4112dcfa0fda52c" 593 593 dependencies = [ 594 - "ahash 0.8.11", 594 + "ahash 0.8.12", 595 595 "arrow-array", 596 596 "arrow-buffer", 597 597 "arrow-data", ··· 601 601 602 602 [[package]] 603 603 name = "arrow-string" 604 - version = "54.2.0" 604 + version = "54.2.1" 605 605 source = "registry+https://github.com/rust-lang/crates.io-index" 606 - checksum = "d44c8eed43be4ead49128370f7131f054839d3d6003e52aebf64322470b8fbd0" 606 + checksum = "912e38bd6a7a7714c1d9b61df80315685553b7455e8a6045c27531d8ecd5b458" 607 607 dependencies = [ 608 608 "arrow-array", 609 609 "arrow-buffer", ··· 644 644 "thiserror 1.0.69", 645 645 "time", 646 646 "tokio", 647 - "tokio-rustls 0.26.1", 647 + "tokio-rustls 0.26.2", 648 648 "tokio-util", 649 649 "tokio-websockets", 650 650 "tracing", ··· 671 671 dependencies = [ 672 672 "proc-macro2", 673 673 "quote", 674 - "syn 2.0.98", 674 + "syn 2.0.101", 675 675 ] 676 676 677 677 [[package]] 678 678 name = "async-trait" 679 - version = "0.1.86" 679 + version = "0.1.88" 680 680 source = "registry+https://github.com/rust-lang/crates.io-index" 681 - checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" 681 + checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 682 682 dependencies = [ 683 683 "proc-macro2", 684 684 "quote", 685 - "syn 2.0.98", 685 + "syn 2.0.101", 686 686 ] 687 687 688 688 [[package]] ··· 712 712 source = "registry+https://github.com/rust-lang/crates.io-index" 713 713 checksum = "07a9b245ba0739fc90935094c29adbaee3f977218b5fb95e822e261cda7f56a3" 714 714 dependencies = [ 715 - "http 1.2.0", 715 + "http 1.3.1", 716 716 "log", 717 - "rustls 0.23.23", 717 + "rustls 0.23.27", 718 718 "serde", 719 719 "serde_json", 720 720 "url", 721 - "webpki-roots", 721 + "webpki-roots 0.26.11", 722 722 ] 723 723 724 724 [[package]] ··· 755 755 756 756 [[package]] 757 757 name = "backtrace" 758 - version = "0.3.74" 758 + version = "0.3.75" 759 759 source = "registry+https://github.com/rust-lang/crates.io-index" 760 - checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 760 + checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 761 761 dependencies = [ 762 762 "addr2line", 763 763 "cfg-if", ··· 788 788 789 789 [[package]] 790 790 name = "base64ct" 791 - version = "1.6.0" 791 + version = "1.7.3" 792 792 source = "registry+https://github.com/rust-lang/crates.io-index" 793 - checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 793 + checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" 794 794 795 795 [[package]] 796 796 name = "bitflags" ··· 800 800 801 801 [[package]] 802 802 name = "bitflags" 803 - version = "2.8.0" 803 + version = "2.9.1" 804 804 source = "registry+https://github.com/rust-lang/crates.io-index" 805 - checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 805 + checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 806 806 dependencies = [ 807 807 "serde", 808 808 ] ··· 830 830 831 831 [[package]] 832 832 name = "borsh" 833 - version = "1.5.5" 833 + version = "1.5.7" 834 834 source = "registry+https://github.com/rust-lang/crates.io-index" 835 - checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" 835 + checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" 836 836 dependencies = [ 837 837 "borsh-derive", 838 838 "cfg_aliases", ··· 840 840 841 841 [[package]] 842 842 name = "borsh-derive" 843 - version = "1.5.5" 843 + version = "1.5.7" 844 844 source = "registry+https://github.com/rust-lang/crates.io-index" 845 - checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" 845 + checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" 846 846 dependencies = [ 847 847 "once_cell", 848 848 "proc-macro-crate", 849 849 "proc-macro2", 850 850 "quote", 851 - "syn 2.0.98", 851 + "syn 2.0.101", 852 852 ] 853 853 854 854 [[package]] 855 855 name = "brotli" 856 - version = "6.0.0" 856 + version = "8.0.1" 857 857 source = "registry+https://github.com/rust-lang/crates.io-index" 858 - checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" 858 + checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" 859 859 dependencies = [ 860 860 "alloc-no-stdlib", 861 861 "alloc-stdlib", ··· 864 864 865 865 [[package]] 866 866 name = "brotli-decompressor" 867 - version = "4.0.2" 867 + version = "5.0.0" 868 868 source = "registry+https://github.com/rust-lang/crates.io-index" 869 - checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" 869 + checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" 870 870 dependencies = [ 871 871 "alloc-no-stdlib", 872 872 "alloc-stdlib", ··· 902 902 903 903 [[package]] 904 904 name = "bytemuck" 905 - version = "1.21.0" 905 + version = "1.23.0" 906 906 source = "registry+https://github.com/rust-lang/crates.io-index" 907 - checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" 907 + checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" 908 908 dependencies = [ 909 909 "bytemuck_derive", 910 910 ] 911 911 912 912 [[package]] 913 913 name = "bytemuck_derive" 914 - version = "1.8.1" 914 + version = "1.9.3" 915 915 source = "registry+https://github.com/rust-lang/crates.io-index" 916 - checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" 916 + checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" 917 917 dependencies = [ 918 918 "proc-macro2", 919 919 "quote", 920 - "syn 2.0.98", 920 + "syn 2.0.101", 921 921 ] 922 922 923 923 [[package]] ··· 928 928 929 929 [[package]] 930 930 name = "bytes" 931 - version = "1.10.0" 931 + version = "1.10.1" 932 932 source = "registry+https://github.com/rust-lang/crates.io-index" 933 - checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" 933 + checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 934 934 dependencies = [ 935 935 "serde", 936 936 ] ··· 961 961 962 962 [[package]] 963 963 name = "cc" 964 - version = "1.2.14" 964 + version = "1.2.24" 965 965 source = "registry+https://github.com/rust-lang/crates.io-index" 966 - checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" 966 + checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" 967 967 dependencies = [ 968 968 "jobserver", 969 969 "libc", 970 970 "shlex", 971 971 ] 972 + 973 + [[package]] 974 + name = "cesu8" 975 + version = "1.1.0" 976 + source = "registry+https://github.com/rust-lang/crates.io-index" 977 + checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 972 978 973 979 [[package]] 974 980 name = "cfg-if" ··· 999 1005 1000 1006 [[package]] 1001 1007 name = "chrono-tz" 1002 - version = "0.10.1" 1008 + version = "0.10.3" 1003 1009 source = "registry+https://github.com/rust-lang/crates.io-index" 1004 - checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f" 1010 + checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" 1005 1011 dependencies = [ 1006 1012 "chrono", 1007 1013 "chrono-tz-build", ··· 1010 1016 1011 1017 [[package]] 1012 1018 name = "chrono-tz-build" 1013 - version = "0.4.0" 1019 + version = "0.4.1" 1014 1020 source = "registry+https://github.com/rust-lang/crates.io-index" 1015 - checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" 1021 + checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" 1016 1022 dependencies = [ 1017 1023 "parse-zoneinfo", 1018 1024 "phf_codegen", ··· 1030 1036 1031 1037 [[package]] 1032 1038 name = "clap" 1033 - version = "4.5.31" 1039 + version = "4.5.38" 1034 1040 source = "registry+https://github.com/rust-lang/crates.io-index" 1035 - checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" 1041 + checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 1036 1042 dependencies = [ 1037 1043 "clap_builder", 1038 1044 ] 1039 1045 1040 1046 [[package]] 1041 1047 name = "clap_builder" 1042 - version = "4.5.31" 1048 + version = "4.5.38" 1043 1049 source = "registry+https://github.com/rust-lang/crates.io-index" 1044 - checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" 1050 + checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 1045 1051 dependencies = [ 1046 1052 "anstream", 1047 1053 "anstyle", ··· 1111 1117 ] 1112 1118 1113 1119 [[package]] 1120 + name = "connect" 1121 + version = "0.1.0" 1122 + dependencies = [ 1123 + "anyhow", 1124 + "async-trait", 1125 + "base64 0.22.1", 1126 + "dirs", 1127 + "futures-util", 1128 + "http 1.3.1", 1129 + "jsonrpsee", 1130 + "owo-colors", 1131 + "reqwest", 1132 + "serde", 1133 + "serde_json", 1134 + "tokio", 1135 + "tokio-stream", 1136 + "tokio-tungstenite", 1137 + "tungstenite", 1138 + ] 1139 + 1140 + [[package]] 1114 1141 name = "const-oid" 1115 1142 version = "0.9.6" 1116 1143 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1131 1158 source = "registry+https://github.com/rust-lang/crates.io-index" 1132 1159 checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" 1133 1160 dependencies = [ 1134 - "getrandom 0.2.15", 1161 + "getrandom 0.2.16", 1135 1162 "once_cell", 1136 1163 "tiny-keccak", 1137 1164 ] ··· 1171 1198 ] 1172 1199 1173 1200 [[package]] 1201 + name = "core-foundation" 1202 + version = "0.10.0" 1203 + source = "registry+https://github.com/rust-lang/crates.io-index" 1204 + checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" 1205 + dependencies = [ 1206 + "core-foundation-sys", 1207 + "libc", 1208 + ] 1209 + 1210 + [[package]] 1174 1211 name = "core-foundation-sys" 1175 1212 version = "0.8.7" 1176 1213 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1187 1224 1188 1225 [[package]] 1189 1226 name = "crc" 1190 - version = "3.2.1" 1227 + version = "3.3.0" 1191 1228 source = "registry+https://github.com/rust-lang/crates.io-index" 1192 - checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" 1229 + checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" 1193 1230 dependencies = [ 1194 1231 "crc-catalog", 1195 1232 ] ··· 1211 1248 1212 1249 [[package]] 1213 1250 name = "crossbeam-channel" 1214 - version = "0.5.14" 1251 + version = "0.5.15" 1215 1252 source = "registry+https://github.com/rust-lang/crates.io-index" 1216 - checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" 1253 + checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 1217 1254 dependencies = [ 1218 1255 "crossbeam-utils", 1219 1256 ] ··· 1258 1295 source = "registry+https://github.com/rust-lang/crates.io-index" 1259 1296 checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 1260 1297 dependencies = [ 1261 - "bitflags 2.8.0", 1298 + "bitflags 2.9.1", 1262 1299 "crossterm_winapi", 1263 1300 "parking_lot", 1264 1301 "rustix 0.38.44", ··· 1323 1360 dependencies = [ 1324 1361 "proc-macro2", 1325 1362 "quote", 1326 - "syn 2.0.98", 1363 + "syn 2.0.101", 1327 1364 ] 1328 1365 1329 1366 [[package]] 1330 1367 name = "data-encoding" 1331 - version = "2.8.0" 1368 + version = "2.9.0" 1332 1369 source = "registry+https://github.com/rust-lang/crates.io-index" 1333 - checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" 1370 + checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" 1334 1371 1335 1372 [[package]] 1336 1373 name = "der" 1337 - version = "0.7.9" 1374 + version = "0.7.10" 1338 1375 source = "registry+https://github.com/rust-lang/crates.io-index" 1339 - checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" 1376 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 1340 1377 dependencies = [ 1341 1378 "const-oid", 1342 1379 "pem-rfc7468", ··· 1345 1382 1346 1383 [[package]] 1347 1384 name = "deranged" 1348 - version = "0.3.11" 1385 + version = "0.4.0" 1349 1386 source = "registry+https://github.com/rust-lang/crates.io-index" 1350 - checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 1387 + checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 1351 1388 dependencies = [ 1352 1389 "powerfmt", 1353 1390 "serde", ··· 1355 1392 1356 1393 [[package]] 1357 1394 name = "derive_more" 1358 - version = "0.99.19" 1395 + version = "0.99.20" 1359 1396 source = "registry+https://github.com/rust-lang/crates.io-index" 1360 - checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" 1397 + checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" 1361 1398 dependencies = [ 1362 1399 "convert_case", 1363 1400 "proc-macro2", 1364 1401 "quote", 1365 1402 "rustc_version", 1366 - "syn 2.0.98", 1403 + "syn 2.0.101", 1367 1404 ] 1368 1405 1369 1406 [[package]] ··· 1372 1409 source = "registry+https://github.com/rust-lang/crates.io-index" 1373 1410 checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 1374 1411 dependencies = [ 1375 - "derive_more-impl", 1412 + "derive_more-impl 1.0.0", 1413 + ] 1414 + 1415 + [[package]] 1416 + name = "derive_more" 1417 + version = "2.0.1" 1418 + source = "registry+https://github.com/rust-lang/crates.io-index" 1419 + checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 1420 + dependencies = [ 1421 + "derive_more-impl 2.0.1", 1376 1422 ] 1377 1423 1378 1424 [[package]] ··· 1383 1429 dependencies = [ 1384 1430 "proc-macro2", 1385 1431 "quote", 1386 - "syn 2.0.98", 1432 + "syn 2.0.101", 1433 + "unicode-xid", 1434 + ] 1435 + 1436 + [[package]] 1437 + name = "derive_more-impl" 1438 + version = "2.0.1" 1439 + source = "registry+https://github.com/rust-lang/crates.io-index" 1440 + checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 1441 + dependencies = [ 1442 + "proc-macro2", 1443 + "quote", 1444 + "syn 2.0.101", 1387 1445 "unicode-xid", 1388 1446 ] 1389 1447 ··· 1400 1458 ] 1401 1459 1402 1460 [[package]] 1461 + name = "dirs" 1462 + version = "6.0.0" 1463 + source = "registry+https://github.com/rust-lang/crates.io-index" 1464 + checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 1465 + dependencies = [ 1466 + "dirs-sys", 1467 + ] 1468 + 1469 + [[package]] 1470 + name = "dirs-sys" 1471 + version = "0.5.0" 1472 + source = "registry+https://github.com/rust-lang/crates.io-index" 1473 + checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 1474 + dependencies = [ 1475 + "libc", 1476 + "option-ext", 1477 + "redox_users", 1478 + "windows-sys 0.59.0", 1479 + ] 1480 + 1481 + [[package]] 1403 1482 name = "displaydoc" 1404 1483 version = "0.2.5" 1405 1484 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1407 1486 dependencies = [ 1408 1487 "proc-macro2", 1409 1488 "quote", 1410 - "syn 2.0.98", 1489 + "syn 2.0.101", 1411 1490 ] 1412 1491 1413 1492 [[package]] ··· 1449 1528 "lofty", 1450 1529 "md5", 1451 1530 "owo-colors", 1452 - "redis 0.29.0", 1531 + "redis 0.29.5", 1453 1532 "reqwest", 1454 1533 "serde", 1455 1534 "serde_json", ··· 1463 1542 1464 1543 [[package]] 1465 1544 name = "duckdb" 1466 - version = "1.2.0" 1545 + version = "1.2.2" 1467 1546 source = "registry+https://github.com/rust-lang/crates.io-index" 1468 - checksum = "8e2093a18d0c07e411104a9d27ef0097872172552ad5774feba304c2b47f382c" 1547 + checksum = "49ac283b6621e3becf8014d1efa655522794075834c72f744573debef9c9f6c8" 1469 1548 dependencies = [ 1470 1549 "arrow", 1471 1550 "cast", ··· 1483 1562 1484 1563 [[package]] 1485 1564 name = "dyn-clone" 1486 - version = "1.0.18" 1565 + version = "1.0.19" 1487 1566 source = "registry+https://github.com/rust-lang/crates.io-index" 1488 - checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" 1567 + checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" 1489 1568 1490 1569 [[package]] 1491 1570 name = "ed25519" ··· 1511 1590 1512 1591 [[package]] 1513 1592 name = "either" 1514 - version = "1.13.0" 1593 + version = "1.15.0" 1515 1594 source = "registry+https://github.com/rust-lang/crates.io-index" 1516 - checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 1595 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 1517 1596 dependencies = [ 1518 1597 "serde", 1519 1598 ] ··· 1536 1615 "once_cell", 1537 1616 "proc-macro2", 1538 1617 "quote", 1539 - "syn 2.0.98", 1618 + "syn 2.0.101", 1540 1619 ] 1541 1620 1542 1621 [[package]] ··· 1547 1626 1548 1627 [[package]] 1549 1628 name = "errno" 1550 - version = "0.3.10" 1629 + version = "0.3.12" 1551 1630 source = "registry+https://github.com/rust-lang/crates.io-index" 1552 - checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 1631 + checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 1553 1632 dependencies = [ 1554 1633 "libc", 1555 1634 "windows-sys 0.59.0", ··· 1568 1647 1569 1648 [[package]] 1570 1649 name = "ethnum" 1571 - version = "1.5.0" 1650 + version = "1.5.2" 1572 1651 source = "registry+https://github.com/rust-lang/crates.io-index" 1573 - checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" 1652 + checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" 1574 1653 1575 1654 [[package]] 1576 1655 name = "event-listener" ··· 1633 1712 1634 1713 [[package]] 1635 1714 name = "flate2" 1636 - version = "1.1.0" 1715 + version = "1.1.1" 1637 1716 source = "registry+https://github.com/rust-lang/crates.io-index" 1638 - checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" 1717 + checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 1639 1718 dependencies = [ 1640 1719 "crc32fast", 1641 1720 "miniz_oxide", ··· 1660 1739 1661 1740 [[package]] 1662 1741 name = "foldhash" 1663 - version = "0.1.4" 1742 + version = "0.1.5" 1664 1743 source = "registry+https://github.com/rust-lang/crates.io-index" 1665 - checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 1744 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 1666 1745 1667 1746 [[package]] 1668 1747 name = "form_urlencoded" ··· 1746 1825 dependencies = [ 1747 1826 "proc-macro2", 1748 1827 "quote", 1749 - "syn 2.0.98", 1828 + "syn 2.0.101", 1750 1829 ] 1751 1830 1752 1831 [[package]] ··· 1762 1841 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 1763 1842 1764 1843 [[package]] 1844 + name = "futures-timer" 1845 + version = "3.0.3" 1846 + source = "registry+https://github.com/rust-lang/crates.io-index" 1847 + checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 1848 + dependencies = [ 1849 + "gloo-timers", 1850 + "send_wrapper", 1851 + ] 1852 + 1853 + [[package]] 1765 1854 name = "futures-util" 1766 1855 version = "0.3.31" 1767 1856 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1791 1880 1792 1881 [[package]] 1793 1882 name = "getrandom" 1794 - version = "0.2.15" 1883 + version = "0.2.16" 1795 1884 source = "registry+https://github.com/rust-lang/crates.io-index" 1796 - checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 1885 + checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 1797 1886 dependencies = [ 1798 1887 "cfg-if", 1799 1888 "js-sys", ··· 1804 1893 1805 1894 [[package]] 1806 1895 name = "getrandom" 1807 - version = "0.3.1" 1896 + version = "0.3.3" 1808 1897 source = "registry+https://github.com/rust-lang/crates.io-index" 1809 - checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 1898 + checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 1810 1899 dependencies = [ 1811 1900 "cfg-if", 1901 + "js-sys", 1812 1902 "libc", 1813 - "wasi 0.13.3+wasi-0.2.2", 1814 - "windows-targets 0.52.6", 1903 + "r-efi", 1904 + "wasi 0.14.2+wasi-0.2.4", 1905 + "wasm-bindgen", 1815 1906 ] 1816 1907 1817 1908 [[package]] ··· 1837 1928 checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 1838 1929 1839 1930 [[package]] 1931 + name = "gloo-net" 1932 + version = "0.6.0" 1933 + source = "registry+https://github.com/rust-lang/crates.io-index" 1934 + checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" 1935 + dependencies = [ 1936 + "futures-channel", 1937 + "futures-core", 1938 + "futures-sink", 1939 + "gloo-utils", 1940 + "http 1.3.1", 1941 + "js-sys", 1942 + "pin-project", 1943 + "serde", 1944 + "serde_json", 1945 + "thiserror 1.0.69", 1946 + "wasm-bindgen", 1947 + "wasm-bindgen-futures", 1948 + "web-sys", 1949 + ] 1950 + 1951 + [[package]] 1952 + name = "gloo-timers" 1953 + version = "0.2.6" 1954 + source = "registry+https://github.com/rust-lang/crates.io-index" 1955 + checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" 1956 + dependencies = [ 1957 + "futures-channel", 1958 + "futures-core", 1959 + "js-sys", 1960 + "wasm-bindgen", 1961 + ] 1962 + 1963 + [[package]] 1964 + name = "gloo-utils" 1965 + version = "0.2.0" 1966 + source = "registry+https://github.com/rust-lang/crates.io-index" 1967 + checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" 1968 + dependencies = [ 1969 + "js-sys", 1970 + "serde", 1971 + "serde_json", 1972 + "wasm-bindgen", 1973 + "web-sys", 1974 + ] 1975 + 1976 + [[package]] 1840 1977 name = "googledrive" 1841 1978 version = "0.1.0" 1842 1979 dependencies = [ ··· 1854 1991 "lofty", 1855 1992 "md5", 1856 1993 "owo-colors", 1857 - "redis 0.29.0", 1994 + "redis 0.29.5", 1858 1995 "reqwest", 1859 1996 "serde", 1860 1997 "serde_json", ··· 1887 2024 ] 1888 2025 1889 2026 [[package]] 2027 + name = "h2" 2028 + version = "0.4.10" 2029 + source = "registry+https://github.com/rust-lang/crates.io-index" 2030 + checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" 2031 + dependencies = [ 2032 + "atomic-waker", 2033 + "bytes", 2034 + "fnv", 2035 + "futures-core", 2036 + "futures-sink", 2037 + "http 1.3.1", 2038 + "indexmap", 2039 + "slab", 2040 + "tokio", 2041 + "tokio-util", 2042 + "tracing", 2043 + ] 2044 + 2045 + [[package]] 1890 2046 name = "half" 1891 - version = "2.4.1" 2047 + version = "2.6.0" 1892 2048 source = "registry+https://github.com/rust-lang/crates.io-index" 1893 - checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 2049 + checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 1894 2050 dependencies = [ 1895 2051 "cfg-if", 1896 2052 "crunchy", ··· 1912 2068 source = "registry+https://github.com/rust-lang/crates.io-index" 1913 2069 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 1914 2070 dependencies = [ 1915 - "ahash 0.8.11", 2071 + "ahash 0.8.12", 1916 2072 "allocator-api2", 1917 2073 "rayon", 1918 2074 "serde", ··· 1920 2076 1921 2077 [[package]] 1922 2078 name = "hashbrown" 1923 - version = "0.15.2" 2079 + version = "0.15.3" 1924 2080 source = "registry+https://github.com/rust-lang/crates.io-index" 1925 - checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 2081 + checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 1926 2082 dependencies = [ 1927 2083 "allocator-api2", 1928 2084 "equivalent", ··· 1946 2102 source = "registry+https://github.com/rust-lang/crates.io-index" 1947 2103 checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 1948 2104 dependencies = [ 1949 - "hashbrown 0.15.2", 2105 + "hashbrown 0.15.3", 1950 2106 ] 1951 2107 1952 2108 [[package]] ··· 2007 2163 2008 2164 [[package]] 2009 2165 name = "http" 2010 - version = "1.2.0" 2166 + version = "1.3.1" 2011 2167 source = "registry+https://github.com/rust-lang/crates.io-index" 2012 - checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" 2168 + checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 2013 2169 dependencies = [ 2014 2170 "bytes", 2015 2171 "fnv", ··· 2034 2190 checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 2035 2191 dependencies = [ 2036 2192 "bytes", 2037 - "http 1.2.0", 2193 + "http 1.3.1", 2038 2194 ] 2039 2195 2040 2196 [[package]] 2041 2197 name = "http-body-util" 2042 - version = "0.1.2" 2198 + version = "0.1.3" 2043 2199 source = "registry+https://github.com/rust-lang/crates.io-index" 2044 - checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 2200 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 2045 2201 dependencies = [ 2046 2202 "bytes", 2047 - "futures-util", 2048 - "http 1.2.0", 2203 + "futures-core", 2204 + "http 1.3.1", 2049 2205 "http-body 1.0.1", 2050 2206 "pin-project-lite", 2051 2207 ] 2052 2208 2053 2209 [[package]] 2054 2210 name = "httparse" 2055 - version = "1.10.0" 2211 + version = "1.10.1" 2056 2212 source = "registry+https://github.com/rust-lang/crates.io-index" 2057 - checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" 2213 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 2058 2214 2059 2215 [[package]] 2060 2216 name = "httpdate" ··· 2094 2250 "bytes", 2095 2251 "futures-channel", 2096 2252 "futures-util", 2097 - "http 1.2.0", 2253 + "h2 0.4.10", 2254 + "http 1.3.1", 2098 2255 "http-body 1.0.1", 2099 2256 "httparse", 2100 2257 "itoa", ··· 2120 2277 2121 2278 [[package]] 2122 2279 name = "hyper-rustls" 2123 - version = "0.27.5" 2280 + version = "0.27.6" 2124 2281 source = "registry+https://github.com/rust-lang/crates.io-index" 2125 - checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 2282 + checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" 2126 2283 dependencies = [ 2127 - "futures-util", 2128 - "http 1.2.0", 2284 + "http 1.3.1", 2129 2285 "hyper 1.6.0", 2130 2286 "hyper-util", 2131 - "rustls 0.23.23", 2287 + "log", 2288 + "rustls 0.23.27", 2132 2289 "rustls-pki-types", 2133 2290 "tokio", 2134 - "tokio-rustls 0.26.1", 2291 + "tokio-rustls 0.26.2", 2135 2292 "tower-service", 2136 - "webpki-roots", 2293 + "webpki-roots 1.0.0", 2137 2294 ] 2138 2295 2139 2296 [[package]] 2140 2297 name = "hyper-util" 2141 - version = "0.1.10" 2298 + version = "0.1.12" 2142 2299 source = "registry+https://github.com/rust-lang/crates.io-index" 2143 - checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 2300 + checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" 2144 2301 dependencies = [ 2145 2302 "bytes", 2146 2303 "futures-channel", 2147 2304 "futures-util", 2148 - "http 1.2.0", 2305 + "http 1.3.1", 2149 2306 "http-body 1.0.1", 2150 2307 "hyper 1.6.0", 2308 + "libc", 2151 2309 "pin-project-lite", 2152 2310 "socket2", 2153 2311 "tokio", ··· 2157 2315 2158 2316 [[package]] 2159 2317 name = "iana-time-zone" 2160 - version = "0.1.61" 2318 + version = "0.1.63" 2161 2319 source = "registry+https://github.com/rust-lang/crates.io-index" 2162 - checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 2320 + checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 2163 2321 dependencies = [ 2164 2322 "android_system_properties", 2165 2323 "core-foundation-sys", 2166 2324 "iana-time-zone-haiku", 2167 2325 "js-sys", 2326 + "log", 2168 2327 "wasm-bindgen", 2169 - "windows-core 0.52.0", 2328 + "windows-core 0.61.2", 2170 2329 ] 2171 2330 2172 2331 [[package]] ··· 2180 2339 2181 2340 [[package]] 2182 2341 name = "icu_collections" 2183 - version = "1.5.0" 2342 + version = "2.0.0" 2184 2343 source = "registry+https://github.com/rust-lang/crates.io-index" 2185 - checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 2344 + checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 2186 2345 dependencies = [ 2187 2346 "displaydoc", 2347 + "potential_utf", 2188 2348 "yoke", 2189 2349 "zerofrom", 2190 2350 "zerovec", 2191 2351 ] 2192 2352 2193 2353 [[package]] 2194 - name = "icu_locid" 2195 - version = "1.5.0" 2354 + name = "icu_locale_core" 2355 + version = "2.0.0" 2196 2356 source = "registry+https://github.com/rust-lang/crates.io-index" 2197 - checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 2357 + checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 2198 2358 dependencies = [ 2199 2359 "displaydoc", 2200 2360 "litemap", ··· 2204 2364 ] 2205 2365 2206 2366 [[package]] 2207 - name = "icu_locid_transform" 2208 - version = "1.5.0" 2209 - source = "registry+https://github.com/rust-lang/crates.io-index" 2210 - checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 2211 - dependencies = [ 2212 - "displaydoc", 2213 - "icu_locid", 2214 - "icu_locid_transform_data", 2215 - "icu_provider", 2216 - "tinystr", 2217 - "zerovec", 2218 - ] 2219 - 2220 - [[package]] 2221 - name = "icu_locid_transform_data" 2222 - version = "1.5.0" 2223 - source = "registry+https://github.com/rust-lang/crates.io-index" 2224 - checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 2225 - 2226 - [[package]] 2227 2367 name = "icu_normalizer" 2228 - version = "1.5.0" 2368 + version = "2.0.0" 2229 2369 source = "registry+https://github.com/rust-lang/crates.io-index" 2230 - checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 2370 + checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 2231 2371 dependencies = [ 2232 2372 "displaydoc", 2233 2373 "icu_collections", ··· 2235 2375 "icu_properties", 2236 2376 "icu_provider", 2237 2377 "smallvec", 2238 - "utf16_iter", 2239 - "utf8_iter", 2240 - "write16", 2241 2378 "zerovec", 2242 2379 ] 2243 2380 2244 2381 [[package]] 2245 2382 name = "icu_normalizer_data" 2246 - version = "1.5.0" 2383 + version = "2.0.0" 2247 2384 source = "registry+https://github.com/rust-lang/crates.io-index" 2248 - checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 2385 + checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 2249 2386 2250 2387 [[package]] 2251 2388 name = "icu_properties" 2252 - version = "1.5.1" 2389 + version = "2.0.1" 2253 2390 source = "registry+https://github.com/rust-lang/crates.io-index" 2254 - checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 2391 + checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 2255 2392 dependencies = [ 2256 2393 "displaydoc", 2257 2394 "icu_collections", 2258 - "icu_locid_transform", 2395 + "icu_locale_core", 2259 2396 "icu_properties_data", 2260 2397 "icu_provider", 2261 - "tinystr", 2398 + "potential_utf", 2399 + "zerotrie", 2262 2400 "zerovec", 2263 2401 ] 2264 2402 2265 2403 [[package]] 2266 2404 name = "icu_properties_data" 2267 - version = "1.5.0" 2405 + version = "2.0.1" 2268 2406 source = "registry+https://github.com/rust-lang/crates.io-index" 2269 - checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 2407 + checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 2270 2408 2271 2409 [[package]] 2272 2410 name = "icu_provider" 2273 - version = "1.5.0" 2411 + version = "2.0.0" 2274 2412 source = "registry+https://github.com/rust-lang/crates.io-index" 2275 - checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 2413 + checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 2276 2414 dependencies = [ 2277 2415 "displaydoc", 2278 - "icu_locid", 2279 - "icu_provider_macros", 2416 + "icu_locale_core", 2280 2417 "stable_deref_trait", 2281 2418 "tinystr", 2282 2419 "writeable", 2283 2420 "yoke", 2284 2421 "zerofrom", 2422 + "zerotrie", 2285 2423 "zerovec", 2286 2424 ] 2287 2425 2288 2426 [[package]] 2289 - name = "icu_provider_macros" 2290 - version = "1.5.0" 2291 - source = "registry+https://github.com/rust-lang/crates.io-index" 2292 - checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 2293 - dependencies = [ 2294 - "proc-macro2", 2295 - "quote", 2296 - "syn 2.0.98", 2297 - ] 2298 - 2299 - [[package]] 2300 2427 name = "idna" 2301 2428 version = "1.0.3" 2302 2429 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2309 2436 2310 2437 [[package]] 2311 2438 name = "idna_adapter" 2312 - version = "1.2.0" 2439 + version = "1.2.1" 2313 2440 source = "registry+https://github.com/rust-lang/crates.io-index" 2314 - checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 2441 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 2315 2442 dependencies = [ 2316 2443 "icu_normalizer", 2317 2444 "icu_properties", ··· 2325 2452 2326 2453 [[package]] 2327 2454 name = "indexmap" 2328 - version = "2.7.1" 2455 + version = "2.9.0" 2329 2456 source = "registry+https://github.com/rust-lang/crates.io-index" 2330 - checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 2457 + checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 2331 2458 dependencies = [ 2332 2459 "equivalent", 2333 - "hashbrown 0.15.2", 2460 + "hashbrown 0.15.3", 2334 2461 "serde", 2335 2462 ] 2336 2463 2337 2464 [[package]] 2338 2465 name = "inout" 2339 - version = "0.1.3" 2466 + version = "0.1.4" 2340 2467 source = "registry+https://github.com/rust-lang/crates.io-index" 2341 - checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 2468 + checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 2342 2469 dependencies = [ 2343 2470 "generic-array", 2344 2471 ] ··· 2357 2484 2358 2485 [[package]] 2359 2486 name = "itoa" 2360 - version = "1.0.14" 2487 + version = "1.0.15" 2361 2488 source = "registry+https://github.com/rust-lang/crates.io-index" 2362 - checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 2489 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 2363 2490 2364 2491 [[package]] 2365 2492 name = "jetstream" ··· 2384 2511 ] 2385 2512 2386 2513 [[package]] 2514 + name = "jni" 2515 + version = "0.21.1" 2516 + source = "registry+https://github.com/rust-lang/crates.io-index" 2517 + checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 2518 + dependencies = [ 2519 + "cesu8", 2520 + "cfg-if", 2521 + "combine", 2522 + "jni-sys", 2523 + "log", 2524 + "thiserror 1.0.69", 2525 + "walkdir", 2526 + "windows-sys 0.45.0", 2527 + ] 2528 + 2529 + [[package]] 2530 + name = "jni-sys" 2531 + version = "0.3.0" 2532 + source = "registry+https://github.com/rust-lang/crates.io-index" 2533 + checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 2534 + 2535 + [[package]] 2387 2536 name = "jobserver" 2388 - version = "0.1.32" 2537 + version = "0.1.33" 2389 2538 source = "registry+https://github.com/rust-lang/crates.io-index" 2390 - checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 2539 + checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 2391 2540 dependencies = [ 2541 + "getrandom 0.3.3", 2392 2542 "libc", 2393 2543 ] 2394 2544 ··· 2403 2553 ] 2404 2554 2405 2555 [[package]] 2556 + name = "jsonrpsee" 2557 + version = "0.25.1" 2558 + source = "registry+https://github.com/rust-lang/crates.io-index" 2559 + checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" 2560 + dependencies = [ 2561 + "jsonrpsee-client-transport", 2562 + "jsonrpsee-core", 2563 + "jsonrpsee-http-client", 2564 + "jsonrpsee-types", 2565 + "jsonrpsee-wasm-client", 2566 + "jsonrpsee-ws-client", 2567 + "tokio", 2568 + ] 2569 + 2570 + [[package]] 2571 + name = "jsonrpsee-client-transport" 2572 + version = "0.25.1" 2573 + source = "registry+https://github.com/rust-lang/crates.io-index" 2574 + checksum = "a2a320a3f1464e4094f780c4d48413acd786ce5627aaaecfac9e9c7431d13ae1" 2575 + dependencies = [ 2576 + "base64 0.22.1", 2577 + "futures-channel", 2578 + "futures-util", 2579 + "gloo-net", 2580 + "http 1.3.1", 2581 + "jsonrpsee-core", 2582 + "pin-project", 2583 + "rustls 0.23.27", 2584 + "rustls-pki-types", 2585 + "rustls-platform-verifier", 2586 + "soketto", 2587 + "thiserror 2.0.12", 2588 + "tokio", 2589 + "tokio-rustls 0.26.2", 2590 + "tokio-util", 2591 + "tracing", 2592 + "url", 2593 + ] 2594 + 2595 + [[package]] 2596 + name = "jsonrpsee-core" 2597 + version = "0.25.1" 2598 + source = "registry+https://github.com/rust-lang/crates.io-index" 2599 + checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" 2600 + dependencies = [ 2601 + "async-trait", 2602 + "bytes", 2603 + "futures-timer", 2604 + "futures-util", 2605 + "http 1.3.1", 2606 + "http-body 1.0.1", 2607 + "http-body-util", 2608 + "jsonrpsee-types", 2609 + "pin-project", 2610 + "rustc-hash", 2611 + "serde", 2612 + "serde_json", 2613 + "thiserror 2.0.12", 2614 + "tokio", 2615 + "tokio-stream", 2616 + "tower", 2617 + "tracing", 2618 + "wasm-bindgen-futures", 2619 + ] 2620 + 2621 + [[package]] 2622 + name = "jsonrpsee-http-client" 2623 + version = "0.25.1" 2624 + source = "registry+https://github.com/rust-lang/crates.io-index" 2625 + checksum = "6962d2bd295f75e97dd328891e58fce166894b974c1f7ce2e7597f02eeceb791" 2626 + dependencies = [ 2627 + "base64 0.22.1", 2628 + "http-body 1.0.1", 2629 + "hyper 1.6.0", 2630 + "hyper-rustls 0.27.6", 2631 + "hyper-util", 2632 + "jsonrpsee-core", 2633 + "jsonrpsee-types", 2634 + "rustls 0.23.27", 2635 + "rustls-platform-verifier", 2636 + "serde", 2637 + "serde_json", 2638 + "thiserror 2.0.12", 2639 + "tokio", 2640 + "tower", 2641 + "url", 2642 + ] 2643 + 2644 + [[package]] 2645 + name = "jsonrpsee-types" 2646 + version = "0.25.1" 2647 + source = "registry+https://github.com/rust-lang/crates.io-index" 2648 + checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" 2649 + dependencies = [ 2650 + "http 1.3.1", 2651 + "serde", 2652 + "serde_json", 2653 + "thiserror 2.0.12", 2654 + ] 2655 + 2656 + [[package]] 2657 + name = "jsonrpsee-wasm-client" 2658 + version = "0.25.1" 2659 + source = "registry+https://github.com/rust-lang/crates.io-index" 2660 + checksum = "6b67695cbcf4653f39f8f8738925547e0e23fd9fe315bccf951097b9f6a38781" 2661 + dependencies = [ 2662 + "jsonrpsee-client-transport", 2663 + "jsonrpsee-core", 2664 + "jsonrpsee-types", 2665 + "tower", 2666 + ] 2667 + 2668 + [[package]] 2669 + name = "jsonrpsee-ws-client" 2670 + version = "0.25.1" 2671 + source = "registry+https://github.com/rust-lang/crates.io-index" 2672 + checksum = "2da2694c9ff271a9d3ebfe520f6b36820e85133a51be77a3cb549fd615095261" 2673 + dependencies = [ 2674 + "http 1.3.1", 2675 + "jsonrpsee-client-transport", 2676 + "jsonrpsee-core", 2677 + "jsonrpsee-types", 2678 + "tower", 2679 + "url", 2680 + ] 2681 + 2682 + [[package]] 2406 2683 name = "jsonwebtoken" 2407 2684 version = "9.3.1" 2408 2685 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2498 2775 2499 2776 [[package]] 2500 2777 name = "libc" 2501 - version = "0.2.169" 2778 + version = "0.2.172" 2502 2779 source = "registry+https://github.com/rust-lang/crates.io-index" 2503 - checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 2780 + checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 2504 2781 2505 2782 [[package]] 2506 2783 name = "libduckdb-sys" 2507 - version = "1.2.0" 2784 + version = "1.2.2" 2508 2785 source = "registry+https://github.com/rust-lang/crates.io-index" 2509 - checksum = "dc4020eaf07df4927b5205cd200ca2a5ed0798b49652dec22e09384ba8efa163" 2786 + checksum = "12cac9d03484c43fefac8b2066a253c9b0b3b0cd02cbe02a9ea2312f7e382618" 2510 2787 dependencies = [ 2511 2788 "autocfg", 2512 2789 "flate2", ··· 2519 2796 2520 2797 [[package]] 2521 2798 name = "libm" 2522 - version = "0.2.11" 2799 + version = "0.2.15" 2523 2800 source = "registry+https://github.com/rust-lang/crates.io-index" 2524 - checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" 2801 + checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 2525 2802 2526 2803 [[package]] 2527 2804 name = "libredox" ··· 2529 2806 source = "registry+https://github.com/rust-lang/crates.io-index" 2530 2807 checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 2531 2808 dependencies = [ 2532 - "bitflags 2.8.0", 2809 + "bitflags 2.9.1", 2533 2810 "libc", 2534 2811 "redox_syscall", 2535 2812 ] ··· 2552 2829 2553 2830 [[package]] 2554 2831 name = "linux-raw-sys" 2555 - version = "0.9.3" 2832 + version = "0.9.4" 2556 2833 source = "registry+https://github.com/rust-lang/crates.io-index" 2557 - checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 2834 + checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 2558 2835 2559 2836 [[package]] 2560 2837 name = "litemap" 2561 - version = "0.7.4" 2838 + version = "0.8.0" 2562 2839 source = "registry+https://github.com/rust-lang/crates.io-index" 2563 - checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 2840 + checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 2564 2841 2565 2842 [[package]] 2566 2843 name = "local-channel" ··· 2591 2868 2592 2869 [[package]] 2593 2870 name = "lofty" 2594 - version = "0.22.2" 2871 + version = "0.22.4" 2595 2872 source = "registry+https://github.com/rust-lang/crates.io-index" 2596 - checksum = "781de624f162b1a8cbfbd577103ee9b8e5f62854b053ff48f4e31e68a0a7df6f" 2873 + checksum = "ca260c51a9c71f823fbfd2e6fbc8eb2ee09834b98c00763d877ca8bfa85cde3e" 2597 2874 dependencies = [ 2598 2875 "byteorder", 2599 2876 "data-encoding", ··· 2612 2889 dependencies = [ 2613 2890 "proc-macro2", 2614 2891 "quote", 2615 - "syn 2.0.98", 2892 + "syn 2.0.101", 2616 2893 ] 2617 2894 2618 2895 [[package]] 2619 2896 name = "log" 2620 - version = "0.4.25" 2897 + version = "0.4.27" 2621 2898 source = "registry+https://github.com/rust-lang/crates.io-index" 2622 - checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 2899 + checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 2900 + 2901 + [[package]] 2902 + name = "lru-slab" 2903 + version = "0.1.2" 2904 + source = "registry+https://github.com/rust-lang/crates.io-index" 2905 + checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 2623 2906 2624 2907 [[package]] 2625 2908 name = "lz4" ··· 2648 2931 dependencies = [ 2649 2932 "proc-macro2", 2650 2933 "quote", 2651 - "syn 2.0.98", 2934 + "syn 2.0.101", 2652 2935 ] 2653 2936 2654 2937 [[package]] ··· 2700 2983 2701 2984 [[package]] 2702 2985 name = "miniz_oxide" 2703 - version = "0.8.4" 2986 + version = "0.8.8" 2704 2987 source = "registry+https://github.com/rust-lang/crates.io-index" 2705 - checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" 2988 + checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 2706 2989 dependencies = [ 2707 2990 "adler2", 2708 2991 ] 2709 2992 2710 2993 [[package]] 2711 2994 name = "mio" 2712 - version = "1.0.3" 2995 + version = "1.0.4" 2713 2996 source = "registry+https://github.com/rust-lang/crates.io-index" 2714 - checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 2997 + checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 2715 2998 dependencies = [ 2716 2999 "libc", 2717 3000 "log", 2718 3001 "wasi 0.11.0+wasi-snapshot-preview1", 2719 - "windows-sys 0.52.0", 3002 + "windows-sys 0.59.0", 2720 3003 ] 2721 3004 2722 3005 [[package]] ··· 2728 3011 "data-encoding", 2729 3012 "ed25519", 2730 3013 "ed25519-dalek", 2731 - "getrandom 0.2.15", 3014 + "getrandom 0.2.16", 2732 3015 "log", 2733 3016 "rand 0.8.5", 2734 3017 "signatory", ··· 2878 3161 2879 3162 [[package]] 2880 3163 name = "once_cell" 2881 - version = "1.20.3" 3164 + version = "1.21.3" 2882 3165 source = "registry+https://github.com/rust-lang/crates.io-index" 2883 - checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 3166 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 3167 + 3168 + [[package]] 3169 + name = "once_cell_polyfill" 3170 + version = "1.70.1" 3171 + source = "registry+https://github.com/rust-lang/crates.io-index" 3172 + checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 2884 3173 2885 3174 [[package]] 2886 3175 name = "opaque-debug" ··· 2895 3184 checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 2896 3185 2897 3186 [[package]] 3187 + name = "option-ext" 3188 + version = "0.2.0" 3189 + source = "registry+https://github.com/rust-lang/crates.io-index" 3190 + checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 3191 + 3192 + [[package]] 2898 3193 name = "ordered-multimap" 2899 3194 version = "0.7.3" 2900 3195 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2906 3201 2907 3202 [[package]] 2908 3203 name = "owo-colors" 2909 - version = "4.1.0" 3204 + version = "4.2.1" 2910 3205 source = "registry+https://github.com/rust-lang/crates.io-index" 2911 - checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" 3206 + checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" 2912 3207 2913 3208 [[package]] 2914 3209 name = "parking" ··· 2956 3251 2957 3252 [[package]] 2958 3253 name = "pem" 2959 - version = "3.0.4" 3254 + version = "3.0.5" 2960 3255 source = "registry+https://github.com/rust-lang/crates.io-index" 2961 - checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" 3256 + checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" 2962 3257 dependencies = [ 2963 3258 "base64 0.22.1", 2964 3259 "serde", ··· 3019 3314 3020 3315 [[package]] 3021 3316 name = "pin-project" 3022 - version = "1.1.9" 3317 + version = "1.1.10" 3023 3318 source = "registry+https://github.com/rust-lang/crates.io-index" 3024 - checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" 3319 + checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 3025 3320 dependencies = [ 3026 3321 "pin-project-internal", 3027 3322 ] 3028 3323 3029 3324 [[package]] 3030 3325 name = "pin-project-internal" 3031 - version = "1.1.9" 3326 + version = "1.1.10" 3032 3327 source = "registry+https://github.com/rust-lang/crates.io-index" 3033 - checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" 3328 + checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 3034 3329 dependencies = [ 3035 3330 "proc-macro2", 3036 3331 "quote", 3037 - "syn 2.0.98", 3332 + "syn 2.0.101", 3038 3333 ] 3039 3334 3040 3335 [[package]] ··· 3072 3367 3073 3368 [[package]] 3074 3369 name = "pkg-config" 3075 - version = "0.3.31" 3370 + version = "0.3.32" 3076 3371 source = "registry+https://github.com/rust-lang/crates.io-index" 3077 - checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 3372 + checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 3078 3373 3079 3374 [[package]] 3080 3375 name = "planus" ··· 3116 3411 source = "registry+https://github.com/rust-lang/crates.io-index" 3117 3412 checksum = "72571dde488ecccbe799798bf99ab7308ebdb7cf5d95bcc498dbd5a132f0da4d" 3118 3413 dependencies = [ 3119 - "getrandom 0.2.15", 3414 + "getrandom 0.2.16", 3120 3415 "polars-arrow", 3121 3416 "polars-core", 3122 3417 "polars-error", ··· 3136 3431 source = "registry+https://github.com/rust-lang/crates.io-index" 3137 3432 checksum = "6611c758d52e799761cc25900666b71552e6c929d88052811bc9daad4b3321a8" 3138 3433 dependencies = [ 3139 - "ahash 0.8.11", 3434 + "ahash 0.8.12", 3140 3435 "atoi_simd", 3141 3436 "bytemuck", 3142 3437 "chrono", ··· 3144 3439 "dyn-clone", 3145 3440 "either", 3146 3441 "ethnum", 3147 - "getrandom 0.2.15", 3148 - "hashbrown 0.15.2", 3442 + "getrandom 0.2.16", 3443 + "hashbrown 0.15.3", 3149 3444 "itoa", 3150 3445 "lz4", 3151 3446 "num-traits", ··· 3199 3494 source = "registry+https://github.com/rust-lang/crates.io-index" 3200 3495 checksum = "796d06eae7e6e74ed28ea54a8fccc584ebac84e6cf0e1e9ba41ffc807b169a01" 3201 3496 dependencies = [ 3202 - "ahash 0.8.11", 3203 - "bitflags 2.8.0", 3497 + "ahash 0.8.12", 3498 + "bitflags 2.9.1", 3204 3499 "bytemuck", 3205 3500 "chrono", 3206 3501 "chrono-tz", 3207 3502 "comfy-table", 3208 3503 "either", 3209 3504 "hashbrown 0.14.5", 3210 - "hashbrown 0.15.2", 3505 + "hashbrown 0.15.3", 3211 3506 "indexmap", 3212 3507 "itoa", 3213 3508 "num-traits", ··· 3223 3518 "rayon", 3224 3519 "regex", 3225 3520 "strum_macros 0.26.4", 3226 - "thiserror 2.0.11", 3521 + "thiserror 2.0.12", 3227 3522 "version_check", 3228 3523 "xxhash-rust", 3229 3524 ] ··· 3237 3532 "polars-arrow-format", 3238 3533 "regex", 3239 3534 "simdutf8", 3240 - "thiserror 2.0.11", 3535 + "thiserror 2.0.12", 3241 3536 ] 3242 3537 3243 3538 [[package]] ··· 3246 3541 source = "registry+https://github.com/rust-lang/crates.io-index" 3247 3542 checksum = "c8e639991a8ad4fb12880ab44bcc3cf44a5703df003142334d9caf86d77d77e7" 3248 3543 dependencies = [ 3249 - "ahash 0.8.11", 3250 - "bitflags 2.8.0", 3251 - "hashbrown 0.15.2", 3544 + "ahash 0.8.12", 3545 + "bitflags 2.9.1", 3546 + "hashbrown 0.15.3", 3252 3547 "num-traits", 3253 3548 "once_cell", 3254 3549 "polars-arrow", ··· 3270 3565 source = "registry+https://github.com/rust-lang/crates.io-index" 3271 3566 checksum = "719a77e94480f6be090512da196e378cbcbeb3584c6fe1134c600aee906e38ab" 3272 3567 dependencies = [ 3273 - "ahash 0.8.11", 3568 + "ahash 0.8.12", 3274 3569 "async-trait", 3275 3570 "atoi_simd", 3276 3571 "bytes", ··· 3278 3573 "fast-float2", 3279 3574 "futures", 3280 3575 "glob", 3281 - "hashbrown 0.15.2", 3576 + "hashbrown 0.15.3", 3282 3577 "home", 3283 3578 "itoa", 3284 3579 "memchr", ··· 3307 3602 source = "registry+https://github.com/rust-lang/crates.io-index" 3308 3603 checksum = "a0a731a672dfc8ac38c1f73c9a4b2ae38d2fc8ac363bfb64c5f3a3e072ffc5ad" 3309 3604 dependencies = [ 3310 - "ahash 0.8.11", 3311 - "bitflags 2.8.0", 3605 + "ahash 0.8.12", 3606 + "bitflags 2.9.1", 3312 3607 "chrono", 3313 3608 "memchr", 3314 3609 "once_cell", ··· 3352 3647 source = "registry+https://github.com/rust-lang/crates.io-index" 3353 3648 checksum = "cbb83218b0c216104f0076cd1a005128be078f958125f3d59b094ee73d78c18e" 3354 3649 dependencies = [ 3355 - "ahash 0.8.11", 3650 + "ahash 0.8.12", 3356 3651 "argminmax", 3357 3652 "base64 0.22.1", 3358 3653 "bytemuck", 3359 3654 "chrono", 3360 3655 "chrono-tz", 3361 3656 "either", 3362 - "hashbrown 0.15.2", 3657 + "hashbrown 0.15.3", 3363 3658 "hex", 3364 3659 "indexmap", 3365 3660 "memchr", ··· 3386 3681 source = "registry+https://github.com/rust-lang/crates.io-index" 3387 3682 checksum = "5c60ee85535590a38db6c703a21be4cb25342e40f573f070d1e16f9d84a53ac7" 3388 3683 dependencies = [ 3389 - "ahash 0.8.11", 3684 + "ahash 0.8.12", 3390 3685 "async-stream", 3391 3686 "base64 0.22.1", 3392 3687 "bytemuck", 3393 3688 "ethnum", 3394 3689 "futures", 3395 - "hashbrown 0.15.2", 3690 + "hashbrown 0.15.3", 3396 3691 "num-traits", 3397 3692 "polars-arrow", 3398 3693 "polars-compute", ··· 3422 3717 "crossbeam-channel", 3423 3718 "crossbeam-queue", 3424 3719 "enum_dispatch", 3425 - "hashbrown 0.15.2", 3720 + "hashbrown 0.15.3", 3426 3721 "num-traits", 3427 3722 "once_cell", 3428 3723 "polars-arrow", ··· 3445 3740 source = "registry+https://github.com/rust-lang/crates.io-index" 3446 3741 checksum = "4f03533a93aa66127fcb909a87153a3c7cfee6f0ae59f497e73d7736208da54c" 3447 3742 dependencies = [ 3448 - "ahash 0.8.11", 3449 - "bitflags 2.8.0", 3743 + "ahash 0.8.12", 3744 + "bitflags 2.9.1", 3450 3745 "bytemuck", 3451 3746 "bytes", 3452 3747 "chrono", 3453 3748 "chrono-tz", 3454 3749 "either", 3455 - "hashbrown 0.15.2", 3750 + "hashbrown 0.15.3", 3456 3751 "memmap2", 3457 3752 "num-traits", 3458 3753 "once_cell", ··· 3477 3772 source = "registry+https://github.com/rust-lang/crates.io-index" 3478 3773 checksum = "6bf47f7409f8e75328d7d034be390842924eb276716d0458607be0bddb8cc839" 3479 3774 dependencies = [ 3480 - "bitflags 2.8.0", 3775 + "bitflags 2.9.1", 3481 3776 "bytemuck", 3482 3777 "polars-arrow", 3483 3778 "polars-compute", ··· 3577 3872 source = "registry+https://github.com/rust-lang/crates.io-index" 3578 3873 checksum = "a8f6c8166a4a7fbc15b87c81645ed9e1f0651ff2e8c96cafc40ac5bf43441a10" 3579 3874 dependencies = [ 3580 - "ahash 0.8.11", 3875 + "ahash 0.8.12", 3581 3876 "bytemuck", 3582 3877 "bytes", 3583 3878 "compact_str", 3584 - "hashbrown 0.15.2", 3879 + "hashbrown 0.15.3", 3585 3880 "indexmap", 3586 3881 "libc", 3587 3882 "memmap2", ··· 3615 3910 checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 3616 3911 3617 3912 [[package]] 3913 + name = "potential_utf" 3914 + version = "0.1.2" 3915 + source = "registry+https://github.com/rust-lang/crates.io-index" 3916 + checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 3917 + dependencies = [ 3918 + "zerovec", 3919 + ] 3920 + 3921 + [[package]] 3618 3922 name = "powerfmt" 3619 3923 version = "0.2.0" 3620 3924 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3622 3926 3623 3927 [[package]] 3624 3928 name = "ppv-lite86" 3625 - version = "0.2.20" 3929 + version = "0.2.21" 3626 3930 source = "registry+https://github.com/rust-lang/crates.io-index" 3627 - checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 3931 + checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 3628 3932 dependencies = [ 3629 - "zerocopy 0.7.35", 3933 + "zerocopy", 3630 3934 ] 3631 3935 3632 3936 [[package]] 3633 3937 name = "proc-macro-crate" 3634 - version = "3.2.0" 3938 + version = "3.3.0" 3635 3939 source = "registry+https://github.com/rust-lang/crates.io-index" 3636 - checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" 3940 + checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" 3637 3941 dependencies = [ 3638 3942 "toml_edit", 3639 3943 ] 3640 3944 3641 3945 [[package]] 3642 3946 name = "proc-macro2" 3643 - version = "1.0.93" 3947 + version = "1.0.95" 3644 3948 source = "registry+https://github.com/rust-lang/crates.io-index" 3645 - checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 3949 + checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 3646 3950 dependencies = [ 3647 3951 "unicode-ident", 3648 3952 ] 3649 3953 3650 3954 [[package]] 3651 3955 name = "psm" 3652 - version = "0.1.25" 3956 + version = "0.1.26" 3653 3957 source = "registry+https://github.com/rust-lang/crates.io-index" 3654 - checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" 3958 + checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" 3655 3959 dependencies = [ 3656 3960 "cc", 3657 3961 ] ··· 3688 3992 3689 3993 [[package]] 3690 3994 name = "quick-xml" 3691 - version = "0.37.4" 3995 + version = "0.37.5" 3692 3996 source = "registry+https://github.com/rust-lang/crates.io-index" 3693 - checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" 3997 + checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" 3694 3998 dependencies = [ 3695 3999 "memchr", 3696 4000 "serde", ··· 3698 4002 3699 4003 [[package]] 3700 4004 name = "quinn" 3701 - version = "0.11.6" 4005 + version = "0.11.8" 3702 4006 source = "registry+https://github.com/rust-lang/crates.io-index" 3703 - checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" 4007 + checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" 3704 4008 dependencies = [ 3705 4009 "bytes", 4010 + "cfg_aliases", 3706 4011 "pin-project-lite", 3707 4012 "quinn-proto", 3708 4013 "quinn-udp", 3709 4014 "rustc-hash", 3710 - "rustls 0.23.23", 4015 + "rustls 0.23.27", 3711 4016 "socket2", 3712 - "thiserror 2.0.11", 4017 + "thiserror 2.0.12", 3713 4018 "tokio", 3714 4019 "tracing", 4020 + "web-time", 3715 4021 ] 3716 4022 3717 4023 [[package]] 3718 4024 name = "quinn-proto" 3719 - version = "0.11.9" 4025 + version = "0.11.12" 3720 4026 source = "registry+https://github.com/rust-lang/crates.io-index" 3721 - checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" 4027 + checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" 3722 4028 dependencies = [ 3723 4029 "bytes", 3724 - "getrandom 0.2.15", 3725 - "rand 0.8.5", 4030 + "getrandom 0.3.3", 4031 + "lru-slab", 4032 + "rand 0.9.1", 3726 4033 "ring", 3727 4034 "rustc-hash", 3728 - "rustls 0.23.23", 4035 + "rustls 0.23.27", 3729 4036 "rustls-pki-types", 3730 4037 "slab", 3731 - "thiserror 2.0.11", 4038 + "thiserror 2.0.12", 3732 4039 "tinyvec", 3733 4040 "tracing", 3734 4041 "web-time", ··· 3736 4043 3737 4044 [[package]] 3738 4045 name = "quinn-udp" 3739 - version = "0.5.9" 4046 + version = "0.5.12" 3740 4047 source = "registry+https://github.com/rust-lang/crates.io-index" 3741 - checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" 4048 + checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" 3742 4049 dependencies = [ 3743 4050 "cfg_aliases", 3744 4051 "libc", ··· 3750 4057 3751 4058 [[package]] 3752 4059 name = "quote" 3753 - version = "1.0.38" 4060 + version = "1.0.40" 3754 4061 source = "registry+https://github.com/rust-lang/crates.io-index" 3755 - checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 4062 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 3756 4063 dependencies = [ 3757 4064 "proc-macro2", 3758 4065 ] 3759 4066 3760 4067 [[package]] 4068 + name = "r-efi" 4069 + version = "5.2.0" 4070 + source = "registry+https://github.com/rust-lang/crates.io-index" 4071 + checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 4072 + 4073 + [[package]] 3761 4074 name = "radium" 3762 4075 version = "0.7.0" 3763 4076 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3776 4089 3777 4090 [[package]] 3778 4091 name = "rand" 3779 - version = "0.9.0" 4092 + version = "0.9.1" 3780 4093 source = "registry+https://github.com/rust-lang/crates.io-index" 3781 - checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 4094 + checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 3782 4095 dependencies = [ 3783 4096 "rand_chacha 0.9.0", 3784 4097 "rand_core 0.9.3", 3785 - "zerocopy 0.8.24", 3786 4098 ] 3787 4099 3788 4100 [[package]] ··· 3811 4123 source = "registry+https://github.com/rust-lang/crates.io-index" 3812 4124 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 3813 4125 dependencies = [ 3814 - "getrandom 0.2.15", 4126 + "getrandom 0.2.16", 3815 4127 ] 3816 4128 3817 4129 [[package]] ··· 3820 4132 source = "registry+https://github.com/rust-lang/crates.io-index" 3821 4133 checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 3822 4134 dependencies = [ 3823 - "getrandom 0.3.1", 4135 + "getrandom 0.3.3", 3824 4136 ] 3825 4137 3826 4138 [[package]] ··· 3835 4147 3836 4148 [[package]] 3837 4149 name = "raw-cpuid" 3838 - version = "11.4.0" 4150 + version = "11.5.0" 3839 4151 source = "registry+https://github.com/rust-lang/crates.io-index" 3840 - checksum = "529468c1335c1c03919960dfefdb1b3648858c20d7ec2d0663e728e4a717efbc" 4152 + checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" 3841 4153 dependencies = [ 3842 - "bitflags 2.8.0", 4154 + "bitflags 2.9.1", 3843 4155 ] 3844 4156 3845 4157 [[package]] ··· 3879 4191 checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" 3880 4192 dependencies = [ 3881 4193 "quote", 3882 - "syn 2.0.98", 4194 + "syn 2.0.101", 3883 4195 ] 3884 4196 3885 4197 [[package]] ··· 3903 4215 3904 4216 [[package]] 3905 4217 name = "redis" 3906 - version = "0.29.0" 4218 + version = "0.29.5" 3907 4219 source = "registry+https://github.com/rust-lang/crates.io-index" 3908 - checksum = "9568894e8bdefd16512bca9e286a9d2abc27773609aa4eb7f428497d64df4373" 4220 + checksum = "1bc42f3a12fd4408ce64d8efef67048a924e543bd35c6591c0447fda9054695f" 3909 4221 dependencies = [ 3910 4222 "arc-swap", 3911 4223 "combine", ··· 3920 4232 3921 4233 [[package]] 3922 4234 name = "redox_syscall" 3923 - version = "0.5.8" 4235 + version = "0.5.12" 4236 + source = "registry+https://github.com/rust-lang/crates.io-index" 4237 + checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 4238 + dependencies = [ 4239 + "bitflags 2.9.1", 4240 + ] 4241 + 4242 + [[package]] 4243 + name = "redox_users" 4244 + version = "0.5.0" 3924 4245 source = "registry+https://github.com/rust-lang/crates.io-index" 3925 - checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 4246 + checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 3926 4247 dependencies = [ 3927 - "bitflags 2.8.0", 4248 + "getrandom 0.2.16", 4249 + "libredox", 4250 + "thiserror 2.0.12", 3928 4251 ] 3929 4252 3930 4253 [[package]] ··· 3973 4296 3974 4297 [[package]] 3975 4298 name = "reqwest" 3976 - version = "0.12.12" 4299 + version = "0.12.15" 3977 4300 source = "registry+https://github.com/rust-lang/crates.io-index" 3978 - checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" 4301 + checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 3979 4302 dependencies = [ 3980 4303 "base64 0.22.1", 3981 4304 "bytes", 3982 4305 "futures-core", 3983 4306 "futures-util", 3984 - "http 1.2.0", 4307 + "http 1.3.1", 3985 4308 "http-body 1.0.1", 3986 4309 "http-body-util", 3987 4310 "hyper 1.6.0", 3988 - "hyper-rustls 0.27.5", 4311 + "hyper-rustls 0.27.6", 3989 4312 "hyper-util", 3990 4313 "ipnet", 3991 4314 "js-sys", ··· 3996 4319 "percent-encoding", 3997 4320 "pin-project-lite", 3998 4321 "quinn", 3999 - "rustls 0.23.23", 4322 + "rustls 0.23.27", 4000 4323 "rustls-pemfile 2.2.0", 4001 4324 "rustls-pki-types", 4002 4325 "serde", ··· 4004 4327 "serde_urlencoded", 4005 4328 "sync_wrapper", 4006 4329 "tokio", 4007 - "tokio-rustls 0.26.1", 4330 + "tokio-rustls 0.26.2", 4008 4331 "tokio-util", 4009 4332 "tower", 4010 4333 "tower-service", ··· 4013 4336 "wasm-bindgen-futures", 4014 4337 "wasm-streams", 4015 4338 "web-sys", 4016 - "webpki-roots", 4339 + "webpki-roots 0.26.11", 4017 4340 "windows-registry", 4018 4341 ] 4019 4342 4020 4343 [[package]] 4021 4344 name = "ring" 4022 - version = "0.17.9" 4345 + version = "0.17.14" 4023 4346 source = "registry+https://github.com/rust-lang/crates.io-index" 4024 - checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" 4347 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 4025 4348 dependencies = [ 4026 4349 "cc", 4027 4350 "cfg-if", 4028 - "getrandom 0.2.15", 4351 + "getrandom 0.2.16", 4029 4352 "libc", 4030 4353 "untrusted", 4031 4354 "windows-sys 0.52.0", ··· 4062 4385 4063 4386 [[package]] 4064 4387 name = "rsa" 4065 - version = "0.9.7" 4388 + version = "0.9.8" 4066 4389 source = "registry+https://github.com/rust-lang/crates.io-index" 4067 - checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" 4390 + checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 4068 4391 dependencies = [ 4069 4392 "const-oid", 4070 4393 "digest", ··· 4130 4453 4131 4454 [[package]] 4132 4455 name = "rust_decimal" 4133 - version = "1.36.0" 4456 + version = "1.37.1" 4134 4457 source = "registry+https://github.com/rust-lang/crates.io-index" 4135 - checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" 4458 + checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" 4136 4459 dependencies = [ 4137 4460 "arrayvec", 4138 4461 "borsh", ··· 4171 4494 source = "registry+https://github.com/rust-lang/crates.io-index" 4172 4495 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 4173 4496 dependencies = [ 4174 - "bitflags 2.8.0", 4497 + "bitflags 2.9.1", 4175 4498 "errno", 4176 4499 "libc", 4177 4500 "linux-raw-sys 0.4.15", ··· 4180 4503 4181 4504 [[package]] 4182 4505 name = "rustix" 4183 - version = "1.0.3" 4506 + version = "1.0.7" 4184 4507 source = "registry+https://github.com/rust-lang/crates.io-index" 4185 - checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" 4508 + checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 4186 4509 dependencies = [ 4187 - "bitflags 2.8.0", 4510 + "bitflags 2.9.1", 4188 4511 "errno", 4189 4512 "libc", 4190 - "linux-raw-sys 0.9.3", 4513 + "linux-raw-sys 0.9.4", 4191 4514 "windows-sys 0.59.0", 4192 4515 ] 4193 4516 ··· 4205 4528 4206 4529 [[package]] 4207 4530 name = "rustls" 4208 - version = "0.23.23" 4531 + version = "0.23.27" 4209 4532 source = "registry+https://github.com/rust-lang/crates.io-index" 4210 - checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" 4533 + checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" 4211 4534 dependencies = [ 4535 + "log", 4212 4536 "once_cell", 4213 4537 "ring", 4214 4538 "rustls-pki-types", 4215 - "rustls-webpki 0.102.8", 4539 + "rustls-webpki 0.103.3", 4216 4540 "subtle", 4217 4541 "zeroize", 4218 4542 ] ··· 4226 4550 "openssl-probe", 4227 4551 "rustls-pemfile 1.0.4", 4228 4552 "schannel", 4229 - "security-framework", 4553 + "security-framework 2.11.1", 4230 4554 ] 4231 4555 4232 4556 [[package]] ··· 4239 4563 "rustls-pemfile 2.2.0", 4240 4564 "rustls-pki-types", 4241 4565 "schannel", 4242 - "security-framework", 4566 + "security-framework 2.11.1", 4567 + ] 4568 + 4569 + [[package]] 4570 + name = "rustls-native-certs" 4571 + version = "0.8.1" 4572 + source = "registry+https://github.com/rust-lang/crates.io-index" 4573 + checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" 4574 + dependencies = [ 4575 + "openssl-probe", 4576 + "rustls-pki-types", 4577 + "schannel", 4578 + "security-framework 3.2.0", 4243 4579 ] 4244 4580 4245 4581 [[package]] ··· 4262 4598 4263 4599 [[package]] 4264 4600 name = "rustls-pki-types" 4265 - version = "1.11.0" 4601 + version = "1.12.0" 4266 4602 source = "registry+https://github.com/rust-lang/crates.io-index" 4267 - checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 4603 + checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 4268 4604 dependencies = [ 4269 4605 "web-time", 4606 + "zeroize", 4270 4607 ] 4271 4608 4272 4609 [[package]] 4610 + name = "rustls-platform-verifier" 4611 + version = "0.5.3" 4612 + source = "registry+https://github.com/rust-lang/crates.io-index" 4613 + checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" 4614 + dependencies = [ 4615 + "core-foundation 0.10.0", 4616 + "core-foundation-sys", 4617 + "jni", 4618 + "log", 4619 + "once_cell", 4620 + "rustls 0.23.27", 4621 + "rustls-native-certs 0.8.1", 4622 + "rustls-platform-verifier-android", 4623 + "rustls-webpki 0.103.3", 4624 + "security-framework 3.2.0", 4625 + "security-framework-sys", 4626 + "webpki-root-certs", 4627 + "windows-sys 0.52.0", 4628 + ] 4629 + 4630 + [[package]] 4631 + name = "rustls-platform-verifier-android" 4632 + version = "0.1.1" 4633 + source = "registry+https://github.com/rust-lang/crates.io-index" 4634 + checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" 4635 + 4636 + [[package]] 4273 4637 name = "rustls-webpki" 4274 4638 version = "0.101.7" 4275 4639 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4285 4649 source = "registry+https://github.com/rust-lang/crates.io-index" 4286 4650 checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 4287 4651 dependencies = [ 4652 + "rustls-pki-types", 4653 + "untrusted", 4654 + ] 4655 + 4656 + [[package]] 4657 + name = "rustls-webpki" 4658 + version = "0.103.3" 4659 + source = "registry+https://github.com/rust-lang/crates.io-index" 4660 + checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 4661 + dependencies = [ 4288 4662 "ring", 4289 4663 "rustls-pki-types", 4290 4664 "untrusted", ··· 4292 4666 4293 4667 [[package]] 4294 4668 name = "rustversion" 4295 - version = "1.0.19" 4669 + version = "1.0.21" 4296 4670 source = "registry+https://github.com/rust-lang/crates.io-index" 4297 - checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 4671 + checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 4298 4672 4299 4673 [[package]] 4300 4674 name = "ryu" 4301 - version = "1.0.19" 4675 + version = "1.0.20" 4302 4676 source = "registry+https://github.com/rust-lang/crates.io-index" 4303 - checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 4677 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 4678 + 4679 + [[package]] 4680 + name = "same-file" 4681 + version = "1.0.6" 4682 + source = "registry+https://github.com/rust-lang/crates.io-index" 4683 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 4684 + dependencies = [ 4685 + "winapi-util", 4686 + ] 4304 4687 4305 4688 [[package]] 4306 4689 name = "schannel" ··· 4333 4716 "jsonwebtoken", 4334 4717 "md5", 4335 4718 "owo-colors", 4336 - "quick-xml 0.37.4", 4337 - "rand 0.9.0", 4338 - "redis 0.29.0", 4719 + "quick-xml 0.37.5", 4720 + "rand 0.9.1", 4721 + "redis 0.29.5", 4339 4722 "reqwest", 4340 4723 "serde", 4341 4724 "serde_json", ··· 4367 4750 source = "registry+https://github.com/rust-lang/crates.io-index" 4368 4751 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 4369 4752 dependencies = [ 4370 - "bitflags 2.8.0", 4371 - "core-foundation", 4753 + "bitflags 2.9.1", 4754 + "core-foundation 0.9.4", 4755 + "core-foundation-sys", 4756 + "libc", 4757 + "security-framework-sys", 4758 + ] 4759 + 4760 + [[package]] 4761 + name = "security-framework" 4762 + version = "3.2.0" 4763 + source = "registry+https://github.com/rust-lang/crates.io-index" 4764 + checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" 4765 + dependencies = [ 4766 + "bitflags 2.9.1", 4767 + "core-foundation 0.10.0", 4372 4768 "core-foundation-sys", 4373 4769 "libc", 4374 4770 "security-framework-sys", ··· 4386 4782 4387 4783 [[package]] 4388 4784 name = "semver" 4389 - version = "1.0.25" 4785 + version = "1.0.26" 4786 + source = "registry+https://github.com/rust-lang/crates.io-index" 4787 + checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 4788 + 4789 + [[package]] 4790 + name = "send_wrapper" 4791 + version = "0.4.0" 4390 4792 source = "registry+https://github.com/rust-lang/crates.io-index" 4391 - checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" 4793 + checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" 4392 4794 4393 4795 [[package]] 4394 4796 name = "serde" ··· 4407 4809 dependencies = [ 4408 4810 "proc-macro2", 4409 4811 "quote", 4410 - "syn 2.0.98", 4812 + "syn 2.0.101", 4411 4813 ] 4412 4814 4413 4815 [[package]] ··· 4433 4835 4434 4836 [[package]] 4435 4837 name = "serde_repr" 4436 - version = "0.1.19" 4838 + version = "0.1.20" 4437 4839 source = "registry+https://github.com/rust-lang/crates.io-index" 4438 - checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" 4840 + checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 4439 4841 dependencies = [ 4440 4842 "proc-macro2", 4441 4843 "quote", 4442 - "syn 2.0.98", 4844 + "syn 2.0.101", 4443 4845 ] 4444 4846 4445 4847 [[package]] ··· 4473 4875 4474 4876 [[package]] 4475 4877 name = "sha2" 4476 - version = "0.10.8" 4878 + version = "0.10.9" 4477 4879 source = "registry+https://github.com/rust-lang/crates.io-index" 4478 - checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 4880 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 4479 4881 dependencies = [ 4480 4882 "cfg-if", 4481 4883 "cpufeatures", ··· 4503 4905 4504 4906 [[package]] 4505 4907 name = "signal-hook-registry" 4506 - version = "1.4.2" 4908 + version = "1.4.5" 4507 4909 source = "registry+https://github.com/rust-lang/crates.io-index" 4508 - checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 4910 + checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 4509 4911 dependencies = [ 4510 4912 "libc", 4511 4913 ] ··· 4546 4948 dependencies = [ 4547 4949 "num-bigint", 4548 4950 "num-traits", 4549 - "thiserror 2.0.11", 4951 + "thiserror 2.0.12", 4550 4952 "time", 4551 4953 ] 4552 4954 ··· 4576 4978 4577 4979 [[package]] 4578 4980 name = "smallvec" 4579 - version = "1.14.0" 4981 + version = "1.15.0" 4580 4982 source = "registry+https://github.com/rust-lang/crates.io-index" 4581 - checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 4983 + checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 4582 4984 dependencies = [ 4583 4985 "serde", 4584 4986 ] 4585 4987 4586 4988 [[package]] 4587 4989 name = "socket2" 4588 - version = "0.5.8" 4990 + version = "0.5.9" 4589 4991 source = "registry+https://github.com/rust-lang/crates.io-index" 4590 - checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 4992 + checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 4591 4993 dependencies = [ 4592 4994 "libc", 4593 4995 "windows-sys 0.52.0", 4594 4996 ] 4595 4997 4596 4998 [[package]] 4999 + name = "soketto" 5000 + version = "0.8.1" 5001 + source = "registry+https://github.com/rust-lang/crates.io-index" 5002 + checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" 5003 + dependencies = [ 5004 + "base64 0.22.1", 5005 + "bytes", 5006 + "futures", 5007 + "httparse", 5008 + "log", 5009 + "rand 0.8.5", 5010 + "sha1", 5011 + ] 5012 + 5013 + [[package]] 4597 5014 name = "spin" 4598 5015 version = "0.9.8" 4599 5016 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4625 5042 "hex", 4626 5043 "jsonwebtoken", 4627 5044 "owo-colors", 4628 - "redis 0.29.0", 5045 + "redis 0.29.5", 4629 5046 "reqwest", 4630 5047 "serde", 4631 5048 "serde_json", ··· 4645 5062 4646 5063 [[package]] 4647 5064 name = "sqlx" 4648 - version = "0.8.3" 5065 + version = "0.8.6" 4649 5066 source = "registry+https://github.com/rust-lang/crates.io-index" 4650 - checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" 5067 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 4651 5068 dependencies = [ 4652 5069 "sqlx-core", 4653 5070 "sqlx-macros", ··· 4658 5075 4659 5076 [[package]] 4660 5077 name = "sqlx-core" 4661 - version = "0.8.3" 5078 + version = "0.8.6" 4662 5079 source = "registry+https://github.com/rust-lang/crates.io-index" 4663 - checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" 5080 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 4664 5081 dependencies = [ 5082 + "base64 0.22.1", 4665 5083 "bytes", 4666 5084 "chrono", 4667 5085 "crc", ··· 4672 5090 "futures-intrusive", 4673 5091 "futures-io", 4674 5092 "futures-util", 4675 - "hashbrown 0.15.2", 5093 + "hashbrown 0.15.3", 4676 5094 "hashlink 0.10.0", 4677 5095 "indexmap", 4678 5096 "log", 4679 5097 "memchr", 4680 5098 "once_cell", 4681 5099 "percent-encoding", 4682 - "rustls 0.23.23", 4683 - "rustls-pemfile 2.2.0", 5100 + "rustls 0.23.27", 4684 5101 "serde", 4685 5102 "serde_json", 4686 5103 "sha2", 4687 5104 "smallvec", 4688 - "thiserror 2.0.11", 5105 + "thiserror 2.0.12", 4689 5106 "tokio", 4690 5107 "tokio-stream", 4691 5108 "tracing", 4692 5109 "url", 4693 - "webpki-roots", 5110 + "webpki-roots 0.26.11", 4694 5111 ] 4695 5112 4696 5113 [[package]] 4697 5114 name = "sqlx-macros" 4698 - version = "0.8.3" 5115 + version = "0.8.6" 4699 5116 source = "registry+https://github.com/rust-lang/crates.io-index" 4700 - checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" 5117 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 4701 5118 dependencies = [ 4702 5119 "proc-macro2", 4703 5120 "quote", 4704 5121 "sqlx-core", 4705 5122 "sqlx-macros-core", 4706 - "syn 2.0.98", 5123 + "syn 2.0.101", 4707 5124 ] 4708 5125 4709 5126 [[package]] 4710 5127 name = "sqlx-macros-core" 4711 - version = "0.8.3" 5128 + version = "0.8.6" 4712 5129 source = "registry+https://github.com/rust-lang/crates.io-index" 4713 - checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" 5130 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 4714 5131 dependencies = [ 4715 5132 "dotenvy", 4716 5133 "either", ··· 4726 5143 "sqlx-mysql", 4727 5144 "sqlx-postgres", 4728 5145 "sqlx-sqlite", 4729 - "syn 2.0.98", 4730 - "tempfile", 5146 + "syn 2.0.101", 4731 5147 "tokio", 4732 5148 "url", 4733 5149 ] 4734 5150 4735 5151 [[package]] 4736 5152 name = "sqlx-mysql" 4737 - version = "0.8.3" 5153 + version = "0.8.6" 4738 5154 source = "registry+https://github.com/rust-lang/crates.io-index" 4739 - checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" 5155 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 4740 5156 dependencies = [ 4741 5157 "atoi", 4742 5158 "base64 0.22.1", 4743 - "bitflags 2.8.0", 5159 + "bitflags 2.9.1", 4744 5160 "byteorder", 4745 5161 "bytes", 4746 5162 "chrono", ··· 4770 5186 "smallvec", 4771 5187 "sqlx-core", 4772 5188 "stringprep", 4773 - "thiserror 2.0.11", 5189 + "thiserror 2.0.12", 4774 5190 "tracing", 4775 5191 "whoami", 4776 5192 ] 4777 5193 4778 5194 [[package]] 4779 5195 name = "sqlx-postgres" 4780 - version = "0.8.3" 5196 + version = "0.8.6" 4781 5197 source = "registry+https://github.com/rust-lang/crates.io-index" 4782 - checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" 5198 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 4783 5199 dependencies = [ 4784 5200 "atoi", 4785 5201 "base64 0.22.1", 4786 - "bitflags 2.8.0", 5202 + "bitflags 2.9.1", 4787 5203 "byteorder", 4788 5204 "chrono", 4789 5205 "crc", ··· 4808 5224 "smallvec", 4809 5225 "sqlx-core", 4810 5226 "stringprep", 4811 - "thiserror 2.0.11", 5227 + "thiserror 2.0.12", 4812 5228 "tracing", 4813 5229 "whoami", 4814 5230 ] 4815 5231 4816 5232 [[package]] 4817 5233 name = "sqlx-sqlite" 4818 - version = "0.8.3" 5234 + version = "0.8.6" 4819 5235 source = "registry+https://github.com/rust-lang/crates.io-index" 4820 - checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" 5236 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 4821 5237 dependencies = [ 4822 5238 "atoi", 4823 5239 "chrono", ··· 4833 5249 "serde", 4834 5250 "serde_urlencoded", 4835 5251 "sqlx-core", 5252 + "thiserror 2.0.12", 4836 5253 "tracing", 4837 5254 "url", 4838 5255 ] ··· 4845 5262 4846 5263 [[package]] 4847 5264 name = "stacker" 4848 - version = "0.1.19" 5265 + version = "0.1.21" 4849 5266 source = "registry+https://github.com/rust-lang/crates.io-index" 4850 - checksum = "d9156ebd5870ef293bfb43f91c7a74528d363ec0d424afe24160ed5a4343d08a" 5267 + checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" 4851 5268 dependencies = [ 4852 5269 "cc", 4853 5270 "cfg-if", ··· 4935 5352 "proc-macro2", 4936 5353 "quote", 4937 5354 "rustversion", 4938 - "syn 2.0.98", 5355 + "syn 2.0.101", 4939 5356 ] 4940 5357 4941 5358 [[package]] ··· 4948 5365 "proc-macro2", 4949 5366 "quote", 4950 5367 "rustversion", 4951 - "syn 2.0.98", 5368 + "syn 2.0.101", 4952 5369 ] 4953 5370 4954 5371 [[package]] ··· 5165 5582 5166 5583 [[package]] 5167 5584 name = "syn" 5168 - version = "2.0.98" 5585 + version = "2.0.101" 5169 5586 source = "registry+https://github.com/rust-lang/crates.io-index" 5170 - checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 5587 + checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 5171 5588 dependencies = [ 5172 5589 "proc-macro2", 5173 5590 "quote", ··· 5185 5602 5186 5603 [[package]] 5187 5604 name = "synstructure" 5188 - version = "0.13.1" 5605 + version = "0.13.2" 5189 5606 source = "registry+https://github.com/rust-lang/crates.io-index" 5190 - checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 5607 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 5191 5608 dependencies = [ 5192 5609 "proc-macro2", 5193 5610 "quote", 5194 - "syn 2.0.98", 5611 + "syn 2.0.101", 5195 5612 ] 5196 5613 5197 5614 [[package]] ··· 5226 5643 5227 5644 [[package]] 5228 5645 name = "tempfile" 5229 - version = "3.19.1" 5646 + version = "3.20.0" 5230 5647 source = "registry+https://github.com/rust-lang/crates.io-index" 5231 - checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 5648 + checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 5232 5649 dependencies = [ 5233 5650 "fastrand", 5234 - "getrandom 0.3.1", 5651 + "getrandom 0.3.3", 5235 5652 "once_cell", 5236 - "rustix 1.0.3", 5653 + "rustix 1.0.7", 5237 5654 "windows-sys 0.59.0", 5238 5655 ] 5239 5656 ··· 5248 5665 5249 5666 [[package]] 5250 5667 name = "thiserror" 5251 - version = "2.0.11" 5668 + version = "2.0.12" 5252 5669 source = "registry+https://github.com/rust-lang/crates.io-index" 5253 - checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 5670 + checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 5254 5671 dependencies = [ 5255 - "thiserror-impl 2.0.11", 5672 + "thiserror-impl 2.0.12", 5256 5673 ] 5257 5674 5258 5675 [[package]] ··· 5263 5680 dependencies = [ 5264 5681 "proc-macro2", 5265 5682 "quote", 5266 - "syn 2.0.98", 5683 + "syn 2.0.101", 5267 5684 ] 5268 5685 5269 5686 [[package]] 5270 5687 name = "thiserror-impl" 5271 - version = "2.0.11" 5688 + version = "2.0.12" 5272 5689 source = "registry+https://github.com/rust-lang/crates.io-index" 5273 - checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 5690 + checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 5274 5691 dependencies = [ 5275 5692 "proc-macro2", 5276 5693 "quote", 5277 - "syn 2.0.98", 5694 + "syn 2.0.101", 5278 5695 ] 5279 5696 5280 5697 [[package]] 5281 5698 name = "time" 5282 - version = "0.3.37" 5699 + version = "0.3.41" 5283 5700 source = "registry+https://github.com/rust-lang/crates.io-index" 5284 - checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 5701 + checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 5285 5702 dependencies = [ 5286 5703 "deranged", 5287 5704 "itoa", ··· 5294 5711 5295 5712 [[package]] 5296 5713 name = "time-core" 5297 - version = "0.1.2" 5714 + version = "0.1.4" 5298 5715 source = "registry+https://github.com/rust-lang/crates.io-index" 5299 - checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 5716 + checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 5300 5717 5301 5718 [[package]] 5302 5719 name = "time-macros" 5303 - version = "0.2.19" 5720 + version = "0.2.22" 5304 5721 source = "registry+https://github.com/rust-lang/crates.io-index" 5305 - checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 5722 + checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 5306 5723 dependencies = [ 5307 5724 "num-conv", 5308 5725 "time-core", ··· 5319 5736 5320 5737 [[package]] 5321 5738 name = "tinystr" 5322 - version = "0.7.6" 5739 + version = "0.8.1" 5323 5740 source = "registry+https://github.com/rust-lang/crates.io-index" 5324 - checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 5741 + checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 5325 5742 dependencies = [ 5326 5743 "displaydoc", 5327 5744 "zerovec", ··· 5329 5746 5330 5747 [[package]] 5331 5748 name = "tinyvec" 5332 - version = "1.8.1" 5749 + version = "1.9.0" 5333 5750 source = "registry+https://github.com/rust-lang/crates.io-index" 5334 - checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" 5751 + checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 5335 5752 dependencies = [ 5336 5753 "tinyvec_macros", 5337 5754 ] ··· 5344 5761 5345 5762 [[package]] 5346 5763 name = "tokio" 5347 - version = "1.43.0" 5764 + version = "1.45.1" 5348 5765 source = "registry+https://github.com/rust-lang/crates.io-index" 5349 - checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 5766 + checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 5350 5767 dependencies = [ 5351 5768 "backtrace", 5352 5769 "bytes", ··· 5368 5785 dependencies = [ 5369 5786 "proc-macro2", 5370 5787 "quote", 5371 - "syn 2.0.98", 5788 + "syn 2.0.101", 5372 5789 ] 5373 5790 5374 5791 [[package]] ··· 5383 5800 5384 5801 [[package]] 5385 5802 name = "tokio-rustls" 5386 - version = "0.26.1" 5803 + version = "0.26.2" 5387 5804 source = "registry+https://github.com/rust-lang/crates.io-index" 5388 - checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" 5805 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 5389 5806 dependencies = [ 5390 - "rustls 0.23.23", 5807 + "rustls 0.23.27", 5391 5808 "tokio", 5392 5809 ] 5393 5810 ··· 5411 5828 dependencies = [ 5412 5829 "futures-util", 5413 5830 "log", 5414 - "rustls 0.23.23", 5831 + "rustls 0.23.27", 5415 5832 "rustls-pki-types", 5416 5833 "tokio", 5417 - "tokio-rustls 0.26.1", 5834 + "tokio-rustls 0.26.2", 5418 5835 "tungstenite", 5419 - "webpki-roots", 5836 + "webpki-roots 0.26.11", 5420 5837 ] 5421 5838 5422 5839 [[package]] 5423 5840 name = "tokio-util" 5424 - version = "0.7.13" 5841 + version = "0.7.15" 5425 5842 source = "registry+https://github.com/rust-lang/crates.io-index" 5426 - checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 5843 + checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 5427 5844 dependencies = [ 5428 5845 "bytes", 5429 5846 "futures-core", 5847 + "futures-io", 5430 5848 "futures-sink", 5431 5849 "pin-project-lite", 5432 5850 "tokio", ··· 5442 5860 "bytes", 5443 5861 "futures-core", 5444 5862 "futures-sink", 5445 - "http 1.2.0", 5863 + "http 1.3.1", 5446 5864 "httparse", 5447 5865 "rand 0.8.5", 5448 5866 "ring", 5449 5867 "rustls-pki-types", 5450 5868 "tokio", 5451 - "tokio-rustls 0.26.1", 5869 + "tokio-rustls 0.26.2", 5452 5870 "tokio-util", 5453 - "webpki-roots", 5871 + "webpki-roots 0.26.11", 5454 5872 ] 5455 5873 5456 5874 [[package]] 5457 5875 name = "toml_datetime" 5458 - version = "0.6.8" 5876 + version = "0.6.9" 5459 5877 source = "registry+https://github.com/rust-lang/crates.io-index" 5460 - checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 5878 + checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 5461 5879 5462 5880 [[package]] 5463 5881 name = "toml_edit" 5464 - version = "0.22.24" 5882 + version = "0.22.26" 5465 5883 source = "registry+https://github.com/rust-lang/crates.io-index" 5466 - checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 5884 + checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 5467 5885 dependencies = [ 5468 5886 "indexmap", 5469 5887 "toml_datetime", ··· 5517 5935 dependencies = [ 5518 5936 "proc-macro2", 5519 5937 "quote", 5520 - "syn 2.0.98", 5938 + "syn 2.0.101", 5521 5939 ] 5522 5940 5523 5941 [[package]] ··· 5560 5978 dependencies = [ 5561 5979 "bytes", 5562 5980 "data-encoding", 5563 - "http 1.2.0", 5981 + "http 1.3.1", 5564 5982 "httparse", 5565 5983 "log", 5566 - "rand 0.9.0", 5567 - "rustls 0.23.23", 5984 + "rand 0.9.1", 5985 + "rustls 0.23.27", 5568 5986 "rustls-pki-types", 5569 5987 "sha1", 5570 - "thiserror 2.0.11", 5988 + "thiserror 2.0.12", 5571 5989 "utf-8", 5572 5990 ] 5573 5991 ··· 5591 6009 5592 6010 [[package]] 5593 6011 name = "unicode-ident" 5594 - version = "1.0.17" 6012 + version = "1.0.18" 5595 6013 source = "registry+https://github.com/rust-lang/crates.io-index" 5596 - checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 6014 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 5597 6015 5598 6016 [[package]] 5599 6017 name = "unicode-normalization" ··· 5671 6089 checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 5672 6090 5673 6091 [[package]] 5674 - name = "utf16_iter" 5675 - version = "1.0.5" 5676 - source = "registry+https://github.com/rust-lang/crates.io-index" 5677 - checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 5678 - 5679 - [[package]] 5680 6092 name = "utf8_iter" 5681 6093 version = "1.0.4" 5682 6094 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5694 6106 source = "registry+https://github.com/rust-lang/crates.io-index" 5695 6107 checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 5696 6108 dependencies = [ 5697 - "getrandom 0.3.1", 6109 + "getrandom 0.3.3", 5698 6110 "js-sys", 5699 6111 "wasm-bindgen", 5700 6112 ] ··· 5712 6124 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 5713 6125 5714 6126 [[package]] 6127 + name = "walkdir" 6128 + version = "2.5.0" 6129 + source = "registry+https://github.com/rust-lang/crates.io-index" 6130 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 6131 + dependencies = [ 6132 + "same-file", 6133 + "winapi-util", 6134 + ] 6135 + 6136 + [[package]] 5715 6137 name = "want" 5716 6138 version = "0.3.1" 5717 6139 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5728 6150 5729 6151 [[package]] 5730 6152 name = "wasi" 5731 - version = "0.13.3+wasi-0.2.2" 6153 + version = "0.14.2+wasi-0.2.4" 5732 6154 source = "registry+https://github.com/rust-lang/crates.io-index" 5733 - checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 6155 + checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 5734 6156 dependencies = [ 5735 6157 "wit-bindgen-rt", 5736 6158 ] ··· 5763 6185 "log", 5764 6186 "proc-macro2", 5765 6187 "quote", 5766 - "syn 2.0.98", 6188 + "syn 2.0.101", 5767 6189 "wasm-bindgen-shared", 5768 6190 ] 5769 6191 ··· 5798 6220 dependencies = [ 5799 6221 "proc-macro2", 5800 6222 "quote", 5801 - "syn 2.0.98", 6223 + "syn 2.0.101", 5802 6224 "wasm-bindgen-backend", 5803 6225 "wasm-bindgen-shared", 5804 6226 ] ··· 5846 6268 ] 5847 6269 5848 6270 [[package]] 5849 - name = "webpki-roots" 6271 + name = "webpki-root-certs" 5850 6272 version = "0.26.8" 5851 6273 source = "registry+https://github.com/rust-lang/crates.io-index" 5852 - checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" 6274 + checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" 6275 + dependencies = [ 6276 + "rustls-pki-types", 6277 + ] 6278 + 6279 + [[package]] 6280 + name = "webpki-roots" 6281 + version = "0.26.11" 6282 + source = "registry+https://github.com/rust-lang/crates.io-index" 6283 + checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 6284 + dependencies = [ 6285 + "webpki-roots 1.0.0", 6286 + ] 6287 + 6288 + [[package]] 6289 + name = "webpki-roots" 6290 + version = "1.0.0" 6291 + source = "registry+https://github.com/rust-lang/crates.io-index" 6292 + checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" 5853 6293 dependencies = [ 5854 6294 "rustls-pki-types", 5855 6295 ] ··· 5870 6310 "jsonwebtoken", 5871 6311 "md5", 5872 6312 "owo-colors", 5873 - "rand 0.9.0", 5874 - "redis 0.29.0", 6313 + "rand 0.9.1", 6314 + "redis 0.29.5", 5875 6315 "reqwest", 5876 6316 "serde", 5877 6317 "serde_json", ··· 5882 6322 5883 6323 [[package]] 5884 6324 name = "whoami" 5885 - version = "1.5.2" 6325 + version = "1.6.0" 5886 6326 source = "registry+https://github.com/rust-lang/crates.io-index" 5887 - checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" 6327 + checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" 5888 6328 dependencies = [ 5889 6329 "redox_syscall", 5890 6330 "wasite", ··· 5907 6347 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 5908 6348 5909 6349 [[package]] 6350 + name = "winapi-util" 6351 + version = "0.1.9" 6352 + source = "registry+https://github.com/rust-lang/crates.io-index" 6353 + checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 6354 + dependencies = [ 6355 + "windows-sys 0.48.0", 6356 + ] 6357 + 6358 + [[package]] 5910 6359 name = "winapi-x86_64-pc-windows-gnu" 5911 6360 version = "0.4.0" 5912 6361 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5924 6373 5925 6374 [[package]] 5926 6375 name = "windows-core" 5927 - version = "0.52.0" 6376 + version = "0.57.0" 5928 6377 source = "registry+https://github.com/rust-lang/crates.io-index" 5929 - checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 6378 + checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" 5930 6379 dependencies = [ 6380 + "windows-implement 0.57.0", 6381 + "windows-interface 0.57.0", 6382 + "windows-result 0.1.2", 5931 6383 "windows-targets 0.52.6", 5932 6384 ] 5933 6385 5934 6386 [[package]] 5935 6387 name = "windows-core" 5936 - version = "0.57.0" 6388 + version = "0.61.2" 5937 6389 source = "registry+https://github.com/rust-lang/crates.io-index" 5938 - checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" 6390 + checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 5939 6391 dependencies = [ 5940 - "windows-implement", 5941 - "windows-interface", 5942 - "windows-result 0.1.2", 5943 - "windows-targets 0.52.6", 6392 + "windows-implement 0.60.0", 6393 + "windows-interface 0.59.1", 6394 + "windows-link", 6395 + "windows-result 0.3.4", 6396 + "windows-strings 0.4.2", 5944 6397 ] 5945 6398 5946 6399 [[package]] ··· 5951 6404 dependencies = [ 5952 6405 "proc-macro2", 5953 6406 "quote", 5954 - "syn 2.0.98", 6407 + "syn 2.0.101", 6408 + ] 6409 + 6410 + [[package]] 6411 + name = "windows-implement" 6412 + version = "0.60.0" 6413 + source = "registry+https://github.com/rust-lang/crates.io-index" 6414 + checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 6415 + dependencies = [ 6416 + "proc-macro2", 6417 + "quote", 6418 + "syn 2.0.101", 5955 6419 ] 5956 6420 5957 6421 [[package]] ··· 5962 6426 dependencies = [ 5963 6427 "proc-macro2", 5964 6428 "quote", 5965 - "syn 2.0.98", 6429 + "syn 2.0.101", 6430 + ] 6431 + 6432 + [[package]] 6433 + name = "windows-interface" 6434 + version = "0.59.1" 6435 + source = "registry+https://github.com/rust-lang/crates.io-index" 6436 + checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 6437 + dependencies = [ 6438 + "proc-macro2", 6439 + "quote", 6440 + "syn 2.0.101", 5966 6441 ] 5967 6442 5968 6443 [[package]] 6444 + name = "windows-link" 6445 + version = "0.1.1" 6446 + source = "registry+https://github.com/rust-lang/crates.io-index" 6447 + checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 6448 + 6449 + [[package]] 5969 6450 name = "windows-registry" 5970 - version = "0.2.0" 6451 + version = "0.4.0" 5971 6452 source = "registry+https://github.com/rust-lang/crates.io-index" 5972 - checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 6453 + checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" 5973 6454 dependencies = [ 5974 - "windows-result 0.2.0", 5975 - "windows-strings", 5976 - "windows-targets 0.52.6", 6455 + "windows-result 0.3.4", 6456 + "windows-strings 0.3.1", 6457 + "windows-targets 0.53.0", 5977 6458 ] 5978 6459 5979 6460 [[package]] ··· 5987 6468 5988 6469 [[package]] 5989 6470 name = "windows-result" 5990 - version = "0.2.0" 6471 + version = "0.3.4" 5991 6472 source = "registry+https://github.com/rust-lang/crates.io-index" 5992 - checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 6473 + checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 5993 6474 dependencies = [ 5994 - "windows-targets 0.52.6", 6475 + "windows-link", 5995 6476 ] 5996 6477 5997 6478 [[package]] 5998 6479 name = "windows-strings" 5999 - version = "0.1.0" 6480 + version = "0.3.1" 6000 6481 source = "registry+https://github.com/rust-lang/crates.io-index" 6001 - checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 6482 + checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 6002 6483 dependencies = [ 6003 - "windows-result 0.2.0", 6004 - "windows-targets 0.52.6", 6484 + "windows-link", 6485 + ] 6486 + 6487 + [[package]] 6488 + name = "windows-strings" 6489 + version = "0.4.2" 6490 + source = "registry+https://github.com/rust-lang/crates.io-index" 6491 + checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 6492 + dependencies = [ 6493 + "windows-link", 6494 + ] 6495 + 6496 + [[package]] 6497 + name = "windows-sys" 6498 + version = "0.45.0" 6499 + source = "registry+https://github.com/rust-lang/crates.io-index" 6500 + checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 6501 + dependencies = [ 6502 + "windows-targets 0.42.2", 6005 6503 ] 6006 6504 6007 6505 [[package]] ··· 6033 6531 6034 6532 [[package]] 6035 6533 name = "windows-targets" 6534 + version = "0.42.2" 6535 + source = "registry+https://github.com/rust-lang/crates.io-index" 6536 + checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 6537 + dependencies = [ 6538 + "windows_aarch64_gnullvm 0.42.2", 6539 + "windows_aarch64_msvc 0.42.2", 6540 + "windows_i686_gnu 0.42.2", 6541 + "windows_i686_msvc 0.42.2", 6542 + "windows_x86_64_gnu 0.42.2", 6543 + "windows_x86_64_gnullvm 0.42.2", 6544 + "windows_x86_64_msvc 0.42.2", 6545 + ] 6546 + 6547 + [[package]] 6548 + name = "windows-targets" 6036 6549 version = "0.48.5" 6037 6550 source = "registry+https://github.com/rust-lang/crates.io-index" 6038 6551 checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" ··· 6055 6568 "windows_aarch64_gnullvm 0.52.6", 6056 6569 "windows_aarch64_msvc 0.52.6", 6057 6570 "windows_i686_gnu 0.52.6", 6058 - "windows_i686_gnullvm", 6571 + "windows_i686_gnullvm 0.52.6", 6059 6572 "windows_i686_msvc 0.52.6", 6060 6573 "windows_x86_64_gnu 0.52.6", 6061 6574 "windows_x86_64_gnullvm 0.52.6", ··· 6063 6576 ] 6064 6577 6065 6578 [[package]] 6579 + name = "windows-targets" 6580 + version = "0.53.0" 6581 + source = "registry+https://github.com/rust-lang/crates.io-index" 6582 + checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 6583 + dependencies = [ 6584 + "windows_aarch64_gnullvm 0.53.0", 6585 + "windows_aarch64_msvc 0.53.0", 6586 + "windows_i686_gnu 0.53.0", 6587 + "windows_i686_gnullvm 0.53.0", 6588 + "windows_i686_msvc 0.53.0", 6589 + "windows_x86_64_gnu 0.53.0", 6590 + "windows_x86_64_gnullvm 0.53.0", 6591 + "windows_x86_64_msvc 0.53.0", 6592 + ] 6593 + 6594 + [[package]] 6595 + name = "windows_aarch64_gnullvm" 6596 + version = "0.42.2" 6597 + source = "registry+https://github.com/rust-lang/crates.io-index" 6598 + checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 6599 + 6600 + [[package]] 6066 6601 name = "windows_aarch64_gnullvm" 6067 6602 version = "0.48.5" 6068 6603 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6075 6610 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 6076 6611 6077 6612 [[package]] 6613 + name = "windows_aarch64_gnullvm" 6614 + version = "0.53.0" 6615 + source = "registry+https://github.com/rust-lang/crates.io-index" 6616 + checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 6617 + 6618 + [[package]] 6619 + name = "windows_aarch64_msvc" 6620 + version = "0.42.2" 6621 + source = "registry+https://github.com/rust-lang/crates.io-index" 6622 + checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 6623 + 6624 + [[package]] 6078 6625 name = "windows_aarch64_msvc" 6079 6626 version = "0.48.5" 6080 6627 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6087 6634 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 6088 6635 6089 6636 [[package]] 6637 + name = "windows_aarch64_msvc" 6638 + version = "0.53.0" 6639 + source = "registry+https://github.com/rust-lang/crates.io-index" 6640 + checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 6641 + 6642 + [[package]] 6643 + name = "windows_i686_gnu" 6644 + version = "0.42.2" 6645 + source = "registry+https://github.com/rust-lang/crates.io-index" 6646 + checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 6647 + 6648 + [[package]] 6090 6649 name = "windows_i686_gnu" 6091 6650 version = "0.48.5" 6092 6651 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6097 6656 version = "0.52.6" 6098 6657 source = "registry+https://github.com/rust-lang/crates.io-index" 6099 6658 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 6659 + 6660 + [[package]] 6661 + name = "windows_i686_gnu" 6662 + version = "0.53.0" 6663 + source = "registry+https://github.com/rust-lang/crates.io-index" 6664 + checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 6100 6665 6101 6666 [[package]] 6102 6667 name = "windows_i686_gnullvm" ··· 6105 6670 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 6106 6671 6107 6672 [[package]] 6673 + name = "windows_i686_gnullvm" 6674 + version = "0.53.0" 6675 + source = "registry+https://github.com/rust-lang/crates.io-index" 6676 + checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 6677 + 6678 + [[package]] 6679 + name = "windows_i686_msvc" 6680 + version = "0.42.2" 6681 + source = "registry+https://github.com/rust-lang/crates.io-index" 6682 + checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 6683 + 6684 + [[package]] 6108 6685 name = "windows_i686_msvc" 6109 6686 version = "0.48.5" 6110 6687 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6117 6694 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 6118 6695 6119 6696 [[package]] 6697 + name = "windows_i686_msvc" 6698 + version = "0.53.0" 6699 + source = "registry+https://github.com/rust-lang/crates.io-index" 6700 + checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 6701 + 6702 + [[package]] 6703 + name = "windows_x86_64_gnu" 6704 + version = "0.42.2" 6705 + source = "registry+https://github.com/rust-lang/crates.io-index" 6706 + checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 6707 + 6708 + [[package]] 6120 6709 name = "windows_x86_64_gnu" 6121 6710 version = "0.48.5" 6122 6711 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6129 6718 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 6130 6719 6131 6720 [[package]] 6721 + name = "windows_x86_64_gnu" 6722 + version = "0.53.0" 6723 + source = "registry+https://github.com/rust-lang/crates.io-index" 6724 + checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 6725 + 6726 + [[package]] 6727 + name = "windows_x86_64_gnullvm" 6728 + version = "0.42.2" 6729 + source = "registry+https://github.com/rust-lang/crates.io-index" 6730 + checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 6731 + 6732 + [[package]] 6132 6733 name = "windows_x86_64_gnullvm" 6133 6734 version = "0.48.5" 6134 6735 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6141 6742 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 6142 6743 6143 6744 [[package]] 6745 + name = "windows_x86_64_gnullvm" 6746 + version = "0.53.0" 6747 + source = "registry+https://github.com/rust-lang/crates.io-index" 6748 + checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 6749 + 6750 + [[package]] 6751 + name = "windows_x86_64_msvc" 6752 + version = "0.42.2" 6753 + source = "registry+https://github.com/rust-lang/crates.io-index" 6754 + checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 6755 + 6756 + [[package]] 6144 6757 name = "windows_x86_64_msvc" 6145 6758 version = "0.48.5" 6146 6759 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 6151 6764 version = "0.52.6" 6152 6765 source = "registry+https://github.com/rust-lang/crates.io-index" 6153 6766 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 6767 + 6768 + [[package]] 6769 + name = "windows_x86_64_msvc" 6770 + version = "0.53.0" 6771 + source = "registry+https://github.com/rust-lang/crates.io-index" 6772 + checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 6154 6773 6155 6774 [[package]] 6156 6775 name = "winnow" 6157 - version = "0.7.3" 6776 + version = "0.7.10" 6158 6777 source = "registry+https://github.com/rust-lang/crates.io-index" 6159 - checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" 6778 + checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 6160 6779 dependencies = [ 6161 6780 "memchr", 6162 6781 ] 6163 6782 6164 6783 [[package]] 6165 6784 name = "wit-bindgen-rt" 6166 - version = "0.33.0" 6785 + version = "0.39.0" 6167 6786 source = "registry+https://github.com/rust-lang/crates.io-index" 6168 - checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 6787 + checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 6169 6788 dependencies = [ 6170 - "bitflags 2.8.0", 6789 + "bitflags 2.9.1", 6171 6790 ] 6172 6791 6173 6792 [[package]] 6174 - name = "write16" 6175 - version = "1.0.0" 6176 - source = "registry+https://github.com/rust-lang/crates.io-index" 6177 - checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 6178 - 6179 - [[package]] 6180 6793 name = "writeable" 6181 - version = "0.5.5" 6794 + version = "0.6.1" 6182 6795 source = "registry+https://github.com/rust-lang/crates.io-index" 6183 - checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 6796 + checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 6184 6797 6185 6798 [[package]] 6186 6799 name = "wyz" ··· 6193 6806 6194 6807 [[package]] 6195 6808 name = "xattr" 6196 - version = "1.4.0" 6809 + version = "1.5.0" 6197 6810 source = "registry+https://github.com/rust-lang/crates.io-index" 6198 - checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" 6811 + checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" 6199 6812 dependencies = [ 6200 6813 "libc", 6201 - "linux-raw-sys 0.4.15", 6202 - "rustix 0.38.44", 6814 + "rustix 1.0.7", 6203 6815 ] 6204 6816 6205 6817 [[package]] ··· 6210 6822 6211 6823 [[package]] 6212 6824 name = "yoke" 6213 - version = "0.7.5" 6825 + version = "0.8.0" 6214 6826 source = "registry+https://github.com/rust-lang/crates.io-index" 6215 - checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 6827 + checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 6216 6828 dependencies = [ 6217 6829 "serde", 6218 6830 "stable_deref_trait", ··· 6222 6834 6223 6835 [[package]] 6224 6836 name = "yoke-derive" 6225 - version = "0.7.5" 6837 + version = "0.8.0" 6226 6838 source = "registry+https://github.com/rust-lang/crates.io-index" 6227 - checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 6839 + checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 6228 6840 dependencies = [ 6229 6841 "proc-macro2", 6230 6842 "quote", 6231 - "syn 2.0.98", 6843 + "syn 2.0.101", 6232 6844 "synstructure", 6233 6845 ] 6234 6846 6235 6847 [[package]] 6236 6848 name = "zerocopy" 6237 - version = "0.7.35" 6849 + version = "0.8.25" 6238 6850 source = "registry+https://github.com/rust-lang/crates.io-index" 6239 - checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 6851 + checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 6240 6852 dependencies = [ 6241 - "byteorder", 6242 - "zerocopy-derive 0.7.35", 6243 - ] 6244 - 6245 - [[package]] 6246 - name = "zerocopy" 6247 - version = "0.8.24" 6248 - source = "registry+https://github.com/rust-lang/crates.io-index" 6249 - checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 6250 - dependencies = [ 6251 - "zerocopy-derive 0.8.24", 6853 + "zerocopy-derive", 6252 6854 ] 6253 6855 6254 6856 [[package]] 6255 6857 name = "zerocopy-derive" 6256 - version = "0.7.35" 6858 + version = "0.8.25" 6257 6859 source = "registry+https://github.com/rust-lang/crates.io-index" 6258 - checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 6860 + checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 6259 6861 dependencies = [ 6260 6862 "proc-macro2", 6261 6863 "quote", 6262 - "syn 2.0.98", 6263 - ] 6264 - 6265 - [[package]] 6266 - name = "zerocopy-derive" 6267 - version = "0.8.24" 6268 - source = "registry+https://github.com/rust-lang/crates.io-index" 6269 - checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 6270 - dependencies = [ 6271 - "proc-macro2", 6272 - "quote", 6273 - "syn 2.0.98", 6864 + "syn 2.0.101", 6274 6865 ] 6275 6866 6276 6867 [[package]] 6277 6868 name = "zerofrom" 6278 - version = "0.1.5" 6869 + version = "0.1.6" 6279 6870 source = "registry+https://github.com/rust-lang/crates.io-index" 6280 - checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 6871 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 6281 6872 dependencies = [ 6282 6873 "zerofrom-derive", 6283 6874 ] 6284 6875 6285 6876 [[package]] 6286 6877 name = "zerofrom-derive" 6287 - version = "0.1.5" 6878 + version = "0.1.6" 6288 6879 source = "registry+https://github.com/rust-lang/crates.io-index" 6289 - checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 6880 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 6290 6881 dependencies = [ 6291 6882 "proc-macro2", 6292 6883 "quote", 6293 - "syn 2.0.98", 6884 + "syn 2.0.101", 6294 6885 "synstructure", 6295 6886 ] 6296 6887 ··· 6301 6892 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 6302 6893 6303 6894 [[package]] 6895 + name = "zerotrie" 6896 + version = "0.2.2" 6897 + source = "registry+https://github.com/rust-lang/crates.io-index" 6898 + checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 6899 + dependencies = [ 6900 + "displaydoc", 6901 + "yoke", 6902 + "zerofrom", 6903 + ] 6904 + 6905 + [[package]] 6304 6906 name = "zerovec" 6305 - version = "0.10.4" 6907 + version = "0.11.2" 6306 6908 source = "registry+https://github.com/rust-lang/crates.io-index" 6307 - checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 6909 + checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 6308 6910 dependencies = [ 6309 6911 "yoke", 6310 6912 "zerofrom", ··· 6313 6915 6314 6916 [[package]] 6315 6917 name = "zerovec-derive" 6316 - version = "0.10.3" 6918 + version = "0.11.1" 6317 6919 source = "registry+https://github.com/rust-lang/crates.io-index" 6318 - checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 6920 + checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 6319 6921 dependencies = [ 6320 6922 "proc-macro2", 6321 6923 "quote", 6322 - "syn 2.0.98", 6924 + "syn 2.0.101", 6323 6925 ] 6324 6926 6325 6927 [[package]] ··· 6333 6935 6334 6936 [[package]] 6335 6937 name = "zstd-safe" 6336 - version = "7.2.3" 6938 + version = "7.2.4" 6337 6939 source = "registry+https://github.com/rust-lang/crates.io-index" 6338 - checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" 6940 + checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 6339 6941 dependencies = [ 6340 6942 "zstd-sys", 6341 6943 ] 6342 6944 6343 6945 [[package]] 6344 6946 name = "zstd-sys" 6345 - version = "2.0.14+zstd.1.5.7" 6947 + version = "2.0.15+zstd.1.5.7" 6346 6948 source = "registry+https://github.com/rust-lang/crates.io-index" 6347 - checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" 6949 + checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 6348 6950 dependencies = [ 6349 6951 "cc", 6350 6952 "pkg-config",
+38 -33
crates/analytics/src/cmd/serve.rs
··· 1 1 use std::env; 2 2 3 - use actix_web::{get, post, web::{self, Data}, App, HttpRequest, HttpResponse, HttpServer, Responder}; 4 - use duckdb::Connection; 3 + use actix_web::{ 4 + get, post, 5 + web::{self, Data}, 6 + App, HttpRequest, HttpResponse, HttpServer, Responder, 7 + }; 5 8 use anyhow::Error; 9 + use duckdb::Connection; 6 10 use owo_colors::OwoColorize; 7 11 use serde_json::json; 8 12 use std::sync::{Arc, Mutex}; ··· 12 16 // return json response 13 17 #[get("/")] 14 18 async fn index(_req: HttpRequest) -> HttpResponse { 15 - HttpResponse::Ok().json(json!({ 16 - "server": "Rocksky Analytics Server", 17 - "version": "0.1.0", 18 - })) 19 + HttpResponse::Ok().json(json!({ 20 + "server": "Rocksky Analytics Server", 21 + "version": "0.1.0", 22 + })) 19 23 } 20 24 21 25 #[post("/{method}")] 22 26 async fn call_method( 23 - data: web::Data<Arc<Mutex<Connection>>>, 24 - mut payload: web::Payload, 25 - req: HttpRequest) -> Result<impl Responder, actix_web::Error> { 26 - let method = req.match_info().get("method").unwrap_or("unknown"); 27 - println!("Method: {}", method.bright_green()); 27 + data: web::Data<Arc<Mutex<Connection>>>, 28 + mut payload: web::Payload, 29 + req: HttpRequest, 30 + ) -> Result<impl Responder, actix_web::Error> { 31 + let method = req.match_info().get("method").unwrap_or("unknown"); 32 + println!("Method: {}", method.bright_green()); 28 33 29 - let conn = data.get_ref().clone(); 30 - handle(method, &mut payload, &req, conn).await 31 - .map_err(actix_web::error::ErrorInternalServerError) 34 + let conn = data.get_ref().clone(); 35 + handle(method, &mut payload, &req, conn) 36 + .await 37 + .map_err(actix_web::error::ErrorInternalServerError) 32 38 } 33 39 34 - 35 40 pub async fn serve(conn: Arc<Mutex<Connection>>) -> Result<(), Error> { 36 - subscribe(conn.clone()).await?; 41 + subscribe(conn.clone()).await?; 37 42 38 - let host = env::var("ANALYTICS_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 39 - let port = env::var("ANALYTICS_PORT").unwrap_or_else(|_| "7879".to_string()); 40 - let addr = format!("{}:{}", host, port); 43 + let host = env::var("ANALYTICS_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 44 + let port = env::var("ANALYTICS_PORT").unwrap_or_else(|_| "7879".to_string()); 45 + let addr = format!("{}:{}", host, port); 41 46 42 - let url = format!("http://{}", addr); 43 - println!("Listening on {}", url.bright_green()); 47 + let url = format!("http://{}", addr); 48 + println!("Listening on {}", url.bright_green()); 44 49 45 - let conn = conn.clone(); 46 - HttpServer::new(move || { 47 - App::new() 48 - .app_data(Data::new(conn.clone())) 49 - .service(index) 50 - .service(call_method) 51 - }) 52 - .bind(&addr)? 53 - .run() 54 - .await 55 - .map_err(Error::new)?; 50 + let conn = conn.clone(); 51 + HttpServer::new(move || { 52 + App::new() 53 + .app_data(Data::new(conn.clone())) 54 + .service(index) 55 + .service(call_method) 56 + }) 57 + .bind(&addr)? 58 + .run() 59 + .await 60 + .map_err(Error::new)?; 56 61 57 - Ok(()) 62 + Ok(()) 58 63 }
+1 -1
crates/analytics/src/cmd/sync.rs
··· 1 1 use std::sync::{Arc, Mutex}; 2 2 3 + use crate::core::*; 3 4 use anyhow::Error; 4 5 use duckdb::Connection; 5 6 use sqlx::{Pool, Postgres}; 6 - use crate::core::*; 7 7 8 8 pub async fn sync(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 9 9 load_tracks(conn.clone(), pool).await?;
+402 -310
crates/analytics/src/core.rs
··· 1 1 use std::sync::{Arc, Mutex}; 2 2 3 - use duckdb::{params, Connection}; 4 3 use anyhow::Error; 4 + use duckdb::{params, Connection}; 5 5 use owo_colors::OwoColorize; 6 6 use sqlx::{Pool, Postgres}; 7 7 8 8 use crate::xata; 9 9 10 - 11 10 pub async fn create_tables(conn: &Connection) -> Result<(), Error> { 12 - conn.execute_batch( 13 - "BEGIN; 11 + conn.execute_batch( 12 + "BEGIN; 14 13 CREATE TABLE IF NOT EXISTS artists ( 15 14 id VARCHAR PRIMARY KEY, 16 15 name VARCHAR NOT NULL, ··· 179 178 ); 180 179 COMMIT; 181 180 ", 182 - )?; 181 + )?; 183 182 184 - Ok(()) 183 + Ok(()) 185 184 } 186 185 187 186 pub async fn load_tracks(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 188 - let conn = conn.lock().unwrap(); 189 - let tracks: Vec<xata::track::Track> = sqlx::query_as(r#" 187 + let conn = conn.lock().unwrap(); 188 + let tracks: Vec<xata::track::Track> = sqlx::query_as( 189 + r#" 190 190 SELECT * FROM tracks 191 - "#) 192 - .fetch_all(pool) 193 - .await?; 191 + "#, 192 + ) 193 + .fetch_all(pool) 194 + .await?; 194 195 195 - for (i, track) in tracks.clone().into_iter().enumerate() { 196 - println!("track {} - {} - {}", i, track.title.bright_green(), track.artist); 197 - match conn.execute( 198 - "INSERT INTO tracks ( 196 + for (i, track) in tracks.clone().into_iter().enumerate() { 197 + println!( 198 + "track {} - {} - {}", 199 + i, 200 + track.title.bright_green(), 201 + track.artist 202 + ); 203 + match conn.execute( 204 + "INSERT INTO tracks ( 199 205 id, 200 206 title, 201 207 artist, ··· 221 227 album_uri, 222 228 created_at 223 229 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 224 - params![ 225 - track.xata_id, 226 - track.title, 227 - track.artist, 228 - track.album_artist, 229 - track.album_art, 230 - track.album, 231 - track.track_number, 232 - track.duration, 233 - track.mb_id, 234 - track.youtube_link, 235 - track.spotify_link, 236 - track.tidal_link, 237 - track.apple_music_link, 238 - track.sha256, 239 - track.lyrics, 240 - track.composer, 241 - track.genre, 242 - track.disc_number, 243 - track.copyright_message, 244 - track.label, 245 - track.uri, 246 - track.artist_uri, 247 - track.album_uri, 248 - track.xata_createdat, 249 - ], 250 - ) { 251 - Ok(_) => (), 252 - Err(e) => println!("error: {}", e), 253 - } 254 - } 230 + params![ 231 + track.xata_id, 232 + track.title, 233 + track.artist, 234 + track.album_artist, 235 + track.album_art, 236 + track.album, 237 + track.track_number, 238 + track.duration, 239 + track.mb_id, 240 + track.youtube_link, 241 + track.spotify_link, 242 + track.tidal_link, 243 + track.apple_music_link, 244 + track.sha256, 245 + track.lyrics, 246 + track.composer, 247 + track.genre, 248 + track.disc_number, 249 + track.copyright_message, 250 + track.label, 251 + track.uri, 252 + track.artist_uri, 253 + track.album_uri, 254 + track.xata_createdat, 255 + ], 256 + ) { 257 + Ok(_) => (), 258 + Err(e) => println!("error: {}", e), 259 + } 260 + } 255 261 256 - println!("tracks: {:?}", tracks.len()); 257 - Ok(()) 262 + println!("tracks: {:?}", tracks.len()); 263 + Ok(()) 258 264 } 259 265 260 - pub async fn load_artists(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 261 - let conn = conn.lock().unwrap(); 262 - let artists: Vec<xata::artist::Artist> = sqlx::query_as(r#" 266 + pub async fn load_artists( 267 + conn: Arc<Mutex<Connection>>, 268 + pool: &Pool<Postgres>, 269 + ) -> Result<(), Error> { 270 + let conn = conn.lock().unwrap(); 271 + let artists: Vec<xata::artist::Artist> = sqlx::query_as( 272 + r#" 263 273 SELECT * FROM artists 264 - "#) 265 - .fetch_all(pool) 266 - .await?; 274 + "#, 275 + ) 276 + .fetch_all(pool) 277 + .await?; 267 278 268 - for (i, artist) in artists.clone().into_iter().enumerate() { 269 - println!("artist {} - {}", i, artist.name.bright_green()); 270 - match conn.execute( 271 - "INSERT INTO artists ( 279 + for (i, artist) in artists.clone().into_iter().enumerate() { 280 + println!("artist {} - {}", i, artist.name.bright_green()); 281 + match conn.execute( 282 + "INSERT INTO artists ( 272 283 id, 273 284 name, 274 285 biography, ··· 295 306 ?, 296 307 ?, 297 308 ?)", 298 - params![ 299 - artist.xata_id, 300 - artist.name, 301 - artist.biography, 302 - artist.born, 303 - artist.born_in, 304 - artist.died, 305 - artist.picture, 306 - artist.sha256, 307 - artist.spotify_link, 308 - artist.tidal_link, 309 - artist.youtube_link, 310 - artist.apple_music_link, 311 - artist.uri, 312 - ], 313 - ) { 314 - Ok(_) => (), 315 - Err(e) => println!("error: {}", e), 316 - } 317 - } 309 + params![ 310 + artist.xata_id, 311 + artist.name, 312 + artist.biography, 313 + artist.born, 314 + artist.born_in, 315 + artist.died, 316 + artist.picture, 317 + artist.sha256, 318 + artist.spotify_link, 319 + artist.tidal_link, 320 + artist.youtube_link, 321 + artist.apple_music_link, 322 + artist.uri, 323 + ], 324 + ) { 325 + Ok(_) => (), 326 + Err(e) => println!("error: {}", e), 327 + } 328 + } 318 329 319 - println!("artists: {:?}", artists.len()); 320 - Ok(()) 330 + println!("artists: {:?}", artists.len()); 331 + Ok(()) 321 332 } 322 333 323 334 pub async fn load_albums(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 324 - let conn = conn.lock().unwrap(); 325 - let albums: Vec<xata::album::Album> = sqlx::query_as(r#" 335 + let conn = conn.lock().unwrap(); 336 + let albums: Vec<xata::album::Album> = sqlx::query_as( 337 + r#" 326 338 SELECT * FROM albums 327 - "#) 328 - .fetch_all(pool) 329 - .await?; 339 + "#, 340 + ) 341 + .fetch_all(pool) 342 + .await?; 330 343 331 - for (i, album) in albums.clone().into_iter().enumerate() { 332 - println!("album {} - {}", i, album.title.bright_green()); 333 - match conn.execute( 334 - "INSERT INTO albums ( 344 + for (i, album) in albums.clone().into_iter().enumerate() { 345 + println!("album {} - {}", i, album.title.bright_green()); 346 + match conn.execute( 347 + "INSERT INTO albums ( 335 348 id, 336 349 title, 337 350 artist, ··· 358 371 ?, 359 372 ?, 360 373 ?)", 361 - params![ 362 - album.xata_id, 363 - album.title, 364 - album.artist, 365 - album.release_date, 366 - album.album_art, 367 - album.year, 368 - album.spotify_link, 369 - album.tidal_link, 370 - album.youtube_link, 371 - album.apple_music_link, 372 - album.sha256, 373 - album.uri, 374 - album.artist_uri, 375 - ], 376 - ) { 377 - Ok(_) => (), 378 - Err(e) => println!("error: {}", e), 379 - } 380 - } 374 + params![ 375 + album.xata_id, 376 + album.title, 377 + album.artist, 378 + album.release_date, 379 + album.album_art, 380 + album.year, 381 + album.spotify_link, 382 + album.tidal_link, 383 + album.youtube_link, 384 + album.apple_music_link, 385 + album.sha256, 386 + album.uri, 387 + album.artist_uri, 388 + ], 389 + ) { 390 + Ok(_) => (), 391 + Err(e) => println!("error: {}", e), 392 + } 393 + } 381 394 382 - println!("albums: {:?}", albums.len()); 383 - Ok(()) 395 + println!("albums: {:?}", albums.len()); 396 + Ok(()) 384 397 } 385 398 386 399 pub async fn load_users(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 387 - let conn = conn.lock().unwrap(); 388 - let users: Vec<xata::user::User> = sqlx::query_as(r#" 400 + let conn = conn.lock().unwrap(); 401 + let users: Vec<xata::user::User> = sqlx::query_as( 402 + r#" 389 403 SELECT * FROM users 390 - "#) 391 - .fetch_all(pool) 392 - .await?; 404 + "#, 405 + ) 406 + .fetch_all(pool) 407 + .await?; 393 408 394 - for (i, user) in users.clone().into_iter().enumerate() { 395 - println!("user {} - {}", i, user.display_name.bright_green()); 396 - match conn.execute( 397 - "INSERT INTO users ( 409 + for (i, user) in users.clone().into_iter().enumerate() { 410 + println!("user {} - {}", i, user.display_name.bright_green()); 411 + match conn.execute( 412 + "INSERT INTO users ( 398 413 id, 399 414 display_name, 400 415 did, ··· 405 420 ?, 406 421 ?, 407 422 ?)", 408 - params![ 409 - user.xata_id, 410 - user.display_name, 411 - user.did, 412 - user.handle, 413 - user.avatar, 414 - ], 415 - ) { 416 - Ok(_) => (), 417 - Err(e) => println!("error: {}", e), 418 - } 419 - } 423 + params![ 424 + user.xata_id, 425 + user.display_name, 426 + user.did, 427 + user.handle, 428 + user.avatar, 429 + ], 430 + ) { 431 + Ok(_) => (), 432 + Err(e) => println!("error: {}", e), 433 + } 434 + } 420 435 421 - println!("users: {:?}", users.len()); 422 - Ok(()) 436 + println!("users: {:?}", users.len()); 437 + Ok(()) 423 438 } 424 439 425 - pub async fn load_scrobbles(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 426 - let conn = conn.lock().unwrap(); 427 - let scrobbles: Vec<xata::scrobble::Scrobble> = sqlx::query_as(r#" 440 + pub async fn load_scrobbles( 441 + conn: Arc<Mutex<Connection>>, 442 + pool: &Pool<Postgres>, 443 + ) -> Result<(), Error> { 444 + let conn = conn.lock().unwrap(); 445 + let scrobbles: Vec<xata::scrobble::Scrobble> = sqlx::query_as( 446 + r#" 428 447 SELECT * FROM scrobbles 429 - "#) 430 - .fetch_all(pool) 431 - .await?; 448 + "#, 449 + ) 450 + .fetch_all(pool) 451 + .await?; 432 452 433 - for (i, scrobble) in scrobbles.clone().into_iter().enumerate() { 434 - println!("scrobble {} - {}", i, 435 - match scrobble.uri.clone() { 436 - Some(uri) => uri.to_string(), 437 - None => "None".to_string(), 438 - }.bright_green() 439 - ); 440 - match conn.execute( 441 - "INSERT INTO scrobbles ( 453 + for (i, scrobble) in scrobbles.clone().into_iter().enumerate() { 454 + println!( 455 + "scrobble {} - {}", 456 + i, 457 + match scrobble.uri.clone() { 458 + Some(uri) => uri.to_string(), 459 + None => "None".to_string(), 460 + } 461 + .bright_green() 462 + ); 463 + match conn.execute( 464 + "INSERT INTO scrobbles ( 442 465 id, 443 466 user_id, 444 467 track_id, ··· 455 478 ?, 456 479 ? 457 480 )", 458 - params![ 459 - scrobble.xata_id, 460 - scrobble.user_id, 461 - scrobble.track_id, 462 - scrobble.album_id, 463 - scrobble.artist_id, 464 - scrobble.uri, 465 - scrobble.xata_createdat, 466 - ], 467 - ) { 468 - Ok(_) => (), 469 - Err(e) => println!("error: {}", e), 470 - } 471 - } 481 + params![ 482 + scrobble.xata_id, 483 + scrobble.user_id, 484 + scrobble.track_id, 485 + scrobble.album_id, 486 + scrobble.artist_id, 487 + scrobble.uri, 488 + scrobble.xata_createdat, 489 + ], 490 + ) { 491 + Ok(_) => (), 492 + Err(e) => println!("error: {}", e), 493 + } 494 + } 472 495 473 - println!("scrobbles: {:?}", scrobbles.len()); 474 - Ok(()) 496 + println!("scrobbles: {:?}", scrobbles.len()); 497 + Ok(()) 475 498 } 476 499 477 - pub async fn load_album_tracks(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 478 - let conn = conn.lock().unwrap(); 479 - let album_tracks: Vec<xata::album_track::AlbumTrack> = sqlx::query_as(r#" 500 + pub async fn load_album_tracks( 501 + conn: Arc<Mutex<Connection>>, 502 + pool: &Pool<Postgres>, 503 + ) -> Result<(), Error> { 504 + let conn = conn.lock().unwrap(); 505 + let album_tracks: Vec<xata::album_track::AlbumTrack> = sqlx::query_as( 506 + r#" 480 507 SELECT * FROM album_tracks 481 - "#) 482 - .fetch_all(pool) 483 - .await?; 508 + "#, 509 + ) 510 + .fetch_all(pool) 511 + .await?; 484 512 485 - for (i, album_track) in album_tracks.clone().into_iter().enumerate() { 486 - println!("album_track {} - {} - {}", i, album_track.album_id.bright_green(), album_track.track_id); 487 - match conn.execute( 488 - "INSERT INTO album_tracks ( 513 + for (i, album_track) in album_tracks.clone().into_iter().enumerate() { 514 + println!( 515 + "album_track {} - {} - {}", 516 + i, 517 + album_track.album_id.bright_green(), 518 + album_track.track_id 519 + ); 520 + match conn.execute( 521 + "INSERT INTO album_tracks ( 489 522 id, 490 523 album_id, 491 524 track_id 492 525 ) VALUES (?, 493 526 ?, 494 527 ?)", 495 - params![ 496 - album_track.xata_id, 497 - album_track.album_id, 498 - album_track.track_id, 499 - ], 500 - ) { 501 - Ok(_) => (), 502 - Err(e) => println!("error: {}", e), 503 - } 504 - } 505 - println!("album_tracks: {:?}", album_tracks.len()); 506 - Ok(()) 528 + params![ 529 + album_track.xata_id, 530 + album_track.album_id, 531 + album_track.track_id, 532 + ], 533 + ) { 534 + Ok(_) => (), 535 + Err(e) => println!("error: {}", e), 536 + } 537 + } 538 + println!("album_tracks: {:?}", album_tracks.len()); 539 + Ok(()) 507 540 } 508 541 509 - pub async fn load_loved_tracks(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 510 - let conn = conn.lock().unwrap(); 511 - let loved_tracks: Vec<xata::user_track::UserTrack> = sqlx::query_as(r#" 542 + pub async fn load_loved_tracks( 543 + conn: Arc<Mutex<Connection>>, 544 + pool: &Pool<Postgres>, 545 + ) -> Result<(), Error> { 546 + let conn = conn.lock().unwrap(); 547 + let loved_tracks: Vec<xata::user_track::UserTrack> = sqlx::query_as( 548 + r#" 512 549 SELECT * FROM loved_tracks 513 - "#) 514 - .fetch_all(pool) 515 - .await?; 550 + "#, 551 + ) 552 + .fetch_all(pool) 553 + .await?; 516 554 517 - for (i, loved_track) in loved_tracks.clone().into_iter().enumerate() { 518 - println!("loved_track {} - {} - {}", i, loved_track.user_id.bright_green(), loved_track.track_id); 519 - match conn.execute( 520 - "INSERT INTO loved_tracks ( 555 + for (i, loved_track) in loved_tracks.clone().into_iter().enumerate() { 556 + println!( 557 + "loved_track {} - {} - {}", 558 + i, 559 + loved_track.user_id.bright_green(), 560 + loved_track.track_id 561 + ); 562 + match conn.execute( 563 + "INSERT INTO loved_tracks ( 521 564 id, 522 565 user_id, 523 566 track_id, ··· 526 569 ?, 527 570 ?, 528 571 ?)", 529 - params![ 530 - loved_track.xata_id, 531 - loved_track.user_id, 532 - loved_track.track_id, 533 - loved_track.xata_createdat, 534 - ], 535 - ) { 536 - Ok(_) => (), 537 - Err(e) => println!("error: {}", e), 538 - } 539 - } 572 + params![ 573 + loved_track.xata_id, 574 + loved_track.user_id, 575 + loved_track.track_id, 576 + loved_track.xata_createdat, 577 + ], 578 + ) { 579 + Ok(_) => (), 580 + Err(e) => println!("error: {}", e), 581 + } 582 + } 540 583 541 - println!("loved_tracks: {:?}", loved_tracks.len()); 542 - Ok(()) 584 + println!("loved_tracks: {:?}", loved_tracks.len()); 585 + Ok(()) 543 586 } 544 587 545 - pub async fn load_artist_tracks(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 546 - let conn = conn.lock().unwrap(); 547 - let artist_tracks: Vec<xata::artist_track::ArtistTrack> = sqlx::query_as(r#" 588 + pub async fn load_artist_tracks( 589 + conn: Arc<Mutex<Connection>>, 590 + pool: &Pool<Postgres>, 591 + ) -> Result<(), Error> { 592 + let conn = conn.lock().unwrap(); 593 + let artist_tracks: Vec<xata::artist_track::ArtistTrack> = sqlx::query_as( 594 + r#" 548 595 SELECT * FROM artist_tracks 549 - "#) 550 - .fetch_all(pool) 551 - .await?; 596 + "#, 597 + ) 598 + .fetch_all(pool) 599 + .await?; 552 600 553 - for (i, artist_track) in artist_tracks.clone().into_iter().enumerate() { 554 - println!("artist_track {} - {} - {}", i, artist_track.artist_id.bright_green(), artist_track.track_id); 555 - match conn.execute( 556 - "INSERT INTO artist_tracks (id, artist_id, track_id, created_at) VALUES (?, ?, ?, ?)", 557 - params![ 558 - artist_track.xata_id, 559 - artist_track.artist_id, 560 - artist_track.track_id, 561 - artist_track.xata_createdat, 562 - ], 563 - ) { 564 - Ok(_) => (), 565 - Err(e) => println!("error: {}", e), 566 - } 567 - } 601 + for (i, artist_track) in artist_tracks.clone().into_iter().enumerate() { 602 + println!( 603 + "artist_track {} - {} - {}", 604 + i, 605 + artist_track.artist_id.bright_green(), 606 + artist_track.track_id 607 + ); 608 + match conn.execute( 609 + "INSERT INTO artist_tracks (id, artist_id, track_id, created_at) VALUES (?, ?, ?, ?)", 610 + params![ 611 + artist_track.xata_id, 612 + artist_track.artist_id, 613 + artist_track.track_id, 614 + artist_track.xata_createdat, 615 + ], 616 + ) { 617 + Ok(_) => (), 618 + Err(e) => println!("error: {}", e), 619 + } 620 + } 568 621 569 - println!("artist_tracks: {:?}", artist_tracks.len()); 570 - Ok(()) 622 + println!("artist_tracks: {:?}", artist_tracks.len()); 623 + Ok(()) 571 624 } 572 625 573 - 574 - pub async fn load_artist_albums(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 626 + pub async fn load_artist_albums( 627 + conn: Arc<Mutex<Connection>>, 628 + pool: &Pool<Postgres>, 629 + ) -> Result<(), Error> { 575 630 let conn = conn.lock().unwrap(); 576 - let artist_albums: Vec<xata::artist_album::ArtistAlbum> = sqlx::query_as(r#" 631 + let artist_albums: Vec<xata::artist_album::ArtistAlbum> = sqlx::query_as( 632 + r#" 577 633 SELECT * FROM artist_albums 578 - "#) 634 + "#, 635 + ) 579 636 .fetch_all(pool) 580 637 .await?; 581 638 582 639 for (i, artist_album) in artist_albums.clone().into_iter().enumerate() { 583 - println!("artist_albums {} - {} - {}", i, artist_album.artist_id.bright_green(), artist_album.album_id); 640 + println!( 641 + "artist_albums {} - {} - {}", 642 + i, 643 + artist_album.artist_id.bright_green(), 644 + artist_album.album_id 645 + ); 584 646 match conn.execute( 585 647 "INSERT INTO artist_albums (id, artist_id, album_id, created_at) VALUES (?, ?, ?, ?)", 586 - params![ 648 + params![ 587 649 artist_album.xata_id, 588 650 artist_album.artist_id, 589 651 artist_album.album_id, 590 652 artist_album.xata_createdat, 591 - ], 653 + ], 592 654 ) { 593 655 Ok(_) => (), 594 656 Err(e) => println!("error: {}", e), ··· 599 661 Ok(()) 600 662 } 601 663 602 - pub async fn load_user_albums(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 603 - let conn = conn.lock().unwrap(); 604 - let user_albums: Vec<xata::user_album::UserAlbum> = sqlx::query_as(r#" 664 + pub async fn load_user_albums( 665 + conn: Arc<Mutex<Connection>>, 666 + pool: &Pool<Postgres>, 667 + ) -> Result<(), Error> { 668 + let conn = conn.lock().unwrap(); 669 + let user_albums: Vec<xata::user_album::UserAlbum> = sqlx::query_as( 670 + r#" 605 671 SELECT * FROM user_albums 606 - "#) 607 - .fetch_all(pool) 608 - .await?; 672 + "#, 673 + ) 674 + .fetch_all(pool) 675 + .await?; 609 676 610 - for (i, user_album) in user_albums.clone().into_iter().enumerate() { 611 - println!("user_album {} - {} - {}", i, user_album.user_id.bright_green(), user_album.album_id); 612 - match conn.execute( 613 - "INSERT INTO user_albums (id, user_id, album_id, created_at) VALUES (?, ?, ?, ?)", 614 - params![ 615 - user_album.xata_id, 616 - user_album.user_id, 617 - user_album.album_id, 618 - user_album.xata_createdat, 619 - ], 620 - ) { 621 - Ok(_) => (), 622 - Err(e) => println!("error: {}", e), 623 - } 624 - } 677 + for (i, user_album) in user_albums.clone().into_iter().enumerate() { 678 + println!( 679 + "user_album {} - {} - {}", 680 + i, 681 + user_album.user_id.bright_green(), 682 + user_album.album_id 683 + ); 684 + match conn.execute( 685 + "INSERT INTO user_albums (id, user_id, album_id, created_at) VALUES (?, ?, ?, ?)", 686 + params![ 687 + user_album.xata_id, 688 + user_album.user_id, 689 + user_album.album_id, 690 + user_album.xata_createdat, 691 + ], 692 + ) { 693 + Ok(_) => (), 694 + Err(e) => println!("error: {}", e), 695 + } 696 + } 625 697 626 - println!("user_albums: {:?}", user_albums.len()); 627 - Ok(()) 698 + println!("user_albums: {:?}", user_albums.len()); 699 + Ok(()) 628 700 } 629 701 630 - pub async fn load_user_artists(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 631 - let conn = conn.lock().unwrap(); 632 - let user_artists: Vec<xata::user_artist::UserArtist> = sqlx::query_as(r#" 702 + pub async fn load_user_artists( 703 + conn: Arc<Mutex<Connection>>, 704 + pool: &Pool<Postgres>, 705 + ) -> Result<(), Error> { 706 + let conn = conn.lock().unwrap(); 707 + let user_artists: Vec<xata::user_artist::UserArtist> = sqlx::query_as( 708 + r#" 633 709 SELECT * FROM user_artists 634 - "#) 635 - .fetch_all(pool) 636 - .await?; 710 + "#, 711 + ) 712 + .fetch_all(pool) 713 + .await?; 637 714 638 - for (i, user_artist) in user_artists.clone().into_iter().enumerate() { 639 - println!("user_artist {} - {} - {}", i, user_artist.user_id.bright_green(), user_artist.artist_id); 640 - match conn.execute( 641 - "INSERT INTO user_artists (id, user_id, artist_id, created_at) VALUES (?, ?, ?, ?)", 642 - params![ 643 - user_artist.xata_id, 644 - user_artist.user_id, 645 - user_artist.artist_id, 646 - user_artist.xata_createdat, 647 - ], 648 - ) { 649 - Ok(_) => (), 650 - Err(e) => println!("error: {}", e), 651 - } 652 - } 715 + for (i, user_artist) in user_artists.clone().into_iter().enumerate() { 716 + println!( 717 + "user_artist {} - {} - {}", 718 + i, 719 + user_artist.user_id.bright_green(), 720 + user_artist.artist_id 721 + ); 722 + match conn.execute( 723 + "INSERT INTO user_artists (id, user_id, artist_id, created_at) VALUES (?, ?, ?, ?)", 724 + params![ 725 + user_artist.xata_id, 726 + user_artist.user_id, 727 + user_artist.artist_id, 728 + user_artist.xata_createdat, 729 + ], 730 + ) { 731 + Ok(_) => (), 732 + Err(e) => println!("error: {}", e), 733 + } 734 + } 653 735 654 - println!("user_artists: {:?}", user_artists.len()); 655 - Ok(()) 736 + println!("user_artists: {:?}", user_artists.len()); 737 + Ok(()) 656 738 } 657 739 658 - pub async fn load_user_tracks(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 659 - let conn = conn.lock().unwrap(); 660 - let user_tracks: Vec<xata::user_track::UserTrack> = sqlx::query_as(r#" 740 + pub async fn load_user_tracks( 741 + conn: Arc<Mutex<Connection>>, 742 + pool: &Pool<Postgres>, 743 + ) -> Result<(), Error> { 744 + let conn = conn.lock().unwrap(); 745 + let user_tracks: Vec<xata::user_track::UserTrack> = sqlx::query_as( 746 + r#" 661 747 SELECT * FROM user_tracks 662 - "#) 663 - .fetch_all(pool) 664 - .await?; 748 + "#, 749 + ) 750 + .fetch_all(pool) 751 + .await?; 665 752 666 - for (i, user_track) in user_tracks.clone().into_iter().enumerate() { 667 - println!("user_track {} - {} - {}", i, user_track.user_id.bright_green(), user_track.track_id); 668 - match conn.execute( 669 - "INSERT INTO user_tracks (id, user_id, track_id, created_at) VALUES (?, ?, ?, ?)", 670 - params![ 671 - user_track.xata_id, 672 - user_track.user_id, 673 - user_track.track_id, 674 - user_track.xata_createdat, 675 - ], 676 - ) { 677 - Ok(_) => (), 678 - Err(e) => println!("error: {}", e), 679 - } 680 - } 753 + for (i, user_track) in user_tracks.clone().into_iter().enumerate() { 754 + println!( 755 + "user_track {} - {} - {}", 756 + i, 757 + user_track.user_id.bright_green(), 758 + user_track.track_id 759 + ); 760 + match conn.execute( 761 + "INSERT INTO user_tracks (id, user_id, track_id, created_at) VALUES (?, ?, ?, ?)", 762 + params![ 763 + user_track.xata_id, 764 + user_track.user_id, 765 + user_track.track_id, 766 + user_track.xata_createdat, 767 + ], 768 + ) { 769 + Ok(_) => (), 770 + Err(e) => println!("error: {}", e), 771 + } 772 + } 681 773 682 - println!("user_tracks: {:?}", user_tracks.len()); 683 - Ok(()) 684 - } 774 + println!("user_tracks: {:?}", user_tracks.len()); 775 + Ok(()) 776 + }
+179 -155
crates/analytics/src/handlers/albums.rs
··· 1 1 use std::sync::{Arc, Mutex}; 2 2 3 3 use actix_web::{web, HttpRequest, HttpResponse}; 4 - use analytics::types::{album::{Album, GetAlbumTracksParams, GetAlbumsParams, GetTopAlbumsParams}, track::Track}; 4 + use analytics::types::{ 5 + album::{Album, GetAlbumTracksParams, GetAlbumsParams, GetTopAlbumsParams}, 6 + track::Track, 7 + }; 8 + use anyhow::Error; 5 9 use duckdb::Connection; 6 - use anyhow::Error; 7 10 use tokio_stream::StreamExt; 8 11 9 12 use crate::read_payload; 10 13 11 - pub async fn get_albums(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 12 - let body = read_payload!(payload); 13 - let params = serde_json::from_slice::<GetAlbumsParams>(&body)?; 14 - let pagination = params.pagination.unwrap_or_default(); 15 - let offset = pagination.skip.unwrap_or(0); 16 - let limit = pagination.take.unwrap_or(20); 17 - let did = params.user_did; 14 + pub async fn get_albums( 15 + payload: &mut web::Payload, 16 + _req: &HttpRequest, 17 + conn: Arc<Mutex<Connection>>, 18 + ) -> Result<HttpResponse, Error> { 19 + let body = read_payload!(payload); 20 + let params = serde_json::from_slice::<GetAlbumsParams>(&body)?; 21 + let pagination = params.pagination.unwrap_or_default(); 22 + let offset = pagination.skip.unwrap_or(0); 23 + let limit = pagination.take.unwrap_or(20); 24 + let did = params.user_did; 18 25 19 - let conn = conn.lock().unwrap(); 20 - let mut stmt = match did { 21 - Some(_) => { 22 - conn.prepare(r#" 26 + let conn = conn.lock().unwrap(); 27 + let mut stmt = match did { 28 + Some(_) => conn.prepare( 29 + r#" 23 30 SELECT a.*, 24 31 COUNT(*) AS play_count, 25 32 COUNT(DISTINCT s.user_id) AS unique_listeners ··· 30 37 WHERE u.did = ? OR u.handle = ? 31 38 GROUP BY a.* 32 39 ORDER BY play_count DESC OFFSET ? LIMIT ?; 33 - "#)? 34 - }, 35 - None => { 36 - conn.prepare("SELECT a.*, 40 + "#, 41 + )?, 42 + None => conn.prepare( 43 + "SELECT a.*, 37 44 COUNT(*) AS play_count, 38 45 COUNT(DISTINCT s.user_id) AS unique_listeners 39 46 FROM albums a 40 47 LEFT JOIN scrobbles s ON s.album_id = a.id 41 48 GROUP BY a.* 42 - ORDER BY play_count DESC OFFSET ? LIMIT ?")? 43 - } 44 - }; 49 + ORDER BY play_count DESC OFFSET ? LIMIT ?", 50 + )?, 51 + }; 45 52 46 - match did { 47 - Some(did) => { 48 - let albums_iter = stmt.query_map([&did, &did, &limit.to_string(), &offset.to_string()], |row| { 49 - Ok(Album { 50 - id: row.get(0)?, 51 - title: row.get(1)?, 52 - artist: row.get(2)?, 53 - release_date: row.get(3)?, 54 - album_art: row.get(4)?, 55 - year: row.get(5)?, 56 - spotify_link: row.get(6)?, 57 - tidal_link: row.get(7)?, 58 - youtube_link: row.get(8)?, 59 - apple_music_link: row.get(9)?, 60 - sha256: row.get(10)?, 61 - uri: row.get(11)?, 62 - artist_uri: row.get(12)?, 63 - play_count: Some(row.get(13)?), 64 - unique_listeners: Some(row.get(14)?), 65 - ..Default::default() 66 - }) 67 - })?; 53 + match did { 54 + Some(did) => { 55 + let albums_iter = stmt.query_map( 56 + [&did, &did, &limit.to_string(), &offset.to_string()], 57 + |row| { 58 + Ok(Album { 59 + id: row.get(0)?, 60 + title: row.get(1)?, 61 + artist: row.get(2)?, 62 + release_date: row.get(3)?, 63 + album_art: row.get(4)?, 64 + year: row.get(5)?, 65 + spotify_link: row.get(6)?, 66 + tidal_link: row.get(7)?, 67 + youtube_link: row.get(8)?, 68 + apple_music_link: row.get(9)?, 69 + sha256: row.get(10)?, 70 + uri: row.get(11)?, 71 + artist_uri: row.get(12)?, 72 + play_count: Some(row.get(13)?), 73 + unique_listeners: Some(row.get(14)?), 74 + ..Default::default() 75 + }) 76 + }, 77 + )?; 68 78 69 - let albums: Result<Vec<_>, _> = albums_iter.collect(); 70 - Ok(HttpResponse::Ok().json(web::Json(albums?))) 71 - }, 72 - None => { 73 - let albums_iter = stmt.query_map([limit, offset], |row| { 74 - Ok(Album { 75 - id: row.get(0)?, 76 - title: row.get(1)?, 77 - artist: row.get(2)?, 78 - release_date: row.get(3)?, 79 - album_art: row.get(4)?, 80 - year: row.get(5)?, 81 - spotify_link: row.get(6)?, 82 - tidal_link: row.get(7)?, 83 - youtube_link: row.get(8)?, 84 - apple_music_link: row.get(9)?, 85 - sha256: row.get(10)?, 86 - uri: row.get(11)?, 87 - artist_uri: row.get(12)?, 88 - play_count: Some(row.get(13)?), 89 - unique_listeners: Some(row.get(14)?), 90 - ..Default::default() 91 - }) 92 - })?; 79 + let albums: Result<Vec<_>, _> = albums_iter.collect(); 80 + Ok(HttpResponse::Ok().json(web::Json(albums?))) 81 + } 82 + None => { 83 + let albums_iter = stmt.query_map([limit, offset], |row| { 84 + Ok(Album { 85 + id: row.get(0)?, 86 + title: row.get(1)?, 87 + artist: row.get(2)?, 88 + release_date: row.get(3)?, 89 + album_art: row.get(4)?, 90 + year: row.get(5)?, 91 + spotify_link: row.get(6)?, 92 + tidal_link: row.get(7)?, 93 + youtube_link: row.get(8)?, 94 + apple_music_link: row.get(9)?, 95 + sha256: row.get(10)?, 96 + uri: row.get(11)?, 97 + artist_uri: row.get(12)?, 98 + play_count: Some(row.get(13)?), 99 + unique_listeners: Some(row.get(14)?), 100 + ..Default::default() 101 + }) 102 + })?; 93 103 94 - let albums: Result<Vec<_>, _> = albums_iter.collect(); 95 - Ok(HttpResponse::Ok().json(web::Json(albums?))) 104 + let albums: Result<Vec<_>, _> = albums_iter.collect(); 105 + Ok(HttpResponse::Ok().json(web::Json(albums?))) 106 + } 96 107 } 97 - } 98 108 } 99 109 100 - 101 - pub async fn get_top_albums(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 102 - let body = read_payload!(payload); 103 - let params = serde_json::from_slice::<GetTopAlbumsParams>(&body)?; 104 - let pagination = params.pagination.unwrap_or_default(); 105 - let offset = pagination.skip.unwrap_or(0); 106 - let limit = pagination.take.unwrap_or(20); 107 - let did = params.user_did; 110 + pub async fn get_top_albums( 111 + payload: &mut web::Payload, 112 + _req: &HttpRequest, 113 + conn: Arc<Mutex<Connection>>, 114 + ) -> Result<HttpResponse, Error> { 115 + let body = read_payload!(payload); 116 + let params = serde_json::from_slice::<GetTopAlbumsParams>(&body)?; 117 + let pagination = params.pagination.unwrap_or_default(); 118 + let offset = pagination.skip.unwrap_or(0); 119 + let limit = pagination.take.unwrap_or(20); 120 + let did = params.user_did; 108 121 109 - let conn = conn.lock().unwrap(); 110 - let mut stmt = match did { 111 - Some(_) => conn.prepare(r#" 122 + let conn = conn.lock().unwrap(); 123 + let mut stmt = match did { 124 + Some(_) => conn.prepare( 125 + r#" 112 126 SELECT 113 127 s.album_id AS id, 114 128 a.title AS title, ··· 136 150 play_count DESC 137 151 OFFSET ? 138 152 LIMIT ?; 139 - "#)?, 140 - None => conn.prepare(r#" 153 + "#, 154 + )?, 155 + None => conn.prepare( 156 + r#" 141 157 SELECT 142 158 s.album_id AS id, 143 159 a.title AS title, ··· 162 178 play_count DESC 163 179 OFFSET ? 164 180 LIMIT ?; 165 - "#)? 166 - }; 181 + "#, 182 + )?, 183 + }; 167 184 168 - match did { 169 - Some(did) => { 170 - let albums = stmt.query_map([&did, &did, &limit.to_string(), &offset.to_string()], |row| { 171 - Ok(Album { 172 - id: row.get(0)?, 173 - title: row.get(1)?, 174 - artist: row.get(2)?, 175 - artist_uri: row.get(3)?, 176 - album_art: row.get(4)?, 177 - release_date: row.get(5)?, 178 - year: row.get(6)?, 179 - uri: row.get(7)?, 180 - sha256: row.get(8)?, 181 - play_count: Some(row.get(9)?), 182 - unique_listeners: Some(row.get(10)?), 183 - ..Default::default() 184 - }) 185 - })?; 186 - let albums: Result<Vec<_>, _> = albums.collect(); 187 - Ok(HttpResponse::Ok().json(web::Json(albums?))) 188 - }, 189 - None => { 190 - let albums = stmt.query_map([limit, offset], |row| { 191 - Ok(Album { 192 - id: row.get(0)?, 193 - title: row.get(1)?, 194 - artist: row.get(2)?, 195 - artist_uri: row.get(3)?, 196 - album_art: row.get(4)?, 197 - release_date: row.get(5)?, 198 - year: row.get(6)?, 199 - uri: row.get(7)?, 200 - sha256: row.get(8)?, 201 - play_count: Some(row.get(9)?), 202 - unique_listeners: Some(row.get(10)?), 203 - ..Default::default() 204 - }) 205 - })?; 206 - let albums: Result<Vec<_>, _> = albums.collect(); 207 - Ok(HttpResponse::Ok().json(web::Json(albums?))) 185 + match did { 186 + Some(did) => { 187 + let albums = stmt.query_map( 188 + [&did, &did, &limit.to_string(), &offset.to_string()], 189 + |row| { 190 + Ok(Album { 191 + id: row.get(0)?, 192 + title: row.get(1)?, 193 + artist: row.get(2)?, 194 + artist_uri: row.get(3)?, 195 + album_art: row.get(4)?, 196 + release_date: row.get(5)?, 197 + year: row.get(6)?, 198 + uri: row.get(7)?, 199 + sha256: row.get(8)?, 200 + play_count: Some(row.get(9)?), 201 + unique_listeners: Some(row.get(10)?), 202 + ..Default::default() 203 + }) 204 + }, 205 + )?; 206 + let albums: Result<Vec<_>, _> = albums.collect(); 207 + Ok(HttpResponse::Ok().json(web::Json(albums?))) 208 + } 209 + None => { 210 + let albums = stmt.query_map([limit, offset], |row| { 211 + Ok(Album { 212 + id: row.get(0)?, 213 + title: row.get(1)?, 214 + artist: row.get(2)?, 215 + artist_uri: row.get(3)?, 216 + album_art: row.get(4)?, 217 + release_date: row.get(5)?, 218 + year: row.get(6)?, 219 + uri: row.get(7)?, 220 + sha256: row.get(8)?, 221 + play_count: Some(row.get(9)?), 222 + unique_listeners: Some(row.get(10)?), 223 + ..Default::default() 224 + }) 225 + })?; 226 + let albums: Result<Vec<_>, _> = albums.collect(); 227 + Ok(HttpResponse::Ok().json(web::Json(albums?))) 228 + } 208 229 } 209 - } 210 230 } 211 231 212 - pub async fn get_album_tracks(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 213 - let body = read_payload!(payload); 214 - let params = serde_json::from_slice::<GetAlbumTracksParams>(&body)?; 215 - let conn = conn.lock().unwrap(); 216 - let mut stmt = conn.prepare(r#" 232 + pub async fn get_album_tracks( 233 + payload: &mut web::Payload, 234 + _req: &HttpRequest, 235 + conn: Arc<Mutex<Connection>>, 236 + ) -> Result<HttpResponse, Error> { 237 + let body = read_payload!(payload); 238 + let params = serde_json::from_slice::<GetAlbumTracksParams>(&body)?; 239 + let conn = conn.lock().unwrap(); 240 + let mut stmt = conn.prepare(r#" 217 241 SELECT 218 242 t.id, 219 243 t.title, ··· 243 267 ORDER BY t.track_number ASC; 244 268 "#)?; 245 269 246 - let tracks = stmt.query_map([&params.album_id, &params.album_id], |row| { 247 - Ok(Track { 248 - id: row.get(0)?, 249 - title: row.get(1)?, 250 - artist: row.get(2)?, 251 - album_artist: row.get(3)?, 252 - album: row.get(4)?, 253 - uri: row.get(5)?, 254 - album_art: row.get(6)?, 255 - duration: row.get(7)?, 256 - disc_number: row.get(8)?, 257 - track_number: row.get(9)?, 258 - artist_uri: row.get(10)?, 259 - album_uri: row.get(11)?, 260 - sha256: row.get(12)?, 261 - copyright_message: row.get(13)?, 262 - label: row.get(14)?, 263 - created_at: row.get(15)?, 264 - play_count: Some(row.get(16)?), 265 - unique_listeners: Some(row.get(17)?), 266 - ..Default::default() 267 - }) 268 - })?; 270 + let tracks = stmt.query_map([&params.album_id, &params.album_id], |row| { 271 + Ok(Track { 272 + id: row.get(0)?, 273 + title: row.get(1)?, 274 + artist: row.get(2)?, 275 + album_artist: row.get(3)?, 276 + album: row.get(4)?, 277 + uri: row.get(5)?, 278 + album_art: row.get(6)?, 279 + duration: row.get(7)?, 280 + disc_number: row.get(8)?, 281 + track_number: row.get(9)?, 282 + artist_uri: row.get(10)?, 283 + album_uri: row.get(11)?, 284 + sha256: row.get(12)?, 285 + copyright_message: row.get(13)?, 286 + label: row.get(14)?, 287 + created_at: row.get(15)?, 288 + play_count: Some(row.get(16)?), 289 + unique_listeners: Some(row.get(17)?), 290 + ..Default::default() 291 + }) 292 + })?; 269 293 270 - let tracks: Result<Vec<_>, _> = tracks.collect(); 271 - Ok(HttpResponse::Ok().json(web::Json(tracks?))) 294 + let tracks: Result<Vec<_>, _> = tracks.collect(); 295 + Ok(HttpResponse::Ok().json(web::Json(tracks?))) 272 296 }
+121 -85
crates/analytics/src/handlers/artists.rs
··· 1 1 use std::sync::{Arc, Mutex}; 2 2 3 3 use actix_web::{web, HttpRequest, HttpResponse}; 4 - use analytics::types::{album::Album, artist::{Artist, GetArtistAlbumsParams, GetArtistTracksParams, GetArtistsParams, GetTopArtistsParams}, track::Track}; 4 + use analytics::types::{ 5 + album::Album, 6 + artist::{ 7 + Artist, GetArtistAlbumsParams, GetArtistTracksParams, GetArtistsParams, GetTopArtistsParams, 8 + }, 9 + track::Track, 10 + }; 11 + use anyhow::Error; 5 12 use duckdb::Connection; 6 - use anyhow::Error; 7 13 use tokio_stream::StreamExt; 8 14 9 15 use crate::read_payload; 10 16 11 - pub async fn get_artists(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 17 + pub async fn get_artists( 18 + payload: &mut web::Payload, 19 + _req: &HttpRequest, 20 + conn: Arc<Mutex<Connection>>, 21 + ) -> Result<HttpResponse, Error> { 12 22 let body = read_payload!(payload); 13 23 let params = serde_json::from_slice::<GetArtistsParams>(&body)?; 14 24 let pagination = params.pagination.unwrap_or_default(); ··· 18 28 19 29 let conn = conn.lock().unwrap(); 20 30 let mut stmt = match did { 21 - Some(_) => { 22 - conn.prepare(r#" 31 + Some(_) => conn.prepare( 32 + r#" 23 33 SELECT a.*, 24 34 COUNT(*) AS play_count, 25 35 COUNT(DISTINCT s.user_id) AS unique_listeners ··· 30 40 WHERE u.did = ? OR u.handle = ? 31 41 GROUP BY a.* 32 42 ORDER BY play_count DESC OFFSET ? LIMIT ?; 33 - "#)? 34 - }, 35 - None => { 36 - conn.prepare("SELECT a.*, 43 + "#, 44 + )?, 45 + None => conn.prepare( 46 + "SELECT a.*, 37 47 COUNT(*) AS play_count, 38 48 COUNT(DISTINCT s.user_id) AS unique_listeners 39 49 FROM artists a 40 50 LEFT JOIN scrobbles s ON s.artist_id = a.id 41 51 GROUP BY a.* 42 - ORDER BY play_count DESC OFFSET ? LIMIT ?")? 43 - } 52 + ORDER BY play_count DESC OFFSET ? LIMIT ?", 53 + )?, 44 54 }; 45 55 46 56 match did { 47 57 Some(did) => { 48 - let artists = stmt.query_map([&did, &did, &limit.to_string(), &offset.to_string()], |row| { 49 - Ok(Artist { 50 - id: row.get(0)?, 51 - name: row.get(1)?, 52 - biography: row.get(2)?, 53 - born: row.get(3)?, 54 - born_in: row.get(4)?, 55 - died: row.get(5)?, 56 - picture: row.get(6)?, 57 - sha256: row.get(7)?, 58 - spotify_link: row.get(8)?, 59 - tidal_link: row.get(9)?, 60 - youtube_link: row.get(10)?, 61 - apple_music_link: row.get(11)?, 62 - uri: row.get(12)?, 63 - play_count: row.get(13)?, 64 - unique_listeners: row.get(14)?, 65 - }) 66 - })?; 58 + let artists = stmt.query_map( 59 + [&did, &did, &limit.to_string(), &offset.to_string()], 60 + |row| { 61 + Ok(Artist { 62 + id: row.get(0)?, 63 + name: row.get(1)?, 64 + biography: row.get(2)?, 65 + born: row.get(3)?, 66 + born_in: row.get(4)?, 67 + died: row.get(5)?, 68 + picture: row.get(6)?, 69 + sha256: row.get(7)?, 70 + spotify_link: row.get(8)?, 71 + tidal_link: row.get(9)?, 72 + youtube_link: row.get(10)?, 73 + apple_music_link: row.get(11)?, 74 + uri: row.get(12)?, 75 + play_count: row.get(13)?, 76 + unique_listeners: row.get(14)?, 77 + }) 78 + }, 79 + )?; 67 80 68 81 let artists: Result<Vec<_>, _> = artists.collect(); 69 82 Ok(HttpResponse::Ok().json(artists?)) 70 - }, 83 + } 71 84 None => { 72 85 let artists = stmt.query_map([limit, offset], |row| { 73 86 Ok(Artist { ··· 95 108 } 96 109 } 97 110 98 - pub async fn get_top_artists(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 111 + pub async fn get_top_artists( 112 + payload: &mut web::Payload, 113 + _req: &HttpRequest, 114 + conn: Arc<Mutex<Connection>>, 115 + ) -> Result<HttpResponse, Error> { 99 116 let body = read_payload!(payload); 100 117 let params = serde_json::from_slice::<GetTopArtistsParams>(&body)?; 101 118 let pagination = params.pagination.unwrap_or_default(); ··· 105 122 106 123 let conn = conn.lock().unwrap(); 107 124 let mut stmt = match did { 108 - Some(_) => { 109 - conn.prepare(r#" 125 + Some(_) => conn.prepare( 126 + r#" 110 127 SELECT 111 128 s.artist_id AS id, 112 129 ar.name AS artist_name, ··· 129 146 play_count DESC 130 147 OFFSET ? 131 148 LIMIT ?; 132 - "#)? 133 - }, 134 - None => { 135 - conn.prepare(r#" 149 + "#, 150 + )?, 151 + None => conn.prepare( 152 + r#" 136 153 SELECT 137 154 s.artist_id AS id, 138 155 ar.name AS artist_name, ··· 153 170 play_count DESC 154 171 OFFSET ? 155 172 LIMIT ?; 156 - "#)? 157 - } 173 + "#, 174 + )?, 158 175 }; 159 176 160 177 match did { 161 178 Some(did) => { 162 - let artists = stmt.query_map([&did, &did, &limit.to_string(), &offset.to_string()], |row| { 163 - Ok(Artist { 164 - id: row.get(0)?, 165 - name: row.get(1)?, 166 - biography: None, 167 - born: None, 168 - born_in: None, 169 - died: None, 170 - picture: row.get(2)?, 171 - sha256: row.get(3)?, 172 - spotify_link: None, 173 - tidal_link: None, 174 - youtube_link: None, 175 - apple_music_link: None, 176 - uri: row.get(4)?, 177 - play_count: Some(row.get(5)?), 178 - unique_listeners: Some(row.get(6)?), 179 - }) 180 - })?; 179 + let artists = stmt.query_map( 180 + [&did, &did, &limit.to_string(), &offset.to_string()], 181 + |row| { 182 + Ok(Artist { 183 + id: row.get(0)?, 184 + name: row.get(1)?, 185 + biography: None, 186 + born: None, 187 + born_in: None, 188 + died: None, 189 + picture: row.get(2)?, 190 + sha256: row.get(3)?, 191 + spotify_link: None, 192 + tidal_link: None, 193 + youtube_link: None, 194 + apple_music_link: None, 195 + uri: row.get(4)?, 196 + play_count: Some(row.get(5)?), 197 + unique_listeners: Some(row.get(6)?), 198 + }) 199 + }, 200 + )?; 181 201 182 202 let artists: Result<Vec<_>, _> = artists.collect(); 183 203 Ok(HttpResponse::Ok().json(artists?)) 184 - }, 204 + } 185 205 None => { 186 206 let artists = stmt.query_map([limit, offset], |row| { 187 207 Ok(Artist { ··· 209 229 } 210 230 } 211 231 212 - pub async fn get_artist_tracks(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 232 + pub async fn get_artist_tracks( 233 + payload: &mut web::Payload, 234 + _req: &HttpRequest, 235 + conn: Arc<Mutex<Connection>>, 236 + ) -> Result<HttpResponse, Error> { 213 237 let body = read_payload!(payload); 214 238 let params = serde_json::from_slice::<GetArtistTracksParams>(&body)?; 215 239 let pagination = params.pagination.unwrap_or_default(); ··· 249 273 LIMIT ?; 250 274 "#)?; 251 275 252 - let tracks = stmt.query_map([&params.artist_id, &params.artist_id, &limit.to_string(), &offset.to_string()], |row| { 253 - Ok(Track { 254 - id: row.get(0)?, 255 - title: row.get(1)?, 256 - artist: row.get(2)?, 257 - album_artist: row.get(3)?, 258 - album: row.get(4)?, 259 - uri: row.get(5)?, 260 - album_art: row.get(6)?, 261 - duration: row.get(7)?, 262 - disc_number: row.get(8)?, 263 - track_number: row.get(9)?, 264 - artist_uri: row.get(10)?, 265 - album_uri: row.get(11)?, 266 - sha256: row.get(12)?, 267 - copyright_message: row.get(13)?, 268 - label: row.get(14)?, 269 - created_at: row.get(15)?, 270 - play_count: Some(row.get(16)?), 271 - unique_listeners: Some(row.get(17)?), 272 - ..Default::default() 273 - }) 274 - })?; 276 + let tracks = stmt.query_map( 277 + [ 278 + &params.artist_id, 279 + &params.artist_id, 280 + &limit.to_string(), 281 + &offset.to_string(), 282 + ], 283 + |row| { 284 + Ok(Track { 285 + id: row.get(0)?, 286 + title: row.get(1)?, 287 + artist: row.get(2)?, 288 + album_artist: row.get(3)?, 289 + album: row.get(4)?, 290 + uri: row.get(5)?, 291 + album_art: row.get(6)?, 292 + duration: row.get(7)?, 293 + disc_number: row.get(8)?, 294 + track_number: row.get(9)?, 295 + artist_uri: row.get(10)?, 296 + album_uri: row.get(11)?, 297 + sha256: row.get(12)?, 298 + copyright_message: row.get(13)?, 299 + label: row.get(14)?, 300 + created_at: row.get(15)?, 301 + play_count: Some(row.get(16)?), 302 + unique_listeners: Some(row.get(17)?), 303 + ..Default::default() 304 + }) 305 + }, 306 + )?; 275 307 276 308 let tracks: Result<Vec<_>, _> = tracks.collect(); 277 309 Ok(HttpResponse::Ok().json(tracks?)) 278 310 } 279 311 280 - pub async fn get_artist_albums(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 312 + pub async fn get_artist_albums( 313 + payload: &mut web::Payload, 314 + _req: &HttpRequest, 315 + conn: Arc<Mutex<Connection>>, 316 + ) -> Result<HttpResponse, Error> { 281 317 let body = read_payload!(payload); 282 318 let params = serde_json::from_slice::<GetArtistAlbumsParams>(&body)?; 283 319 let conn = conn.lock().unwrap();
+44 -37
crates/analytics/src/handlers/mod.rs
··· 2 2 3 3 use actix_web::{web, HttpRequest, HttpResponse}; 4 4 use albums::{get_album_tracks, get_albums, get_top_albums}; 5 + use anyhow::Error; 5 6 use artists::{get_artist_albums, get_artist_tracks, get_artists, get_top_artists}; 6 7 use duckdb::Connection; 7 8 use scrobbles::{get_distinct_scrobbles, get_scrobbles}; 8 - use stats::{get_album_scrobbles, get_artist_scrobbles, get_scrobbles_per_day, get_scrobbles_per_month, get_scrobbles_per_year, get_stats, get_track_scrobbles}; 9 + use stats::{ 10 + get_album_scrobbles, get_artist_scrobbles, get_scrobbles_per_day, get_scrobbles_per_month, 11 + get_scrobbles_per_year, get_stats, get_track_scrobbles, 12 + }; 9 13 use tracks::{get_loved_tracks, get_top_tracks, get_tracks}; 10 - use anyhow::Error; 11 14 12 15 pub mod albums; 13 16 pub mod artists; 14 17 pub mod scrobbles; 15 - pub mod tracks; 16 18 pub mod stats; 17 - 19 + pub mod tracks; 18 20 19 21 #[macro_export] 20 22 macro_rules! read_payload { 21 - ($payload:expr) => {{ 22 - let mut body = Vec::new(); 23 - while let Some(chunk) = $payload.next().await { 24 - match chunk { 25 - Ok(bytes) => body.extend_from_slice(&bytes), 26 - Err(err) => return Err(err.into()), 27 - } 28 - } 29 - body 30 - }}; 23 + ($payload:expr) => {{ 24 + let mut body = Vec::new(); 25 + while let Some(chunk) = $payload.next().await { 26 + match chunk { 27 + Ok(bytes) => body.extend_from_slice(&bytes), 28 + Err(err) => return Err(err.into()), 29 + } 30 + } 31 + body 32 + }}; 31 33 } 32 34 33 - pub async fn handle(method: &str, payload: &mut web::Payload, req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 34 - match method { 35 - "library.getAlbums" => get_albums(payload, req, conn.clone()).await, 36 - "library.getArtists" => get_artists(payload, req, conn.clone()).await, 37 - "library.getTracks" => get_tracks(payload, req, conn.clone()).await, 38 - "library.getScrobbles" => get_scrobbles(payload, req, conn.clone()).await, 39 - "library.getDistinctScrobbles" => get_distinct_scrobbles(payload, req, conn.clone()).await, 40 - "library.getLovedTracks" => get_loved_tracks(payload, req, conn.clone()).await, 41 - "library.getStats" => get_stats(payload, req, conn.clone()).await, 42 - "library.getTopAlbums" => get_top_albums(payload, req, conn.clone()).await, 43 - "library.getTopArtists" => get_top_artists(payload, req, conn.clone()).await, 44 - "library.getTopTracks" => get_top_tracks(payload, req, conn.clone()).await, 45 - "library.getScrobblesPerDay" => get_scrobbles_per_day(payload, req, conn.clone()).await, 46 - "library.getScrobblesPerMonth" => get_scrobbles_per_month(payload, req, conn.clone()).await, 47 - "library.getScrobblesPerYear" => get_scrobbles_per_year(payload, req, conn.clone()).await, 48 - "library.getAlbumScrobbles" => get_album_scrobbles(payload, req, conn.clone()).await, 49 - "library.getArtistScrobbles" => get_artist_scrobbles(payload, req, conn.clone()).await, 50 - "library.getTrackScrobbles" => get_track_scrobbles(payload, req, conn.clone()).await, 51 - "library.getAlbumTracks" => get_album_tracks(payload, req, conn.clone()).await, 52 - "library.getArtistAlbums" => get_artist_albums(payload, req, conn.clone()).await, 53 - "library.getArtistTracks" => get_artist_tracks(payload, req, conn.clone()).await, 54 - _ => return Err(anyhow::anyhow!("Method not found")), 55 - } 35 + pub async fn handle( 36 + method: &str, 37 + payload: &mut web::Payload, 38 + req: &HttpRequest, 39 + conn: Arc<Mutex<Connection>>, 40 + ) -> Result<HttpResponse, Error> { 41 + match method { 42 + "library.getAlbums" => get_albums(payload, req, conn.clone()).await, 43 + "library.getArtists" => get_artists(payload, req, conn.clone()).await, 44 + "library.getTracks" => get_tracks(payload, req, conn.clone()).await, 45 + "library.getScrobbles" => get_scrobbles(payload, req, conn.clone()).await, 46 + "library.getDistinctScrobbles" => get_distinct_scrobbles(payload, req, conn.clone()).await, 47 + "library.getLovedTracks" => get_loved_tracks(payload, req, conn.clone()).await, 48 + "library.getStats" => get_stats(payload, req, conn.clone()).await, 49 + "library.getTopAlbums" => get_top_albums(payload, req, conn.clone()).await, 50 + "library.getTopArtists" => get_top_artists(payload, req, conn.clone()).await, 51 + "library.getTopTracks" => get_top_tracks(payload, req, conn.clone()).await, 52 + "library.getScrobblesPerDay" => get_scrobbles_per_day(payload, req, conn.clone()).await, 53 + "library.getScrobblesPerMonth" => get_scrobbles_per_month(payload, req, conn.clone()).await, 54 + "library.getScrobblesPerYear" => get_scrobbles_per_year(payload, req, conn.clone()).await, 55 + "library.getAlbumScrobbles" => get_album_scrobbles(payload, req, conn.clone()).await, 56 + "library.getArtistScrobbles" => get_artist_scrobbles(payload, req, conn.clone()).await, 57 + "library.getTrackScrobbles" => get_track_scrobbles(payload, req, conn.clone()).await, 58 + "library.getAlbumTracks" => get_album_tracks(payload, req, conn.clone()).await, 59 + "library.getArtistAlbums" => get_artist_albums(payload, req, conn.clone()).await, 60 + "library.getArtistTracks" => get_artist_tracks(payload, req, conn.clone()).await, 61 + _ => return Err(anyhow::anyhow!("Method not found")), 62 + } 56 63 }
+38 -26
crates/analytics/src/handlers/scrobbles.rs
··· 2 2 3 3 use actix_web::{web, HttpRequest, HttpResponse}; 4 4 use analytics::types::scrobble::{GetScrobblesParams, ScrobbleTrack}; 5 - use duckdb::Connection; 6 5 use anyhow::Error; 6 + use duckdb::Connection; 7 7 use tokio_stream::StreamExt; 8 8 9 9 use crate::read_payload; 10 10 11 - pub async fn get_scrobbles(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 11 + pub async fn get_scrobbles( 12 + payload: &mut web::Payload, 13 + _req: &HttpRequest, 14 + conn: Arc<Mutex<Connection>>, 15 + ) -> Result<HttpResponse, Error> { 12 16 let body = read_payload!(payload); 13 17 let params = serde_json::from_slice::<GetScrobblesParams>(&body)?; 14 18 let pagination = params.pagination.unwrap_or_default(); ··· 74 78 }; 75 79 match did { 76 80 Some(did) => { 77 - let scrobbles = stmt.query_map([&did, &did, &limit.to_string(), &offset.to_string()], |row| { 78 - Ok(ScrobbleTrack { 79 - id: row.get(0)?, 80 - track_id: row.get(1)?, 81 - title: row.get(2)?, 82 - artist: row.get(3)?, 83 - album_artist: row.get(4)?, 84 - album: row.get(5)?, 85 - album_art: row.get(6)?, 86 - handle: row.get(7)?, 87 - did: row.get(8)?, 88 - avatar: None, 89 - uri: row.get(9)?, 90 - track_uri: row.get(10)?, 91 - artist_uri: row.get(11)?, 92 - album_uri: row.get(12)?, 93 - created_at: row.get(13)?, 94 - }) 95 - })?; 81 + let scrobbles = stmt.query_map( 82 + [&did, &did, &limit.to_string(), &offset.to_string()], 83 + |row| { 84 + Ok(ScrobbleTrack { 85 + id: row.get(0)?, 86 + track_id: row.get(1)?, 87 + title: row.get(2)?, 88 + artist: row.get(3)?, 89 + album_artist: row.get(4)?, 90 + album: row.get(5)?, 91 + album_art: row.get(6)?, 92 + handle: row.get(7)?, 93 + did: row.get(8)?, 94 + avatar: None, 95 + uri: row.get(9)?, 96 + track_uri: row.get(10)?, 97 + artist_uri: row.get(11)?, 98 + album_uri: row.get(12)?, 99 + created_at: row.get(13)?, 100 + }) 101 + }, 102 + )?; 96 103 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 97 104 Ok(HttpResponse::Ok().json(scrobbles?)) 98 - }, 105 + } 99 106 None => { 100 107 let scrobbles = stmt.query_map([limit, offset], |row| { 101 108 Ok(ScrobbleTrack { ··· 122 129 } 123 130 } 124 131 125 - pub async fn get_distinct_scrobbles(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 132 + pub async fn get_distinct_scrobbles( 133 + payload: &mut web::Payload, 134 + _req: &HttpRequest, 135 + conn: Arc<Mutex<Connection>>, 136 + ) -> Result<HttpResponse, Error> { 126 137 let body = read_payload!(payload); 127 138 let params = serde_json::from_slice::<GetScrobblesParams>(&body)?; 128 139 let pagination = params.pagination.unwrap_or_default(); ··· 130 141 let limit = pagination.take.unwrap_or(10); 131 142 132 143 let conn = conn.lock().unwrap(); 133 - let mut stmt = conn.prepare(r#" 144 + let mut stmt = conn.prepare( 145 + r#" 134 146 WITH ranked_scrobbles AS ( 135 147 SELECT 136 148 s.id, ··· 177 189 ORDER BY created_at DESC 178 190 OFFSET ? 179 191 LIMIT ?; 180 - "#)?; 192 + "#, 193 + )?; 181 194 182 195 let scrobbles = stmt.query_map([limit, offset], |row| { 183 196 Ok(ScrobbleTrack { ··· 201 214 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 202 215 Ok(HttpResponse::Ok().json(scrobbles?)) 203 216 } 204 -
+137 -69
crates/analytics/src/handlers/stats.rs
··· 1 1 use std::sync::{Arc, Mutex}; 2 2 3 + use crate::read_payload; 3 4 use actix_web::{web, HttpRequest, HttpResponse}; 4 - use analytics::types::{scrobble::{ScrobblesPerDay, ScrobblesPerMonth, ScrobblesPerYear}, stats::{GetAlbumScrobblesParams, GetArtistScrobblesParams, GetScrobblesPerDayParams, GetScrobblesPerMonthParams, GetScrobblesPerYearParams, GetStatsParams, GetTrackScrobblesParams}}; 5 - use duckdb::Connection; 5 + use analytics::types::{ 6 + scrobble::{ScrobblesPerDay, ScrobblesPerMonth, ScrobblesPerYear}, 7 + stats::{ 8 + GetAlbumScrobblesParams, GetArtistScrobblesParams, GetScrobblesPerDayParams, 9 + GetScrobblesPerMonthParams, GetScrobblesPerYearParams, GetStatsParams, 10 + GetTrackScrobblesParams, 11 + }, 12 + }; 6 13 use anyhow::Error; 14 + use duckdb::Connection; 7 15 use serde_json::json; 8 16 use tokio_stream::StreamExt; 9 - use crate::read_payload; 10 17 11 - pub async fn get_stats(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 18 + pub async fn get_stats( 19 + payload: &mut web::Payload, 20 + _req: &HttpRequest, 21 + conn: Arc<Mutex<Connection>>, 22 + ) -> Result<HttpResponse, Error> { 12 23 let body = read_payload!(payload); 13 24 14 25 let params = serde_json::from_slice::<GetStatsParams>(&body)?; ··· 17 28 let mut stmt = conn.prepare("SELECT COUNT(*) FROM scrobbles s LEFT JOIN users u ON s.user_id = u.id WHERE u.did = ? OR u.handle = ?")?; 18 29 let scrobbles: i64 = stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 19 30 20 - let mut stmt = conn.prepare(r#" 31 + let mut stmt = conn.prepare( 32 + r#" 21 33 SELECT COUNT(*) FROM ( 22 34 SELECT 23 35 s.artist_id AS id, ··· 38 50 GROUP BY 39 51 s.artist_id, ar.name, ar.uri, ar.picture, ar.sha256 40 52 ) 41 - "#)?; 53 + "#, 54 + )?; 42 55 let artists: i64 = stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 43 56 44 57 let mut stmt = conn.prepare("SELECT COUNT(*) FROM loved_tracks LEFT JOIN users u ON loved_tracks.user_id = u.id WHERE u.did = ? OR u.handle = ?")?; 45 - let loved_tracks: i64 = stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 58 + let loved_tracks: i64 = 59 + stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 46 60 47 61 let mut stmt = conn.prepare(r#"SELECT COUNT(*) FROM ( 48 62 SELECT ··· 83 97 }))) 84 98 } 85 99 86 - pub async fn get_scrobbles_per_day(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 100 + pub async fn get_scrobbles_per_day( 101 + payload: &mut web::Payload, 102 + _req: &HttpRequest, 103 + conn: Arc<Mutex<Connection>>, 104 + ) -> Result<HttpResponse, Error> { 87 105 let body = read_payload!(payload); 88 106 let params = serde_json::from_slice::<GetScrobblesPerDayParams>(&body)?; 89 - let start = params.start.unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 90 - let end = params.end.unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 107 + let start = params 108 + .start 109 + .unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 110 + let end = params 111 + .end 112 + .unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 91 113 let did = params.user_did; 92 114 93 115 let conn = conn.lock().unwrap(); 94 116 match did { 95 117 Some(did) => { 96 - let mut stmt = conn.prepare(r#" 118 + let mut stmt = conn.prepare( 119 + r#" 97 120 SELECT 98 121 date_trunc('day', created_at) AS date, 99 122 COUNT(track_id) AS count ··· 107 130 date_trunc('day', created_at) 108 131 ORDER BY 109 132 date; 110 - "#)?; 133 + "#, 134 + )?; 111 135 let scrobbles = stmt.query_map([&did, &did, &start, &end], |row| { 112 136 Ok(ScrobblesPerDay { 113 137 date: row.get(0)?, ··· 116 140 })?; 117 141 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 118 142 Ok(HttpResponse::Ok().json(scrobbles?)) 119 - }, 143 + } 120 144 None => { 121 - let mut stmt = conn.prepare(r#" 145 + let mut stmt = conn.prepare( 146 + r#" 122 147 SELECT 123 148 date_trunc('day', created_at) AS date, 124 149 COUNT(track_id) AS count ··· 130 155 date_trunc('day', created_at) 131 156 ORDER BY 132 157 date; 133 - "#)?; 158 + "#, 159 + )?; 134 160 let scrobbles = stmt.query_map([start, end], |row| { 135 161 Ok(ScrobblesPerDay { 136 162 date: row.get(0)?, ··· 143 169 } 144 170 } 145 171 146 - pub async fn get_scrobbles_per_month(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 172 + pub async fn get_scrobbles_per_month( 173 + payload: &mut web::Payload, 174 + _req: &HttpRequest, 175 + conn: Arc<Mutex<Connection>>, 176 + ) -> Result<HttpResponse, Error> { 147 177 let body = read_payload!(payload); 148 178 let params = serde_json::from_slice::<GetScrobblesPerMonthParams>(&body)?; 149 - let start = params.start.unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 150 - let end = params.end.unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 179 + let start = params 180 + .start 181 + .unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 182 + let end = params 183 + .end 184 + .unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 151 185 let did = params.user_did; 152 186 153 187 let conn = conn.lock().unwrap(); 154 188 match did { 155 189 Some(did) => { 156 - let mut stmt = conn.prepare(r#" 190 + let mut stmt = conn.prepare( 191 + r#" 157 192 SELECT 158 193 EXTRACT(YEAR FROM created_at) || '-' || 159 194 LPAD(EXTRACT(MONTH FROM created_at)::VARCHAR, 2, '0') AS year_month, ··· 169 204 EXTRACT(MONTH FROM created_at) 170 205 ORDER BY 171 206 year_month; 172 - "#)?; 207 + "#, 208 + )?; 173 209 let scrobbles = stmt.query_map([&did, &did, &start, &end], |row| { 174 210 Ok(ScrobblesPerMonth { 175 211 year_month: row.get(0)?, ··· 178 214 })?; 179 215 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 180 216 Ok(HttpResponse::Ok().json(scrobbles?)) 181 - }, 217 + } 182 218 None => { 183 - let mut stmt = conn.prepare(r#" 219 + let mut stmt = conn.prepare( 220 + r#" 184 221 SELECT 185 222 EXTRACT(YEAR FROM created_at) || '-' || 186 223 LPAD(EXTRACT(MONTH FROM created_at)::VARCHAR, 2, '0') AS year_month, ··· 194 231 EXTRACT(MONTH FROM created_at) 195 232 ORDER BY 196 233 year_month; 197 - "#)?; 234 + "#, 235 + )?; 198 236 let scrobbles = stmt.query_map([start, end], |row| { 199 237 Ok(ScrobblesPerMonth { 200 238 year_month: row.get(0)?, ··· 207 245 } 208 246 } 209 247 210 - pub async fn get_scrobbles_per_year(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 248 + pub async fn get_scrobbles_per_year( 249 + payload: &mut web::Payload, 250 + _req: &HttpRequest, 251 + conn: Arc<Mutex<Connection>>, 252 + ) -> Result<HttpResponse, Error> { 211 253 let body = read_payload!(payload); 212 254 let params = serde_json::from_slice::<GetScrobblesPerYearParams>(&body)?; 213 - let start = params.start.unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 214 - let end = params.end.unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 255 + let start = params 256 + .start 257 + .unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 258 + let end = params 259 + .end 260 + .unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 215 261 let did = params.user_did; 216 262 217 263 let conn = conn.lock().unwrap(); 218 264 match did { 219 265 Some(did) => { 220 - let mut stmt = conn.prepare(r#" 266 + let mut stmt = conn.prepare( 267 + r#" 221 268 SELECT 222 269 EXTRACT(YEAR FROM created_at) AS year, 223 270 COUNT(*) AS count ··· 231 278 EXTRACT(YEAR FROM created_at) 232 279 ORDER BY 233 280 year; 234 - "#)?; 281 + "#, 282 + )?; 235 283 let scrobbles = stmt.query_map([&did, &did, &start, &end], |row| { 236 284 Ok(ScrobblesPerYear { 237 285 year: row.get(0)?, ··· 240 288 })?; 241 289 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 242 290 Ok(HttpResponse::Ok().json(scrobbles?)) 243 - }, 291 + } 244 292 None => { 245 - let mut stmt = conn.prepare(r#" 293 + let mut stmt = conn.prepare( 294 + r#" 246 295 SELECT 247 296 EXTRACT(YEAR FROM created_at) AS year, 248 297 COUNT(*) AS count ··· 254 303 EXTRACT(YEAR FROM created_at) 255 304 ORDER BY 256 305 year; 257 - "#)?; 306 + "#, 307 + )?; 258 308 let scrobbles = stmt.query_map([start, end], |row| { 259 309 Ok(ScrobblesPerYear { 260 310 year: row.get(0)?, ··· 267 317 } 268 318 } 269 319 270 - pub async fn get_album_scrobbles(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 320 + pub async fn get_album_scrobbles( 321 + payload: &mut web::Payload, 322 + _req: &HttpRequest, 323 + conn: Arc<Mutex<Connection>>, 324 + ) -> Result<HttpResponse, Error> { 271 325 let body = read_payload!(payload); 272 326 let params = serde_json::from_slice::<GetAlbumScrobblesParams>(&body)?; 273 - let start = params.start.unwrap_or(GetAlbumScrobblesParams::default().start.unwrap()); 274 - let end = params.end.unwrap_or(GetAlbumScrobblesParams::default().end.unwrap()); 327 + let start = params 328 + .start 329 + .unwrap_or(GetAlbumScrobblesParams::default().start.unwrap()); 330 + let end = params 331 + .end 332 + .unwrap_or(GetAlbumScrobblesParams::default().end.unwrap()); 275 333 let conn = conn.lock().unwrap(); 276 - let mut stmt = conn.prepare(r#" 334 + let mut stmt = conn.prepare( 335 + r#" 277 336 SELECT 278 337 date_trunc('day', s.created_at) AS date, 279 338 COUNT(s.album_id) AS count ··· 287 346 date_trunc('day', s.created_at) 288 347 ORDER BY 289 348 date; 290 - "#)?; 291 - let scrobbles = stmt.query_map([ 292 - &params.album_id, 293 - &params.album_id, 294 - &start, 295 - &end 296 - ], |row| { 349 + "#, 350 + )?; 351 + let scrobbles = stmt.query_map([&params.album_id, &params.album_id, &start, &end], |row| { 297 352 Ok(ScrobblesPerDay { 298 353 date: row.get(0)?, 299 354 count: row.get(1)?, ··· 303 358 Ok(HttpResponse::Ok().json(scrobbles?)) 304 359 } 305 360 306 - pub async fn get_artist_scrobbles(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 361 + pub async fn get_artist_scrobbles( 362 + payload: &mut web::Payload, 363 + _req: &HttpRequest, 364 + conn: Arc<Mutex<Connection>>, 365 + ) -> Result<HttpResponse, Error> { 307 366 let body = read_payload!(payload); 308 367 let params = serde_json::from_slice::<GetArtistScrobblesParams>(&body)?; 309 - let start = params.start.unwrap_or(GetArtistScrobblesParams::default().start.unwrap()); 310 - let end = params.end.unwrap_or(GetArtistScrobblesParams::default().end.unwrap()); 368 + let start = params 369 + .start 370 + .unwrap_or(GetArtistScrobblesParams::default().start.unwrap()); 371 + let end = params 372 + .end 373 + .unwrap_or(GetArtistScrobblesParams::default().end.unwrap()); 311 374 let conn = conn.lock().unwrap(); 312 375 313 - let mut stmt = conn.prepare(r#" 376 + let mut stmt = conn.prepare( 377 + r#" 314 378 SELECT 315 379 date_trunc('day', s.created_at) AS date, 316 380 COUNT(s.artist_id) AS count ··· 324 388 date_trunc('day', s.created_at) 325 389 ORDER BY 326 390 date; 327 - "#)?; 391 + "#, 392 + )?; 328 393 329 - let scrobbles = stmt.query_map([ 330 - &params.artist_id, 331 - &params.artist_id, 332 - &start, 333 - &end 334 - ], |row| { 335 - Ok(ScrobblesPerDay { 336 - date: row.get(0)?, 337 - count: row.get(1)?, 338 - }) 339 - })?; 394 + let scrobbles = stmt.query_map( 395 + [&params.artist_id, &params.artist_id, &start, &end], 396 + |row| { 397 + Ok(ScrobblesPerDay { 398 + date: row.get(0)?, 399 + count: row.get(1)?, 400 + }) 401 + }, 402 + )?; 340 403 341 404 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 342 405 Ok(HttpResponse::Ok().json(scrobbles?)) 343 406 } 344 407 345 - pub async fn get_track_scrobbles(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 408 + pub async fn get_track_scrobbles( 409 + payload: &mut web::Payload, 410 + _req: &HttpRequest, 411 + conn: Arc<Mutex<Connection>>, 412 + ) -> Result<HttpResponse, Error> { 346 413 let body = read_payload!(payload); 347 414 let params = serde_json::from_slice::<GetTrackScrobblesParams>(&body)?; 348 - let start = params.start.unwrap_or(GetTrackScrobblesParams::default().start.unwrap()); 349 - let end = params.end.unwrap_or(GetTrackScrobblesParams::default().end.unwrap()); 415 + let start = params 416 + .start 417 + .unwrap_or(GetTrackScrobblesParams::default().start.unwrap()); 418 + let end = params 419 + .end 420 + .unwrap_or(GetTrackScrobblesParams::default().end.unwrap()); 350 421 let conn = conn.lock().unwrap(); 351 422 352 - let mut stmt = conn.prepare(r#" 423 + let mut stmt = conn.prepare( 424 + r#" 353 425 SELECT 354 426 date_trunc('day', s.created_at) AS date, 355 427 COUNT(s.track_id) AS count ··· 363 435 date_trunc('day', s.created_at) 364 436 ORDER BY 365 437 date; 366 - "#)?; 438 + "#, 439 + )?; 367 440 368 - let scrobbles = stmt.query_map([ 369 - &params.track_id, 370 - &params.track_id, 371 - &start, 372 - &end 373 - ], |row| { 441 + let scrobbles = stmt.query_map([&params.track_id, &params.track_id, &start, &end], |row| { 374 442 Ok(ScrobblesPerDay { 375 443 date: row.get(0)?, 376 444 count: row.get(1)?,
+110 -88
crates/analytics/src/handlers/tracks.rs
··· 2 2 3 3 use actix_web::{web, HttpRequest, HttpResponse}; 4 4 use analytics::types::track::{GetLovedTracksParams, GetTopTracksParams, GetTracksParams, Track}; 5 - use duckdb::Connection; 6 5 use anyhow::Error; 6 + use duckdb::Connection; 7 7 use tokio_stream::StreamExt; 8 8 9 9 use crate::read_payload; 10 10 11 - pub async fn get_tracks(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>> 11 + pub async fn get_tracks( 12 + payload: &mut web::Payload, 13 + _req: &HttpRequest, 14 + conn: Arc<Mutex<Connection>>, 12 15 ) -> Result<HttpResponse, Error> { 13 16 let body = read_payload!(payload); 14 17 let params = serde_json::from_slice::<GetTracksParams>(&body)?; ··· 57 60 OFFSET ? 58 61 LIMIT ?; 59 62 "#)?; 60 - let tracks = stmt.query_map([&did, &did, &limit.to_string(), &offset.to_string()], |row| { 61 - Ok(Track { 62 - id: row.get(0)?, 63 - title: row.get(1)?, 64 - artist: row.get(2)?, 65 - album_artist: row.get(3)?, 66 - album_art: row.get(4)?, 67 - album: row.get(5)?, 68 - track_number: row.get(6)?, 69 - duration: row.get(7)?, 70 - mb_id: row.get(8)?, 71 - youtube_link: row.get(9)?, 72 - spotify_link: row.get(10)?, 73 - tidal_link: row.get(11)?, 74 - apple_music_link: row.get(12)?, 75 - sha256: row.get(13)?, 76 - composer: row.get(14)?, 77 - genre: row.get(15)?, 78 - disc_number: row.get(16)?, 79 - label: row.get(17)?, 80 - uri: row.get(18)?, 81 - copyright_message: row.get(19)?, 82 - artist_uri: row.get(20)?, 83 - album_uri: row.get(21)?, 84 - created_at: row.get(22)?, 85 - play_count: row.get(23)?, 86 - unique_listeners: row.get(24)?, 87 - ..Default::default() 88 - }) 89 - })?; 63 + let tracks = stmt.query_map( 64 + [&did, &did, &limit.to_string(), &offset.to_string()], 65 + |row| { 66 + Ok(Track { 67 + id: row.get(0)?, 68 + title: row.get(1)?, 69 + artist: row.get(2)?, 70 + album_artist: row.get(3)?, 71 + album_art: row.get(4)?, 72 + album: row.get(5)?, 73 + track_number: row.get(6)?, 74 + duration: row.get(7)?, 75 + mb_id: row.get(8)?, 76 + youtube_link: row.get(9)?, 77 + spotify_link: row.get(10)?, 78 + tidal_link: row.get(11)?, 79 + apple_music_link: row.get(12)?, 80 + sha256: row.get(13)?, 81 + composer: row.get(14)?, 82 + genre: row.get(15)?, 83 + disc_number: row.get(16)?, 84 + label: row.get(17)?, 85 + uri: row.get(18)?, 86 + copyright_message: row.get(19)?, 87 + artist_uri: row.get(20)?, 88 + album_uri: row.get(21)?, 89 + created_at: row.get(22)?, 90 + play_count: row.get(23)?, 91 + unique_listeners: row.get(24)?, 92 + ..Default::default() 93 + }) 94 + }, 95 + )?; 90 96 let tracks: Result<Vec<_>, _> = tracks.collect(); 91 97 Ok(HttpResponse::Ok().json(tracks?)) 92 - }, 98 + } 93 99 None => { 94 100 let mut stmt = conn.prepare(r#" 95 101 SELECT ··· 161 167 } 162 168 } 163 169 164 - pub async fn get_loved_tracks(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 170 + pub async fn get_loved_tracks( 171 + payload: &mut web::Payload, 172 + _req: &HttpRequest, 173 + conn: Arc<Mutex<Connection>>, 174 + ) -> Result<HttpResponse, Error> { 165 175 let body = read_payload!(payload); 166 176 let params = serde_json::from_slice::<GetLovedTracksParams>(&body)?; 167 177 let pagination = params.pagination.unwrap_or_default(); ··· 170 180 let did = params.user_did; 171 181 172 182 let conn = conn.lock().unwrap(); 173 - let mut stmt = conn.prepare(r#" 183 + let mut stmt = conn.prepare( 184 + r#" 174 185 SELECT 175 186 t.id, 176 187 t.title, ··· 202 213 ORDER BY l.created_at DESC 203 214 OFFSET ? 204 215 LIMIT ?; 205 - "#)?; 206 - let loved_tracks = stmt.query_map([&did, &did, &limit.to_string(), &offset.to_string()], |row| { 207 - Ok(Track { 208 - id: row.get(0)?, 209 - title: row.get(1)?, 210 - artist: row.get(2)?, 211 - album: row.get(3)?, 212 - album_artist: row.get(4)?, 213 - album_art: row.get(5)?, 214 - album_uri: row.get(6)?, 215 - artist_uri: row.get(7)?, 216 - composer: row.get(8)?, 217 - copyright_message: row.get(9)?, 218 - disc_number: row.get(10)?, 219 - duration: row.get(11)?, 220 - track_number: row.get(12)?, 221 - label: row.get(13)?, 222 - spotify_link: row.get(14)?, 223 - tidal_link: row.get(15)?, 224 - youtube_link: row.get(16)?, 225 - apple_music_link: row.get(17)?, 226 - sha256: row.get(18)?, 227 - uri: row.get(19)?, 228 - handle: row.get(20)?, 229 - did: row.get(21)?, 230 - created_at: row.get(22)?, 231 - ..Default::default() 232 - }) 233 - })?; 216 + "#, 217 + )?; 218 + let loved_tracks = stmt.query_map( 219 + [&did, &did, &limit.to_string(), &offset.to_string()], 220 + |row| { 221 + Ok(Track { 222 + id: row.get(0)?, 223 + title: row.get(1)?, 224 + artist: row.get(2)?, 225 + album: row.get(3)?, 226 + album_artist: row.get(4)?, 227 + album_art: row.get(5)?, 228 + album_uri: row.get(6)?, 229 + artist_uri: row.get(7)?, 230 + composer: row.get(8)?, 231 + copyright_message: row.get(9)?, 232 + disc_number: row.get(10)?, 233 + duration: row.get(11)?, 234 + track_number: row.get(12)?, 235 + label: row.get(13)?, 236 + spotify_link: row.get(14)?, 237 + tidal_link: row.get(15)?, 238 + youtube_link: row.get(16)?, 239 + apple_music_link: row.get(17)?, 240 + sha256: row.get(18)?, 241 + uri: row.get(19)?, 242 + handle: row.get(20)?, 243 + did: row.get(21)?, 244 + created_at: row.get(22)?, 245 + ..Default::default() 246 + }) 247 + }, 248 + )?; 234 249 let loved_tracks: Result<Vec<_>, _> = loved_tracks.collect(); 235 250 Ok(HttpResponse::Ok().json(loved_tracks?)) 236 251 } 237 252 238 - pub async fn get_top_tracks(payload: &mut web::Payload, _req: &HttpRequest, conn: Arc<Mutex<Connection>>) -> Result<HttpResponse, Error> { 253 + pub async fn get_top_tracks( 254 + payload: &mut web::Payload, 255 + _req: &HttpRequest, 256 + conn: Arc<Mutex<Connection>>, 257 + ) -> Result<HttpResponse, Error> { 239 258 let body = read_payload!(payload); 240 259 let params = serde_json::from_slice::<GetTopTracksParams>(&body)?; 241 260 let pagination = params.pagination.unwrap_or_default(); ··· 275 294 OFFSET ? 276 295 LIMIT ?; 277 296 "#)?; 278 - let top_tracks = stmt.query_map([&did, &did, &limit.to_string(), &offset.to_string()], |row| { 279 - Ok(Track { 280 - id: row.get(0)?, 281 - title: row.get(1)?, 282 - artist: row.get(2)?, 283 - album_artist: row.get(3)?, 284 - album: row.get(4)?, 285 - uri: row.get(5)?, 286 - album_art: row.get(6)?, 287 - duration: row.get(7)?, 288 - disc_number: row.get(8)?, 289 - track_number: row.get(9)?, 290 - artist_uri: row.get(10)?, 291 - album_uri: row.get(11)?, 292 - sha256: row.get(12)?, 293 - created_at: row.get(13)?, 294 - play_count: row.get(14)?, 295 - unique_listeners: row.get(15)?, 296 - ..Default::default() 297 - }) 298 - })?; 297 + let top_tracks = stmt.query_map( 298 + [&did, &did, &limit.to_string(), &offset.to_string()], 299 + |row| { 300 + Ok(Track { 301 + id: row.get(0)?, 302 + title: row.get(1)?, 303 + artist: row.get(2)?, 304 + album_artist: row.get(3)?, 305 + album: row.get(4)?, 306 + uri: row.get(5)?, 307 + album_art: row.get(6)?, 308 + duration: row.get(7)?, 309 + disc_number: row.get(8)?, 310 + track_number: row.get(9)?, 311 + artist_uri: row.get(10)?, 312 + album_uri: row.get(11)?, 313 + sha256: row.get(12)?, 314 + created_at: row.get(13)?, 315 + play_count: row.get(14)?, 316 + unique_listeners: row.get(15)?, 317 + ..Default::default() 318 + }) 319 + }, 320 + )?; 299 321 let top_tracks: Result<Vec<_>, _> = top_tracks.collect(); 300 322 Ok(HttpResponse::Ok().json(top_tracks?)) 301 - }, 323 + } 302 324 None => { 303 325 let mut stmt = conn.prepare(r#" 304 326 SELECT ··· 353 375 Ok(HttpResponse::Ok().json(top_tracks?)) 354 376 } 355 377 } 356 - } 378 + }
+13 -14
crates/analytics/src/main.rs
··· 1 1 use core::create_tables; 2 - use std::{env, sync::{Arc, Mutex}}; 2 + use std::{ 3 + env, 4 + sync::{Arc, Mutex}, 5 + }; 3 6 4 7 use clap::Command; 5 8 use cmd::{serve::serve, sync::sync}; 9 + use dotenv::dotenv; 6 10 use duckdb::Connection; 7 11 use sqlx::postgres::PgPoolOptions; 8 - use dotenv::dotenv; 9 12 10 - pub mod types; 11 - pub mod xata; 12 13 pub mod cmd; 13 14 pub mod core; 14 15 pub mod handlers; 15 16 pub mod subscriber; 17 + pub mod types; 18 + pub mod xata; 16 19 17 20 fn cli() -> Command { 18 21 Command::new("analytics") 19 22 .version(env!("CARGO_PKG_VERSION")) 20 23 .about("Rocksky Analytics CLI built with Rust and DuckDB") 21 - .subcommand( 22 - Command::new("sync") 23 - .about("Sync data from Xata to DuckDB") 24 - ) 25 - .subcommand( 26 - Command::new("serve") 27 - .about("Serve the Rocksky Analytics API") 28 - ) 24 + .subcommand(Command::new("sync").about("Sync data from Xata to DuckDB")) 25 + .subcommand(Command::new("serve").about("Serve the Rocksky Analytics API")) 29 26 } 30 27 31 28 #[tokio::main] 32 29 async fn main() -> Result<(), Box<dyn std::error::Error>> { 33 30 dotenv().ok(); 34 31 35 - 36 - let pool= PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; 32 + let pool = PgPoolOptions::new() 33 + .max_connections(5) 34 + .connect(&env::var("XATA_POSTGRES_URL")?) 35 + .await?; 37 36 let conn = Connection::open("./rocksky-analytics.ddb")?; 38 37 39 38 create_tables(&conn).await?;
+454 -443
crates/analytics/src/subscriber/mod.rs
··· 1 - use std::{env, sync::{Arc, Mutex}, thread}; 2 1 use anyhow::Error; 3 2 use async_nats::{connect, Client}; 4 3 use duckdb::{params, Connection}; 5 4 use owo_colors::OwoColorize; 5 + use std::{ 6 + env, 7 + sync::{Arc, Mutex}, 8 + thread, 9 + }; 6 10 use tokio_stream::StreamExt; 7 11 use types::{LikePayload, NewTrackPayload, ScrobblePayload, UnlikePayload, UserPayload}; 8 12 ··· 25 29 } 26 30 27 31 pub fn on_scrobble(nc: Arc<Mutex<Client>>, conn: Arc<Mutex<Connection>>) { 28 - thread::spawn(move || { 29 - let rt = tokio::runtime::Runtime::new().unwrap(); 30 - let conn = conn.clone(); 31 - let nc = nc.clone(); 32 - rt.block_on(async { 33 - let nc = nc.lock().unwrap(); 34 - let mut sub = nc.subscribe("rocksky.scrobble".to_string()).await?; 35 - drop(nc); 32 + thread::spawn(move || { 33 + let rt = tokio::runtime::Runtime::new().unwrap(); 34 + let conn = conn.clone(); 35 + let nc = nc.clone(); 36 + rt.block_on(async { 37 + let nc = nc.lock().unwrap(); 38 + let mut sub = nc.subscribe("rocksky.scrobble".to_string()).await?; 39 + drop(nc); 36 40 37 - while let Some(msg) = sub.next().await { 38 - let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 39 - match serde_json::from_str::<ScrobblePayload>(&data) { 40 - Ok(payload) => { 41 - match save_scrobble(conn.clone(), payload.clone()).await { 42 - Ok(_) => println!("Scrobble saved successfully for {}", payload.scrobble.uri.cyan()), 43 - Err(e) => eprintln!("Error saving scrobble: {}", e), 41 + while let Some(msg) = sub.next().await { 42 + let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 43 + match serde_json::from_str::<ScrobblePayload>(&data) { 44 + Ok(payload) => match save_scrobble(conn.clone(), payload.clone()).await { 45 + Ok(_) => println!( 46 + "Scrobble saved successfully for {}", 47 + payload.scrobble.uri.cyan() 48 + ), 49 + Err(e) => eprintln!("Error saving scrobble: {}", e), 50 + }, 51 + Err(e) => { 52 + eprintln!("Error parsing payload: {}", e); 53 + println!("{}", data); 54 + } 55 + } 44 56 } 45 - }, 46 - Err(e) => { 47 - eprintln!("Error parsing payload: {}", e); 48 - println!("{}", data); 49 - } 50 - } 51 - } 52 57 53 - Ok::<(), Error>(()) 54 - })?; 58 + Ok::<(), Error>(()) 59 + })?; 55 60 56 - Ok::<(), Error>(()) 57 - }); 61 + Ok::<(), Error>(()) 62 + }); 58 63 } 59 64 60 65 pub fn on_new_track(nc: Arc<Mutex<Client>>, conn: Arc<Mutex<Connection>>) { 61 - thread::spawn(move || { 62 - let rt = tokio::runtime::Runtime::new().unwrap(); 63 - let conn = conn.clone(); 64 - let nc = nc.clone(); 65 - rt.block_on(async { 66 - let nc = nc.lock().unwrap(); 67 - let mut sub = nc.subscribe("rocksky.track".to_string()).await?; 68 - drop(nc); 66 + thread::spawn(move || { 67 + let rt = tokio::runtime::Runtime::new().unwrap(); 68 + let conn = conn.clone(); 69 + let nc = nc.clone(); 70 + rt.block_on(async { 71 + let nc = nc.lock().unwrap(); 72 + let mut sub = nc.subscribe("rocksky.track".to_string()).await?; 73 + drop(nc); 69 74 70 - while let Some(msg) = sub.next().await { 71 - let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 72 - match serde_json::from_str::<NewTrackPayload>(&data) { 73 - Ok(payload) => { 74 - match save_track(conn.clone(), payload.clone()).await { 75 - Ok(_) => println!("Song saved successfully for {}", payload.track.title.cyan()), 76 - Err(e) => eprintln!("Error saving song: {}", e), 75 + while let Some(msg) = sub.next().await { 76 + let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 77 + match serde_json::from_str::<NewTrackPayload>(&data) { 78 + Ok(payload) => match save_track(conn.clone(), payload.clone()).await { 79 + Ok(_) => { 80 + println!("Song saved successfully for {}", payload.track.title.cyan()) 81 + } 82 + Err(e) => eprintln!("Error saving song: {}", e), 83 + }, 84 + Err(e) => { 85 + eprintln!("Error parsing payload: {}", e); 86 + println!("{}", data); 87 + } 88 + } 77 89 } 78 - }, 79 - Err(e) => { 80 - eprintln!("Error parsing payload: {}", e); 81 - println!("{}", data); 82 - } 83 - } 84 - } 85 90 86 - Ok::<(), Error>(()) 87 - })?; 91 + Ok::<(), Error>(()) 92 + })?; 88 93 89 - Ok::<(), Error>(()) 90 - }); 94 + Ok::<(), Error>(()) 95 + }); 91 96 } 92 97 93 98 pub fn on_like(nc: Arc<Mutex<Client>>, conn: Arc<Mutex<Connection>>) { 94 - thread::spawn(move || { 95 - let rt = tokio::runtime::Runtime::new().unwrap(); 96 - let conn = conn.clone(); 97 - let nc = nc.clone(); 98 - rt.block_on(async { 99 - let nc = nc.lock().unwrap(); 100 - let mut sub = nc.subscribe("rocksky.like".to_string()).await?; 101 - drop(nc); 99 + thread::spawn(move || { 100 + let rt = tokio::runtime::Runtime::new().unwrap(); 101 + let conn = conn.clone(); 102 + let nc = nc.clone(); 103 + rt.block_on(async { 104 + let nc = nc.lock().unwrap(); 105 + let mut sub = nc.subscribe("rocksky.like".to_string()).await?; 106 + drop(nc); 102 107 103 - while let Some(msg) = sub.next().await { 104 - let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 105 - match serde_json::from_str::<LikePayload>(&data) { 106 - Ok(payload) => { 107 - match like(conn.clone(), payload.clone()).await { 108 - Ok(_) => println!("Like saved successfully for {}", payload.track_id.xata_id.cyan()), 109 - Err(e) => eprintln!("Error saving like: {}", e), 108 + while let Some(msg) = sub.next().await { 109 + let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 110 + match serde_json::from_str::<LikePayload>(&data) { 111 + Ok(payload) => match like(conn.clone(), payload.clone()).await { 112 + Ok(_) => println!( 113 + "Like saved successfully for {}", 114 + payload.track_id.xata_id.cyan() 115 + ), 116 + Err(e) => eprintln!("Error saving like: {}", e), 117 + }, 118 + Err(e) => { 119 + eprintln!("Error parsing payload: {}", e); 120 + println!("{}", data); 121 + } 122 + } 110 123 } 111 - }, 112 - Err(e) => { 113 - eprintln!("Error parsing payload: {}", e); 114 - println!("{}", data); 115 - } 116 - } 117 - } 118 124 119 - Ok::<(), Error>(()) 120 - })?; 125 + Ok::<(), Error>(()) 126 + })?; 121 127 122 - Ok::<(), Error>(()) 123 - }); 128 + Ok::<(), Error>(()) 129 + }); 124 130 } 125 131 126 132 pub fn on_unlike(nc: Arc<Mutex<Client>>, conn: Arc<Mutex<Connection>>) { 127 - thread::spawn(move || { 128 - let rt = tokio::runtime::Runtime::new().unwrap(); 129 - let conn = conn.clone(); 130 - let nc = nc.clone(); 131 - rt.block_on(async { 132 - let nc = nc.lock().unwrap(); 133 - let mut sub = nc.subscribe("rocksky.unlike".to_string()).await?; 134 - drop(nc); 133 + thread::spawn(move || { 134 + let rt = tokio::runtime::Runtime::new().unwrap(); 135 + let conn = conn.clone(); 136 + let nc = nc.clone(); 137 + rt.block_on(async { 138 + let nc = nc.lock().unwrap(); 139 + let mut sub = nc.subscribe("rocksky.unlike".to_string()).await?; 140 + drop(nc); 135 141 136 - while let Some(msg) = sub.next().await { 137 - let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 138 - match serde_json::from_str::<UnlikePayload>(&data) { 139 - Ok(payload) => { 140 - match unlike(conn.clone(), payload.clone()).await { 141 - Ok(_) => println!("Unlike saved successfully for {}", payload.track_id.xata_id.cyan()), 142 - Err(e) => eprintln!("Error saving unlike: {}", e), 142 + while let Some(msg) = sub.next().await { 143 + let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 144 + match serde_json::from_str::<UnlikePayload>(&data) { 145 + Ok(payload) => match unlike(conn.clone(), payload.clone()).await { 146 + Ok(_) => println!( 147 + "Unlike saved successfully for {}", 148 + payload.track_id.xata_id.cyan() 149 + ), 150 + Err(e) => eprintln!("Error saving unlike: {}", e), 151 + }, 152 + Err(e) => { 153 + eprintln!("Error parsing payload: {}", e); 154 + println!("{}", data); 155 + } 156 + } 143 157 } 144 - }, 145 - Err(e) => { 146 - eprintln!("Error parsing payload: {}", e); 147 - println!("{}", data); 148 - } 149 - } 150 - } 151 158 152 - Ok::<(), Error>(()) 153 - })?; 159 + Ok::<(), Error>(()) 160 + })?; 154 161 155 - Ok::<(), Error>(()) 156 - }); 162 + Ok::<(), Error>(()) 163 + }); 157 164 } 158 165 159 166 pub fn on_new_user(nc: Arc<Mutex<Client>>, conn: Arc<Mutex<Connection>>) { 160 - thread::spawn(move || { 161 - let rt = tokio::runtime::Runtime::new().unwrap(); 162 - let conn = conn.clone(); 163 - let nc = nc.clone(); 164 - rt.block_on(async { 165 - let nc = nc.lock().unwrap(); 166 - let mut sub = nc.subscribe("rocksky.user".to_string()).await?; 167 - drop(nc); 167 + thread::spawn(move || { 168 + let rt = tokio::runtime::Runtime::new().unwrap(); 169 + let conn = conn.clone(); 170 + let nc = nc.clone(); 171 + rt.block_on(async { 172 + let nc = nc.lock().unwrap(); 173 + let mut sub = nc.subscribe("rocksky.user".to_string()).await?; 174 + drop(nc); 168 175 169 - while let Some(msg) = sub.next().await { 170 - let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 171 - match serde_json::from_str::<UserPayload>(&data) { 172 - Ok(payload) => { 173 - match save_user(conn.clone(), payload.clone()).await { 174 - Ok(_) => println!("User saved successfully for {}{}", "@".cyan(), payload.handle.cyan()), 175 - Err(e) => eprintln!("Error saving user: {}", e), 176 + while let Some(msg) = sub.next().await { 177 + let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 178 + match serde_json::from_str::<UserPayload>(&data) { 179 + Ok(payload) => match save_user(conn.clone(), payload.clone()).await { 180 + Ok(_) => println!( 181 + "User saved successfully for {}{}", 182 + "@".cyan(), 183 + payload.handle.cyan() 184 + ), 185 + Err(e) => eprintln!("Error saving user: {}", e), 186 + }, 187 + Err(e) => { 188 + eprintln!("Error parsing payload: {}", e); 189 + println!("{}", data); 190 + } 191 + } 176 192 } 177 - }, 178 - Err(e) => { 179 - eprintln!("Error parsing payload: {}", e); 180 - println!("{}", data); 181 - } 182 - } 183 - } 184 193 185 - Ok::<(), Error>(()) 186 - })?; 194 + Ok::<(), Error>(()) 195 + })?; 187 196 188 - Ok::<(), Error>(()) 189 - }); 197 + Ok::<(), Error>(()) 198 + }); 190 199 } 191 200 192 - pub async fn save_scrobble(conn: Arc<Mutex<Connection>>, payload: ScrobblePayload) -> Result<(), Error> { 201 + pub async fn save_scrobble( 202 + conn: Arc<Mutex<Connection>>, 203 + payload: ScrobblePayload, 204 + ) -> Result<(), Error> { 193 205 let conn = conn.lock().unwrap(); 194 206 195 207 match conn.execute( 196 - "INSERT INTO artists ( 208 + "INSERT INTO artists ( 197 209 id, 198 210 name, 199 211 biography, ··· 222 234 ?, 223 235 ? 224 236 )", 225 - params![ 226 - payload.scrobble.artist_id.xata_id, 227 - payload.scrobble.artist_id.name, 228 - payload.scrobble.artist_id.biography, 229 - payload.scrobble.artist_id.born, 230 - payload.scrobble.artist_id.born_in, 231 - payload.scrobble.artist_id.died, 232 - payload.scrobble.artist_id.picture, 233 - payload.scrobble.artist_id.sha256, 234 - payload.scrobble.artist_id.spotify_link, 235 - payload.scrobble.artist_id.tidal_link, 236 - payload.scrobble.artist_id.youtube_link, 237 - payload.scrobble.artist_id.apple_music_link, 238 - payload.scrobble.artist_id.uri, 239 - ], 240 - ) { 241 - Ok(_) => (), 242 - Err(e) => { 243 - if !e.to_string().contains("violates primary key constraint") { 244 - println!("[artists] error: {}", e); 245 - return Err(e.into()); 246 - } 247 - } 248 - } 237 + params![ 238 + payload.scrobble.artist_id.xata_id, 239 + payload.scrobble.artist_id.name, 240 + payload.scrobble.artist_id.biography, 241 + payload.scrobble.artist_id.born, 242 + payload.scrobble.artist_id.born_in, 243 + payload.scrobble.artist_id.died, 244 + payload.scrobble.artist_id.picture, 245 + payload.scrobble.artist_id.sha256, 246 + payload.scrobble.artist_id.spotify_link, 247 + payload.scrobble.artist_id.tidal_link, 248 + payload.scrobble.artist_id.youtube_link, 249 + payload.scrobble.artist_id.apple_music_link, 250 + payload.scrobble.artist_id.uri, 251 + ], 252 + ) { 253 + Ok(_) => (), 254 + Err(e) => { 255 + if !e.to_string().contains("violates primary key constraint") { 256 + println!("[artists] error: {}", e); 257 + return Err(e.into()); 258 + } 259 + } 260 + } 249 261 250 262 match conn.execute( 251 - "INSERT INTO albums ( 263 + "INSERT INTO albums ( 252 264 id, 253 265 title, 254 266 artist, ··· 277 289 ?, 278 290 ? 279 291 )", 280 - params![ 281 - payload.scrobble.album_id.xata_id, 282 - payload.scrobble.album_id.title, 283 - payload.scrobble.album_id.artist, 284 - payload.scrobble.album_id.release_date, 285 - payload.scrobble.album_id.album_art, 286 - payload.scrobble.album_id.year, 287 - payload.scrobble.album_id.spotify_link, 288 - payload.scrobble.album_id.tidal_link, 289 - payload.scrobble.album_id.youtube_link, 290 - payload.scrobble.album_id.apple_music_link, 291 - payload.scrobble.album_id.sha256, 292 - payload.scrobble.album_id.uri, 293 - payload.scrobble.album_id.artist_uri, 294 - ], 292 + params![ 293 + payload.scrobble.album_id.xata_id, 294 + payload.scrobble.album_id.title, 295 + payload.scrobble.album_id.artist, 296 + payload.scrobble.album_id.release_date, 297 + payload.scrobble.album_id.album_art, 298 + payload.scrobble.album_id.year, 299 + payload.scrobble.album_id.spotify_link, 300 + payload.scrobble.album_id.tidal_link, 301 + payload.scrobble.album_id.youtube_link, 302 + payload.scrobble.album_id.apple_music_link, 303 + payload.scrobble.album_id.sha256, 304 + payload.scrobble.album_id.uri, 305 + payload.scrobble.album_id.artist_uri, 306 + ], 295 307 ) { 296 308 Ok(_) => (), 297 309 Err(e) => { 298 - if !e.to_string().contains("violates primary key constraint") { 299 - println!("[albums] error: {}", e); 300 - return Err(e.into()); 301 - } 302 - }, 310 + if !e.to_string().contains("violates primary key constraint") { 311 + println!("[albums] error: {}", e); 312 + return Err(e.into()); 313 + } 314 + } 303 315 } 304 316 305 317 match conn.execute( 306 - "INSERT INTO tracks ( 318 + "INSERT INTO tracks ( 307 319 id, 308 320 title, 309 321 artist, ··· 329 341 album_uri, 330 342 created_at 331 343 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 332 - params![ 333 - payload.scrobble.track_id.xata_id, 334 - payload.scrobble.track_id.title, 335 - payload.scrobble.track_id.artist, 336 - payload.scrobble.track_id.album_artist, 337 - payload.scrobble.track_id.album_art, 338 - payload.scrobble.track_id.album, 339 - payload.scrobble.track_id.track_number, 340 - payload.scrobble.track_id.duration, 341 - payload.scrobble.track_id.mb_id, 342 - payload.scrobble.track_id.youtube_link, 343 - payload.scrobble.track_id.spotify_link, 344 - payload.scrobble.track_id.tidal_link, 345 - payload.scrobble.track_id.apple_music_link, 346 - payload.scrobble.track_id.sha256, 347 - payload.scrobble.track_id.lyrics, 348 - payload.scrobble.track_id.composer, 349 - payload.scrobble.track_id.genre, 350 - payload.scrobble.track_id.disc_number, 351 - payload.scrobble.track_id.copyright_message, 352 - payload.scrobble.track_id.label, 353 - payload.scrobble.track_id.uri, 354 - payload.scrobble.track_id.artist_uri, 355 - payload.scrobble.track_id.album_uri, 356 - payload.scrobble.track_id.xata_createdat, 357 - ], 344 + params![ 345 + payload.scrobble.track_id.xata_id, 346 + payload.scrobble.track_id.title, 347 + payload.scrobble.track_id.artist, 348 + payload.scrobble.track_id.album_artist, 349 + payload.scrobble.track_id.album_art, 350 + payload.scrobble.track_id.album, 351 + payload.scrobble.track_id.track_number, 352 + payload.scrobble.track_id.duration, 353 + payload.scrobble.track_id.mb_id, 354 + payload.scrobble.track_id.youtube_link, 355 + payload.scrobble.track_id.spotify_link, 356 + payload.scrobble.track_id.tidal_link, 357 + payload.scrobble.track_id.apple_music_link, 358 + payload.scrobble.track_id.sha256, 359 + payload.scrobble.track_id.lyrics, 360 + payload.scrobble.track_id.composer, 361 + payload.scrobble.track_id.genre, 362 + payload.scrobble.track_id.disc_number, 363 + payload.scrobble.track_id.copyright_message, 364 + payload.scrobble.track_id.label, 365 + payload.scrobble.track_id.uri, 366 + payload.scrobble.track_id.artist_uri, 367 + payload.scrobble.track_id.album_uri, 368 + payload.scrobble.track_id.xata_createdat, 369 + ], 358 370 ) { 359 371 Ok(_) => (), 360 372 Err(e) => { 361 - if !e.to_string().contains("violates primary key constraint") { 362 - println!("[tracks] error: {}", e); 363 - return Err(e.into()); 364 - } 373 + if !e.to_string().contains("violates primary key constraint") { 374 + println!("[tracks] error: {}", e); 375 + return Err(e.into()); 376 + } 365 377 } 366 378 } 367 379 368 380 match conn.execute( 369 - "INSERT INTO album_tracks ( 381 + "INSERT INTO album_tracks ( 370 382 id, 371 383 album_id, 372 384 track_id 373 385 ) VALUES (?, 374 386 ?, 375 387 ?)", 376 - params![ 377 - payload.album_track.xata_id, 378 - payload.album_track.album_id.xata_id, 379 - payload.album_track.track_id.xata_id, 380 - ], 388 + params![ 389 + payload.album_track.xata_id, 390 + payload.album_track.album_id.xata_id, 391 + payload.album_track.track_id.xata_id, 392 + ], 381 393 ) { 382 394 Ok(_) => (), 383 395 Err(e) => { 384 - if !e.to_string().contains("violates primary key constraint") { 385 - println!("[album_tracks] error: {}", e); 386 - return Err(e.into()); 387 - } 396 + if !e.to_string().contains("violates primary key constraint") { 397 + println!("[album_tracks] error: {}", e); 398 + return Err(e.into()); 399 + } 388 400 } 389 401 } 390 402 391 403 match conn.execute( 392 - "INSERT INTO artist_tracks (id, artist_id, track_id, created_at) VALUES (?, ?, ?, ?)", 393 - params![ 394 - payload.artist_track.xata_id, 395 - payload.artist_track.artist_id.xata_id, 396 - payload.artist_track.track_id.xata_id, 397 - payload.artist_track.xata_createdat, 398 - ], 404 + "INSERT INTO artist_tracks (id, artist_id, track_id, created_at) VALUES (?, ?, ?, ?)", 405 + params![ 406 + payload.artist_track.xata_id, 407 + payload.artist_track.artist_id.xata_id, 408 + payload.artist_track.track_id.xata_id, 409 + payload.artist_track.xata_createdat, 410 + ], 399 411 ) { 400 412 Ok(_) => (), 401 413 Err(e) => { 402 - if !e.to_string().contains("violates primary key constraint") { 403 - println!("[artist_tracks] error: {}", e); 404 - return Err(e.into()); 405 - } 414 + if !e.to_string().contains("violates primary key constraint") { 415 + println!("[artist_tracks] error: {}", e); 416 + return Err(e.into()); 417 + } 406 418 } 407 419 } 408 420 409 421 match conn.execute( 410 - "INSERT INTO artist_albums (id, artist_id, album_id, created_at) VALUES (?, ?, ?, ?)", 411 - params![ 422 + "INSERT INTO artist_albums (id, artist_id, album_id, created_at) VALUES (?, ?, ?, ?)", 423 + params![ 412 424 payload.artist_album.xata_id, 413 425 payload.artist_album.artist_id.xata_id, 414 426 payload.artist_album.album_id.xata_id, 415 427 payload.artist_album.xata_createdat, 416 - ], 417 - ) { 418 - Ok(_) => (), 419 - Err(e) => { 420 - if !e.to_string().contains("violates primary key constraint") { 421 - println!("[artist_albums] error: {}", e); 422 - return Err(e.into()); 428 + ], 429 + ) { 430 + Ok(_) => (), 431 + Err(e) => { 432 + if !e.to_string().contains("violates primary key constraint") { 433 + println!("[artist_albums] error: {}", e); 434 + return Err(e.into()); 435 + } 423 436 } 424 - } 425 - } 437 + } 426 438 427 439 match conn.execute( 428 - "INSERT INTO user_albums (id, user_id, album_id, created_at) VALUES (?, ?, ?, ?)", 429 - params![ 430 - payload.user_album.xata_id, 431 - payload.user_album.user_id.xata_id, 432 - payload.user_album.album_id.xata_id, 433 - payload.user_album.xata_createdat, 434 - ], 440 + "INSERT INTO user_albums (id, user_id, album_id, created_at) VALUES (?, ?, ?, ?)", 441 + params![ 442 + payload.user_album.xata_id, 443 + payload.user_album.user_id.xata_id, 444 + payload.user_album.album_id.xata_id, 445 + payload.user_album.xata_createdat, 446 + ], 435 447 ) { 436 448 Ok(_) => (), 437 449 Err(e) => { 438 - if !e.to_string().contains("violates primary key constraint") { 439 - println!("[user_albums] error: {}", e); 440 - return Err(e.into()); 441 - } 450 + if !e.to_string().contains("violates primary key constraint") { 451 + println!("[user_albums] error: {}", e); 452 + return Err(e.into()); 453 + } 442 454 } 443 455 } 444 456 445 457 match conn.execute( 446 - "INSERT INTO user_artists (id, user_id, artist_id, created_at) VALUES (?, ?, ?, ?)", 447 - params![ 448 - payload.user_artist.xata_id, 449 - payload.user_artist.user_id.xata_id, 450 - payload.user_artist.artist_id.xata_id, 451 - payload.user_artist.xata_createdat, 452 - ], 458 + "INSERT INTO user_artists (id, user_id, artist_id, created_at) VALUES (?, ?, ?, ?)", 459 + params![ 460 + payload.user_artist.xata_id, 461 + payload.user_artist.user_id.xata_id, 462 + payload.user_artist.artist_id.xata_id, 463 + payload.user_artist.xata_createdat, 464 + ], 453 465 ) { 454 466 Ok(_) => (), 455 467 Err(e) => { 456 - if !e.to_string().contains("violates primary key constraint") { 457 - println!("[user_artists] error: {}", e); 458 - return Err(e.into()); 459 - } 468 + if !e.to_string().contains("violates primary key constraint") { 469 + println!("[user_artists] error: {}", e); 470 + return Err(e.into()); 471 + } 460 472 } 461 473 } 462 474 463 475 match conn.execute( 464 - "INSERT INTO user_tracks (id, user_id, track_id, created_at) VALUES (?, ?, ?, ?)", 465 - params![ 466 - payload.user_track.xata_id, 467 - payload.user_track.user_id.xata_id, 468 - payload.user_track.track_id.xata_id, 469 - payload.user_track.xata_createdat, 470 - ], 476 + "INSERT INTO user_tracks (id, user_id, track_id, created_at) VALUES (?, ?, ?, ?)", 477 + params![ 478 + payload.user_track.xata_id, 479 + payload.user_track.user_id.xata_id, 480 + payload.user_track.track_id.xata_id, 481 + payload.user_track.xata_createdat, 482 + ], 471 483 ) { 472 484 Ok(_) => (), 473 485 Err(e) => { 474 - if !e.to_string().contains("violates primary key constraint") { 475 - println!("[user_tracks] error: {}", e); 476 - return Err(e.into()); 477 - } 486 + if !e.to_string().contains("violates primary key constraint") { 487 + println!("[user_tracks] error: {}", e); 488 + return Err(e.into()); 489 + } 478 490 } 479 491 } 480 492 481 493 match conn.execute( 482 - "INSERT INTO scrobbles ( 494 + "INSERT INTO scrobbles ( 483 495 id, 484 496 user_id, 485 497 track_id, ··· 496 508 ?, 497 509 ? 498 510 )", 499 - params![ 500 - payload.scrobble.xata_id, 501 - payload.scrobble.user_id.xata_id, 502 - payload.scrobble.track_id.xata_id, 503 - payload.scrobble.album_id.xata_id, 504 - payload.scrobble.artist_id.xata_id, 505 - payload.scrobble.uri, 506 - payload.scrobble.timestamp, 507 - ], 508 - ) { 509 - Ok(_) => (), 510 - Err(e) => { 511 - if !e.to_string().contains("violates primary key constraint") { 512 - println!("[scrobbles] error: {}", e); 513 - return Err(e.into()); 514 - } 515 - } 516 - } 511 + params![ 512 + payload.scrobble.xata_id, 513 + payload.scrobble.user_id.xata_id, 514 + payload.scrobble.track_id.xata_id, 515 + payload.scrobble.album_id.xata_id, 516 + payload.scrobble.artist_id.xata_id, 517 + payload.scrobble.uri, 518 + payload.scrobble.timestamp, 519 + ], 520 + ) { 521 + Ok(_) => (), 522 + Err(e) => { 523 + if !e.to_string().contains("violates primary key constraint") { 524 + println!("[scrobbles] error: {}", e); 525 + return Err(e.into()); 526 + } 527 + } 528 + } 517 529 518 530 Ok(()) 519 531 } 520 532 521 - pub async fn save_track(conn: Arc<Mutex<Connection>>, payload: NewTrackPayload) -> Result<(), Error> { 522 - let conn = conn.lock().unwrap(); 533 + pub async fn save_track( 534 + conn: Arc<Mutex<Connection>>, 535 + payload: NewTrackPayload, 536 + ) -> Result<(), Error> { 537 + let conn = conn.lock().unwrap(); 523 538 524 - match conn.execute( 525 - "INSERT INTO tracks ( 539 + match conn.execute( 540 + "INSERT INTO tracks ( 526 541 id, 527 542 title, 528 543 artist, ··· 548 563 album_uri, 549 564 created_at 550 565 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 551 - params![ 552 - payload.track.xata_id, 553 - payload.track.title, 554 - payload.track.artist, 555 - payload.track.album_artist, 556 - payload.track.album_art, 557 - payload.track.album, 558 - payload.track.track_number, 559 - payload.track.duration, 560 - payload.track.mb_id, 561 - payload.track.youtube_link, 562 - payload.track.spotify_link, 563 - payload.track.tidal_link, 564 - payload.track.apple_music_link, 565 - payload.track.sha256, 566 - payload.track.lyrics, 567 - payload.track.composer, 568 - payload.track.genre, 569 - payload.track.disc_number, 570 - payload.track.copyright_message, 571 - payload.track.label, 572 - payload.track.uri, 573 - payload.track.artist_uri, 574 - payload.track.album_uri, 575 - payload.track.xata_createdat, 576 - ], 577 - ) { 578 - Ok(_) => (), 579 - Err(e) => { 580 - if !e.to_string().contains("violates primary key constraint") { 581 - println!("[tracks] error: {}", e); 582 - return Err(e.into()); 566 + params![ 567 + payload.track.xata_id, 568 + payload.track.title, 569 + payload.track.artist, 570 + payload.track.album_artist, 571 + payload.track.album_art, 572 + payload.track.album, 573 + payload.track.track_number, 574 + payload.track.duration, 575 + payload.track.mb_id, 576 + payload.track.youtube_link, 577 + payload.track.spotify_link, 578 + payload.track.tidal_link, 579 + payload.track.apple_music_link, 580 + payload.track.sha256, 581 + payload.track.lyrics, 582 + payload.track.composer, 583 + payload.track.genre, 584 + payload.track.disc_number, 585 + payload.track.copyright_message, 586 + payload.track.label, 587 + payload.track.uri, 588 + payload.track.artist_uri, 589 + payload.track.album_uri, 590 + payload.track.xata_createdat, 591 + ], 592 + ) { 593 + Ok(_) => (), 594 + Err(e) => { 595 + if !e.to_string().contains("violates primary key constraint") { 596 + println!("[tracks] error: {}", e); 597 + return Err(e.into()); 598 + } 583 599 } 584 - } 585 - } 600 + } 586 601 587 - match conn.execute( 588 - "INSERT INTO album_tracks ( 602 + match conn.execute( 603 + "INSERT INTO album_tracks ( 589 604 id, 590 605 album_id, 591 606 track_id 592 607 ) VALUES (?, 593 608 ?, 594 609 ?)", 595 - params![ 596 - payload.album_track.xata_id, 597 - payload.album_track.album_id.xata_id, 598 - payload.album_track.track_id.xata_id, 599 - ], 600 - ) { 601 - Ok(_) => (), 602 - Err(e) => { 603 - if !e.to_string().contains("violates primary key constraint") { 604 - println!("[album_tracks] error: {}", e); 605 - return Err(e.into()); 610 + params![ 611 + payload.album_track.xata_id, 612 + payload.album_track.album_id.xata_id, 613 + payload.album_track.track_id.xata_id, 614 + ], 615 + ) { 616 + Ok(_) => (), 617 + Err(e) => { 618 + if !e.to_string().contains("violates primary key constraint") { 619 + println!("[album_tracks] error: {}", e); 620 + return Err(e.into()); 621 + } 606 622 } 607 - } 608 - } 623 + } 609 624 610 - match conn.execute( 611 - "INSERT INTO artist_tracks (id, artist_id, track_id, created_at) VALUES (?, ?, ?, ?)", 612 - params![ 613 - payload.artist_track.xata_id, 614 - payload.artist_track.artist_id.xata_id, 615 - payload.artist_track.track_id.xata_id, 616 - payload.artist_track.xata_createdat, 617 - ], 618 - ) { 619 - Ok(_) => (), 620 - Err(e) => { 621 - if !e.to_string().contains("violates primary key constraint") { 622 - println!("[artist_tracks] error: {}", e); 623 - return Err(e.into()); 625 + match conn.execute( 626 + "INSERT INTO artist_tracks (id, artist_id, track_id, created_at) VALUES (?, ?, ?, ?)", 627 + params![ 628 + payload.artist_track.xata_id, 629 + payload.artist_track.artist_id.xata_id, 630 + payload.artist_track.track_id.xata_id, 631 + payload.artist_track.xata_createdat, 632 + ], 633 + ) { 634 + Ok(_) => (), 635 + Err(e) => { 636 + if !e.to_string().contains("violates primary key constraint") { 637 + println!("[artist_tracks] error: {}", e); 638 + return Err(e.into()); 639 + } 624 640 } 625 - } 626 - } 641 + } 627 642 628 - match conn.execute( 629 - "INSERT INTO artist_albums (id, artist_id, album_id, created_at) VALUES (?, ?, ?, ?)", 643 + match conn.execute( 644 + "INSERT INTO artist_albums (id, artist_id, album_id, created_at) VALUES (?, ?, ?, ?)", 630 645 params![ 631 - payload.artist_album.xata_id, 632 - payload.artist_album.artist_id.xata_id, 633 - payload.artist_album.album_id.xata_id, 634 - payload.artist_album.xata_createdat, 646 + payload.artist_album.xata_id, 647 + payload.artist_album.artist_id.xata_id, 648 + payload.artist_album.album_id.xata_id, 649 + payload.artist_album.xata_createdat, 635 650 ], 636 - ) { 637 - Ok(_) => (), 638 - Err(e) => { 639 - if !e.to_string().contains("violates primary key constraint") { 640 - println!("[artist_albums] error: {}", e); 641 - return Err(e.into()); 651 + ) { 652 + Ok(_) => (), 653 + Err(e) => { 654 + if !e.to_string().contains("violates primary key constraint") { 655 + println!("[artist_albums] error: {}", e); 656 + return Err(e.into()); 657 + } 642 658 } 643 - } 644 - } 645 - Ok(()) 659 + } 660 + Ok(()) 646 661 } 647 662 648 663 pub async fn like(conn: Arc<Mutex<Connection>>, payload: LikePayload) -> Result<(), Error> { 649 - let conn = conn.lock().unwrap(); 650 - match conn.execute( 651 - "INSERT INTO loved_tracks ( 664 + let conn = conn.lock().unwrap(); 665 + match conn.execute( 666 + "INSERT INTO loved_tracks ( 652 667 id, 653 668 user_id, 654 669 track_id, ··· 659 674 ?, 660 675 ? 661 676 )", 662 - params![ 663 - payload.xata_id, 664 - payload.user_id.xata_id, 665 - payload.track_id.xata_id, 666 - payload.xata_createdat, 667 - ], 668 - ) { 669 - Ok(_) => (), 670 - Err(e) => { 671 - if !e.to_string().contains("violates primary key constraint") { 672 - println!("[likes] error: {}", e); 673 - return Err(e.into()); 674 - } 675 - } 676 - } 677 - Ok(()) 677 + params![ 678 + payload.xata_id, 679 + payload.user_id.xata_id, 680 + payload.track_id.xata_id, 681 + payload.xata_createdat, 682 + ], 683 + ) { 684 + Ok(_) => (), 685 + Err(e) => { 686 + if !e.to_string().contains("violates primary key constraint") { 687 + println!("[likes] error: {}", e); 688 + return Err(e.into()); 689 + } 690 + } 691 + } 692 + Ok(()) 678 693 } 679 694 680 695 pub async fn unlike(conn: Arc<Mutex<Connection>>, payload: UnlikePayload) -> Result<(), Error> { 681 - let conn = conn.lock().unwrap(); 682 - match conn.execute( 683 - "DELETE FROM loved_tracks WHERE user_id = ? AND track_id = ?", 684 - params![ 685 - payload.user_id.xata_id, 686 - payload.track_id.xata_id, 687 - ], 688 - ) { 689 - Ok(_) => (), 690 - Err(e) => { 691 - println!("[unlikes] error: {}", e); 692 - return Err(e.into()); 693 - } 694 - } 695 - Ok(()) 696 + let conn = conn.lock().unwrap(); 697 + match conn.execute( 698 + "DELETE FROM loved_tracks WHERE user_id = ? AND track_id = ?", 699 + params![payload.user_id.xata_id, payload.track_id.xata_id,], 700 + ) { 701 + Ok(_) => (), 702 + Err(e) => { 703 + println!("[unlikes] error: {}", e); 704 + return Err(e.into()); 705 + } 706 + } 707 + Ok(()) 696 708 } 697 709 698 710 pub async fn save_user(conn: Arc<Mutex<Connection>>, payload: UserPayload) -> Result<(), Error> { 699 - let conn = conn.lock().unwrap(); 711 + let conn = conn.lock().unwrap(); 700 712 701 - match conn.execute( 702 - "INSERT INTO users ( 713 + match conn.execute( 714 + "INSERT INTO users ( 703 715 id, 704 716 avatar, 705 717 did, ··· 717 729 did = EXCLUDED.did, 718 730 display_name = EXCLUDED.display_name, 719 731 handle = EXCLUDED.handle", 720 - params![ 721 - payload.xata_id, 722 - payload.avatar, 723 - payload.did, 724 - payload.display_name, 725 - payload.handle, 726 - ], 727 - ) { 728 - Ok(_) => (), 729 - Err(e) => { 730 - if !e.to_string().contains("violates primary key constraint") { 731 - println!("[users] error: {}", e); 732 - return Err(e.into()); 733 - } 734 - } 735 - } 736 - Ok(()) 732 + params![ 733 + payload.xata_id, 734 + payload.avatar, 735 + payload.did, 736 + payload.display_name, 737 + payload.handle, 738 + ], 739 + ) { 740 + Ok(_) => (), 741 + Err(e) => { 742 + if !e.to_string().contains("violates primary key constraint") { 743 + println!("[users] error: {}", e); 744 + return Err(e.into()); 745 + } 746 + } 747 + } 748 + Ok(()) 737 749 } 738 750 739 751 #[cfg(test)] ··· 742 754 use super::types; 743 755 744 756 #[test] 745 - fn test_parse_scrobble() { 746 - let data = r#" 757 + fn test_parse_scrobble() { 758 + let data = r#" 747 759 { 748 760 "scrobble": { 749 761 "album_id": { ··· 907 919 } 908 920 "#; 909 921 910 - match serde_json::from_str::<types::ScrobblePayload>(data) { 911 - Err(e) => { 912 - eprintln!("Error parsing payload: {}", e); 913 - println!("{}", data); 914 - }, 915 - Ok(_) => { 916 - } 922 + match serde_json::from_str::<types::ScrobblePayload>(data) { 923 + Err(e) => { 924 + eprintln!("Error parsing payload: {}", e); 925 + println!("{}", data); 926 + } 927 + Ok(_) => {} 928 + } 929 + assert!(true); 917 930 } 918 - assert!(true); 919 - } 920 - } 931 + }
+30
crates/connect/Cargo.toml
··· 1 + [package] 2 + name = "connect" 3 + version = "0.1.0" 4 + authors.workspace = true 5 + edition.workspace = true 6 + license.workspace = true 7 + repository.workspace = true 8 + 9 + [dependencies] 10 + tungstenite = { version = "0.26.2", features = ["rustls"] } 11 + tokio-tungstenite = { version = "0.26.2", features = [ 12 + "tokio-rustls", 13 + "rustls-tls-webpki-roots", 14 + ] } 15 + futures-util = "0.3.31" 16 + tokio-stream = "0.1.17" 17 + tokio = { version = "1.45.1", features = ["full"] } 18 + dirs = "6.0.0" 19 + serde = { version = "1.0.217", features = ["derive"] } 20 + serde_json = "1.0.139" 21 + owo-colors = "4.2.1" 22 + anyhow = "1.0.98" 23 + async-trait = "0.1.88" 24 + reqwest = { version = "0.12.15", features = [ 25 + "rustls-tls", 26 + "json", 27 + ], default-features = false } 28 + jsonrpsee = { version = "0.25.1", features = ["client", "tokio"] } 29 + http = "1.3.1" 30 + base64 = "0.22.1"
+201
crates/connect/LICENSE
··· 1 + Apache License 2 + Version 2.0, January 2004 3 + http://www.apache.org/licenses/ 4 + 5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 + 7 + 1. Definitions. 8 + 9 + "License" shall mean the terms and conditions for use, reproduction, 10 + and distribution as defined by Sections 1 through 9 of this document. 11 + 12 + "Licensor" shall mean the copyright owner or entity authorized by 13 + the copyright owner that is granting the License. 14 + 15 + "Legal Entity" shall mean the union of the acting entity and all 16 + other entities that control, are controlled by, or are under common 17 + control with that entity. For the purposes of this definition, 18 + "control" means (i) the power, direct or indirect, to cause the 19 + direction or management of such entity, whether by contract or 20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 + outstanding shares, or (iii) beneficial ownership of such entity. 22 + 23 + "You" (or "Your") shall mean an individual or Legal Entity 24 + exercising permissions granted by this License. 25 + 26 + "Source" form shall mean the preferred form for making modifications, 27 + including but not limited to software source code, documentation 28 + source, and configuration files. 29 + 30 + "Object" form shall mean any form resulting from mechanical 31 + transformation or translation of a Source form, including but 32 + not limited to compiled object code, generated documentation, 33 + and conversions to other media types. 34 + 35 + "Work" shall mean the work of authorship, whether in Source or 36 + Object form, made available under the License, as indicated by a 37 + copyright notice that is included in or attached to the work 38 + (an example is provided in the Appendix below). 39 + 40 + "Derivative Works" shall mean any work, whether in Source or Object 41 + form, that is based on (or derived from) the Work and for which the 42 + editorial revisions, annotations, elaborations, or other modifications 43 + represent, as a whole, an original work of authorship. For the purposes 44 + of this License, Derivative Works shall not include works that remain 45 + separable from, or merely link (or bind by name) to the interfaces of, 46 + the Work and Derivative Works thereof. 47 + 48 + "Contribution" shall mean any work of authorship, including 49 + the original version of the Work and any modifications or additions 50 + to that Work or Derivative Works thereof, that is intentionally 51 + submitted to Licensor for inclusion in the Work by the copyright owner 52 + or by an individual or Legal Entity authorized to submit on behalf of 53 + the copyright owner. For the purposes of this definition, "submitted" 54 + means any form of electronic, verbal, or written communication sent 55 + to the Licensor or its representatives, including but not limited to 56 + communication on electronic mailing lists, source code control systems, 57 + and issue tracking systems that are managed by, or on behalf of, the 58 + Licensor for the purpose of discussing and improving the Work, but 59 + excluding communication that is conspicuously marked or otherwise 60 + designated in writing by the copyright owner as "Not a Contribution." 61 + 62 + "Contributor" shall mean Licensor and any individual or Legal Entity 63 + on behalf of whom a Contribution has been received by Licensor and 64 + subsequently incorporated within the Work. 65 + 66 + 2. Grant of Copyright License. Subject to the terms and conditions of 67 + this License, each Contributor hereby grants to You a perpetual, 68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 + copyright license to reproduce, prepare Derivative Works of, 70 + publicly display, publicly perform, sublicense, and distribute the 71 + Work and such Derivative Works in Source or Object form. 72 + 73 + 3. Grant of Patent License. Subject to the terms and conditions of 74 + this License, each Contributor hereby grants to You a perpetual, 75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 + (except as stated in this section) patent license to make, have made, 77 + use, offer to sell, sell, import, and otherwise transfer the Work, 78 + where such license applies only to those patent claims licensable 79 + by such Contributor that are necessarily infringed by their 80 + Contribution(s) alone or by combination of their Contribution(s) 81 + with the Work to which such Contribution(s) was submitted. If You 82 + institute patent litigation against any entity (including a 83 + cross-claim or counterclaim in a lawsuit) alleging that the Work 84 + or a Contribution incorporated within the Work constitutes direct 85 + or contributory patent infringement, then any patent licenses 86 + granted to You under this License for that Work shall terminate 87 + as of the date such litigation is filed. 88 + 89 + 4. Redistribution. You may reproduce and distribute copies of the 90 + Work or Derivative Works thereof in any medium, with or without 91 + modifications, and in Source or Object form, provided that You 92 + meet the following conditions: 93 + 94 + (a) You must give any other recipients of the Work or 95 + Derivative Works a copy of this License; and 96 + 97 + (b) You must cause any modified files to carry prominent notices 98 + stating that You changed the files; and 99 + 100 + (c) You must retain, in the Source form of any Derivative Works 101 + that You distribute, all copyright, patent, trademark, and 102 + attribution notices from the Source form of the Work, 103 + excluding those notices that do not pertain to any part of 104 + the Derivative Works; and 105 + 106 + (d) If the Work includes a "NOTICE" text file as part of its 107 + distribution, then any Derivative Works that You distribute must 108 + include a readable copy of the attribution notices contained 109 + within such NOTICE file, excluding those notices that do not 110 + pertain to any part of the Derivative Works, in at least one 111 + of the following places: within a NOTICE text file distributed 112 + as part of the Derivative Works; within the Source form or 113 + documentation, if provided along with the Derivative Works; or, 114 + within a display generated by the Derivative Works, if and 115 + wherever such third-party notices normally appear. The contents 116 + of the NOTICE file are for informational purposes only and 117 + do not modify the License. You may add Your own attribution 118 + notices within Derivative Works that You distribute, alongside 119 + or as an addendum to the NOTICE text from the Work, provided 120 + that such additional attribution notices cannot be construed 121 + as modifying the License. 122 + 123 + You may add Your own copyright statement to Your modifications and 124 + may provide additional or different license terms and conditions 125 + for use, reproduction, or distribution of Your modifications, or 126 + for any such Derivative Works as a whole, provided Your use, 127 + reproduction, and distribution of the Work otherwise complies with 128 + the conditions stated in this License. 129 + 130 + 5. Submission of Contributions. Unless You explicitly state otherwise, 131 + any Contribution intentionally submitted for inclusion in the Work 132 + by You to the Licensor shall be under the terms and conditions of 133 + this License, without any additional terms or conditions. 134 + Notwithstanding the above, nothing herein shall supersede or modify 135 + the terms of any separate license agreement you may have executed 136 + with Licensor regarding such Contributions. 137 + 138 + 6. Trademarks. This License does not grant permission to use the trade 139 + names, trademarks, service marks, or product names of the Licensor, 140 + except as required for reasonable and customary use in describing the 141 + origin of the Work and reproducing the content of the NOTICE file. 142 + 143 + 7. Disclaimer of Warranty. Unless required by applicable law or 144 + agreed to in writing, Licensor provides the Work (and each 145 + Contributor provides its Contributions) on an "AS IS" BASIS, 146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 + implied, including, without limitation, any warranties or conditions 148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 + PARTICULAR PURPOSE. You are solely responsible for determining the 150 + appropriateness of using or redistributing the Work and assume any 151 + risks associated with Your exercise of permissions under this License. 152 + 153 + 8. Limitation of Liability. In no event and under no legal theory, 154 + whether in tort (including negligence), contract, or otherwise, 155 + unless required by applicable law (such as deliberate and grossly 156 + negligent acts) or agreed to in writing, shall any Contributor be 157 + liable to You for damages, including any direct, indirect, special, 158 + incidental, or consequential damages of any character arising as a 159 + result of this License or out of the use or inability to use the 160 + Work (including but not limited to damages for loss of goodwill, 161 + work stoppage, computer failure or malfunction, or any and all 162 + other commercial damages or losses), even if such Contributor 163 + has been advised of the possibility of such damages. 164 + 165 + 9. Accepting Warranty or Additional Liability. While redistributing 166 + the Work or Derivative Works thereof, You may choose to offer, 167 + and charge a fee for, acceptance of support, warranty, indemnity, 168 + or other liability obligations and/or rights consistent with this 169 + License. However, in accepting such obligations, You may act only 170 + on Your own behalf and on Your sole responsibility, not on behalf 171 + of any other Contributor, and only if You agree to indemnify, 172 + defend, and hold each Contributor harmless for any liability 173 + incurred by, or claims asserted against, such Contributor by reason 174 + of your accepting any such warranty or additional liability. 175 + 176 + END OF TERMS AND CONDITIONS 177 + 178 + APPENDIX: How to apply the Apache License to your work. 179 + 180 + To apply the Apache License to your work, attach the following 181 + boilerplate notice, with the fields enclosed by brackets "[]" 182 + replaced with your own identifying information. (Don't include 183 + the brackets!) The text should be enclosed in the appropriate 184 + comment syntax for the file format. We also recommend that a 185 + file or class name and description of purpose be included on the 186 + same "printed page" as the copyright notice for easier 187 + identification within third-party archives. 188 + 189 + Copyright 2025 Tsiry Sandratraina 190 + 191 + Licensed under the Apache License, Version 2.0 (the "License"); 192 + you may not use this file except in compliance with the License. 193 + You may obtain a copy of the License at 194 + 195 + http://www.apache.org/licenses/LICENSE-2.0 196 + 197 + Unless required by applicable law or agreed to in writing, software 198 + distributed under the License is distributed on an "AS IS" BASIS, 199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 + See the License for the specific language governing permissions and 201 + limitations under the License.
+7
crates/connect/README.md
··· 1 + # Rocksky Connect 🔌 2 + 3 + Remote playback control for any local player — inspired by "Spotify Connect". 4 + 5 + Rocksky Connect lets you link your local music player to the Rocksky Web UI, enabling seamless remote playback control from anywhere. It's like Spotify Connect, but designed to work with any player that integrates with Rocksky. 6 + 7 + 🛠️ **Work in progress:** Rocksky Connect is still under active development — features and compatibility may change frequently.
+55
crates/connect/src/main.rs
··· 1 + use std::thread; 2 + 3 + use owo_colors::OwoColorize; 4 + use websocket::connect_to_rocksky_websocket; 5 + 6 + pub mod players; 7 + pub mod websocket; 8 + 9 + #[tokio::main] 10 + async fn main() -> Result<(), Box<dyn std::error::Error>> { 11 + let home = dirs::home_dir().unwrap(); 12 + let token_file = home.join(".rocksky").join("token.json"); 13 + 14 + if !token_file.exists() { 15 + println!( 16 + "Please run {} to authenticate with Rocksky before connecting to the WebSocket", 17 + "`rocksky login`".magenta() 18 + ); 19 + return Ok(()); 20 + } 21 + 22 + let token = std::fs::read_to_string(token_file)?; 23 + let token: serde_json::Value = serde_json::from_str(&token)?; 24 + let token = token 25 + .get("token") 26 + .and_then(|t| t.as_str()) 27 + .ok_or("Token not found")? 28 + .to_string(); 29 + 30 + thread::spawn(move || { 31 + let rt = tokio::runtime::Runtime::new().unwrap(); 32 + rt.block_on(async move { 33 + let delay = 3; 34 + 35 + loop { 36 + match connect_to_rocksky_websocket(token.clone()).await { 37 + Ok(_) => { 38 + println!("WebSocket session ended cleanly"); 39 + } 40 + Err(e) => { 41 + eprintln!("WebSocket session error: {}", e); 42 + } 43 + } 44 + 45 + println!("Reconnecting in {} seconds...", delay); 46 + tokio::time::sleep(std::time::Duration::from_secs(delay)).await; 47 + } 48 + }) 49 + }); 50 + 51 + // Keep the main thread alive to allow the WebSocket to run 52 + loop { 53 + std::thread::park(); 54 + } 55 + }
+41
crates/connect/src/players/jellyfin.rs
··· 1 + use super::Player; 2 + use async_trait::async_trait; 3 + 4 + use anyhow::Error; 5 + use tokio::sync::mpsc::Sender; 6 + pub struct JellyfinPlayer {} 7 + 8 + pub fn new() -> JellyfinPlayer { 9 + JellyfinPlayer {} 10 + } 11 + 12 + #[async_trait] 13 + impl Player for JellyfinPlayer { 14 + async fn play(&self) -> Result<(), Error> { 15 + Ok(()) 16 + } 17 + 18 + async fn pause(&self) -> Result<(), Error> { 19 + Ok(()) 20 + } 21 + 22 + async fn next(&self) -> Result<(), Error> { 23 + Ok(()) 24 + } 25 + 26 + async fn previous(&self) -> Result<(), Error> { 27 + Ok(()) 28 + } 29 + 30 + async fn seek(&self, _position: u64) -> Result<(), Error> { 31 + Ok(()) 32 + } 33 + 34 + async fn broadcast_now_playing(&self, _tx: Sender<String>) -> Result<(), Error> { 35 + Ok(()) 36 + } 37 + 38 + async fn broadcast_status(&self, _tx: Sender<String>) -> Result<(), Error> { 39 + Ok(()) 40 + } 41 + }
+246
crates/connect/src/players/kodi.rs
··· 1 + use std::{env, time::Duration}; 2 + 3 + use super::Player; 4 + use anyhow::Error; 5 + use async_trait::async_trait; 6 + use base64::{engine::general_purpose::STANDARD, Engine as _}; 7 + use jsonrpsee::{ 8 + core::{ 9 + client::ClientT, 10 + params::{ArrayParams, ObjectParams}, 11 + }, 12 + http_client::{HttpClient, HttpClientBuilder}, 13 + rpc_params, 14 + }; 15 + use reqwest::header::HeaderMap; 16 + use serde_json::{json, Value}; 17 + use tokio::sync::mpsc::Sender; 18 + 19 + #[derive(Clone)] 20 + pub struct KodiPlayer { 21 + client: HttpClient, 22 + player_id: usize, 23 + } 24 + 25 + pub fn new() -> Result<KodiPlayer, Error> { 26 + let user = env::var("KODI_USER")?; 27 + let password = env::var("KODI_PASSWORD")?; 28 + let mut headers = HeaderMap::new(); 29 + headers.insert( 30 + http::header::AUTHORIZATION, 31 + format!( 32 + "Basic {}", 33 + STANDARD.encode(format!("{}:{}", user, password).as_bytes()) 34 + ) 35 + .parse() 36 + .unwrap(), 37 + ); 38 + 39 + let kodi_url = 40 + env::var("KODI_URL").unwrap_or_else(|_| "http://localhost:8080/jsonrpc".to_string()); 41 + 42 + let client = HttpClientBuilder::default() 43 + .set_headers(headers) 44 + .build(kodi_url)?; 45 + 46 + Ok(KodiPlayer { 47 + client, 48 + player_id: 0, 49 + }) 50 + } 51 + 52 + impl KodiPlayer { 53 + pub async fn get_properties(&self, properties: Vec<&str>) -> Result<Value, Error> { 54 + let mut params = ObjectParams::new(); 55 + params.insert("properties", properties)?; 56 + 57 + let response = self 58 + .client 59 + .request::<Value, ObjectParams>("Application.GetProperties", params) 60 + .await?; 61 + Ok(response) 62 + } 63 + 64 + pub async fn get_active_players(&self) -> Result<Value, Error> { 65 + let response = self 66 + .client 67 + .request::<Value, ArrayParams>("Application.GetActivePlayers", rpc_params![]) 68 + .await?; 69 + Ok(response) 70 + } 71 + 72 + pub async fn set_player_id(&mut self, player_id: usize) -> Result<Self, Error> { 73 + self.player_id = player_id; 74 + Ok(self.clone()) 75 + } 76 + } 77 + 78 + #[async_trait] 79 + impl Player for KodiPlayer { 80 + async fn play(&self) -> Result<(), Error> { 81 + let mut params = ObjectParams::new(); 82 + params.insert("playerid", self.player_id)?; 83 + let _response = self 84 + .client 85 + .request::<Value, ObjectParams>("Player.PlayPause", params) 86 + .await?; 87 + 88 + Ok(()) 89 + } 90 + 91 + async fn pause(&self) -> Result<(), Error> { 92 + let mut params = ObjectParams::new(); 93 + params.insert("playerid", self.player_id)?; 94 + let _response = self 95 + .client 96 + .request::<Value, ObjectParams>("Player.PlayPause", params) 97 + .await?; 98 + Ok(()) 99 + } 100 + 101 + async fn next(&self) -> Result<(), Error> { 102 + let mut params = ObjectParams::new(); 103 + params.insert("playerid", self.player_id)?; 104 + params.insert("to", "next")?; 105 + let _response = self 106 + .client 107 + .request::<Value, ObjectParams>("Player.GoTo", params) 108 + .await?; 109 + Ok(()) 110 + } 111 + 112 + async fn previous(&self) -> Result<(), Error> { 113 + let mut params = ObjectParams::new(); 114 + params.insert("playerid", self.player_id)?; 115 + params.insert("to", "previous")?; 116 + let _response = self 117 + .client 118 + .request::<Value, ObjectParams>("Player.GoTo", params) 119 + .await?; 120 + Ok(()) 121 + } 122 + 123 + async fn seek(&self, position: u64) -> Result<(), Error> { 124 + let mut params = ObjectParams::new(); 125 + params.insert("playerid", self.player_id)?; 126 + params.insert("value", position)?; 127 + let _response = self 128 + .client 129 + .request::<Value, ObjectParams>("Player.Seek", params) 130 + .await?; 131 + Ok(()) 132 + } 133 + 134 + async fn broadcast_now_playing(&self, tx: Sender<String>) -> Result<(), Error> { 135 + loop { 136 + let mut params = ObjectParams::new(); 137 + params.insert("playerid", self.player_id)?; 138 + params.insert( 139 + "properties", 140 + vec!["title", "artist", "album", "duration", "file"], 141 + )?; 142 + 143 + let current_track = self 144 + .client 145 + .request::<Value, ObjectParams>("Player.GetItem", params) 146 + .await?; 147 + 148 + let mut params = ObjectParams::new(); 149 + params.insert("playerid", self.player_id)?; 150 + params.insert( 151 + "properties", 152 + vec!["time", "totaltime", "percentage", "speed"], 153 + )?; 154 + 155 + let progress = self 156 + .client 157 + .request::<Value, ObjectParams>("Player.GetProperties", params) 158 + .await?; 159 + 160 + println!("{:#?}", progress); 161 + 162 + let hours = progress 163 + .get("time") 164 + .and_then(|time| time.get("hours")) 165 + .and_then(Value::as_u64) 166 + .unwrap_or(0); 167 + let minutes = progress 168 + .get("time") 169 + .and_then(|time| time.get("minutes")) 170 + .and_then(Value::as_u64) 171 + .unwrap_or(0); 172 + let seconds = progress 173 + .get("time") 174 + .and_then(|time| time.get("seconds")) 175 + .and_then(Value::as_u64) 176 + .unwrap_or(0); 177 + let milliseconds = progress 178 + .get("time") 179 + .and_then(|time| time.get("milliseconds")) 180 + .and_then(Value::as_u64) 181 + .unwrap_or(0); 182 + 183 + tx.send( 184 + json!({ 185 + "type": "track", 186 + "title": current_track 187 + .get("item") 188 + .and_then(|item| item.get("title")) 189 + .and_then(Value::as_str) 190 + .unwrap_or("Unknown Title"), 191 + "artist": current_track 192 + .get("item") 193 + .and_then(|item| item.get("artist")) 194 + .and_then(Value::as_array) 195 + .map(|arr| arr.iter().map(Value::as_str).map(|x| x.unwrap()).collect::<Vec<_>>().join(", ")) 196 + .unwrap_or("Unknown Artist".into()), 197 + // "album_artist": "", 198 + "album": current_track 199 + .get("item") 200 + .and_then(|item| item.get("album")) 201 + .and_then(Value::as_str) 202 + .unwrap_or("Unknown Album"), 203 + "length": current_track 204 + .get("item") 205 + .and_then(|item| item.get("duration")) 206 + .and_then(Value::as_u64) 207 + .unwrap_or(0) * 1000, // Convert to milliseconds 208 + "elapsed": ((hours * 3600) + (minutes * 60) + seconds) * 1000 + milliseconds, 209 + }) 210 + .to_string(), 211 + ).await?; 212 + 213 + tokio::time::sleep(Duration::from_secs(3)).await; 214 + } 215 + } 216 + 217 + async fn broadcast_status(&self, tx: Sender<String>) -> Result<(), Error> { 218 + loop { 219 + let mut params = ObjectParams::new(); 220 + params.insert("playerid", self.player_id).unwrap(); 221 + params.insert("properties", vec!["speed"]).unwrap(); 222 + 223 + let response = self 224 + .client 225 + .request::<Value, ObjectParams>("Player.GetProperties", params) 226 + .await?; 227 + 228 + tx.send( 229 + json!({ 230 + "type": "status", 231 + "status": match response.get("speed") { 232 + Some(Value::Number(speed)) => match speed.as_i64() { 233 + Some(0) => 2, 234 + Some(_) => 1, 235 + None => 2, 236 + }, 237 + _ => 2, 238 + }, 239 + }) 240 + .to_string(), 241 + ) 242 + .await?; 243 + tokio::time::sleep(Duration::from_secs(3)).await; 244 + } 245 + } 246 + }
+49
crates/connect/src/players/mod.rs
··· 1 + use anyhow::Error; 2 + use async_trait::async_trait; 3 + use owo_colors::OwoColorize; 4 + use tokio::sync::mpsc::Sender; 5 + 6 + pub mod jellyfin; 7 + pub mod kodi; 8 + pub mod mopidy; 9 + pub mod mpd; 10 + pub mod mpris; 11 + pub mod vlc; 12 + 13 + pub const SUPPORTED_PLAYERS: [&str; 6] = ["jellyfin", "kodi", "mopidy", "mpd", "mpris", "vlc"]; 14 + 15 + #[async_trait] 16 + pub trait Player { 17 + async fn play(&self) -> Result<(), Error>; 18 + async fn pause(&self) -> Result<(), Error>; 19 + async fn next(&self) -> Result<(), Error>; 20 + async fn previous(&self) -> Result<(), Error>; 21 + async fn seek(&self, position: u64) -> Result<(), Error>; 22 + async fn broadcast_now_playing(&self, tx: Sender<String>) -> Result<(), Error>; 23 + async fn broadcast_status(&self, tx: Sender<String>) -> Result<(), Error>; 24 + } 25 + 26 + pub fn get_current_player() -> Result<Box<dyn Player + Send + Sync>, Error> { 27 + let player_type = std::env::var("ROCKSKY_PLAYER"); 28 + if player_type.is_err() { 29 + return Err(Error::msg(format!( 30 + "{} environment variable not set", 31 + "ROCKSKY_PLAYER".green() 32 + ))); 33 + } 34 + 35 + let player_type = player_type.unwrap(); 36 + 37 + match player_type.as_str() { 38 + "jellyfin" => Ok(Box::new(jellyfin::new())), 39 + "kodi" => Ok(Box::new(kodi::new()?)), 40 + "mopidy" => Ok(Box::new(mopidy::new())), 41 + "mpd" => Ok(Box::new(mpd::new())), 42 + "mpris" => Ok(Box::new(mpris::new())), 43 + "vlc" => Ok(Box::new(vlc::new())), 44 + _ => Err(Error::msg(format!( 45 + "Unsupported player type: {}", 46 + player_type.magenta() 47 + ))), 48 + } 49 + }
+41
crates/connect/src/players/mopidy.rs
··· 1 + use super::Player; 2 + use anyhow::Error; 3 + use async_trait::async_trait; 4 + use tokio::sync::mpsc::Sender; 5 + 6 + pub struct MopidyPlayer {} 7 + 8 + pub fn new() -> MopidyPlayer { 9 + MopidyPlayer {} 10 + } 11 + 12 + #[async_trait] 13 + impl Player for MopidyPlayer { 14 + async fn play(&self) -> Result<(), Error> { 15 + Ok(()) 16 + } 17 + 18 + async fn pause(&self) -> Result<(), Error> { 19 + Ok(()) 20 + } 21 + 22 + async fn next(&self) -> Result<(), Error> { 23 + Ok(()) 24 + } 25 + 26 + async fn previous(&self) -> Result<(), Error> { 27 + Ok(()) 28 + } 29 + 30 + async fn seek(&self, _position: u64) -> Result<(), Error> { 31 + Ok(()) 32 + } 33 + 34 + async fn broadcast_now_playing(&self, _tx: Sender<String>) -> Result<(), Error> { 35 + Ok(()) 36 + } 37 + 38 + async fn broadcast_status(&self, _tx: Sender<String>) -> Result<(), Error> { 39 + Ok(()) 40 + } 41 + }
+41
crates/connect/src/players/mpd.rs
··· 1 + use super::Player; 2 + use anyhow::Error; 3 + use async_trait::async_trait; 4 + use tokio::sync::mpsc::Sender; 5 + 6 + pub struct MpdPlayer {} 7 + 8 + pub fn new() -> MpdPlayer { 9 + MpdPlayer {} 10 + } 11 + 12 + #[async_trait] 13 + impl Player for MpdPlayer { 14 + async fn play(&self) -> Result<(), Error> { 15 + Ok(()) 16 + } 17 + 18 + async fn pause(&self) -> Result<(), Error> { 19 + Ok(()) 20 + } 21 + 22 + async fn next(&self) -> Result<(), Error> { 23 + Ok(()) 24 + } 25 + 26 + async fn previous(&self) -> Result<(), Error> { 27 + Ok(()) 28 + } 29 + 30 + async fn seek(&self, _position: u64) -> Result<(), Error> { 31 + Ok(()) 32 + } 33 + 34 + async fn broadcast_now_playing(&self, _tx: Sender<String>) -> Result<(), Error> { 35 + Ok(()) 36 + } 37 + 38 + async fn broadcast_status(&self, _tx: Sender<String>) -> Result<(), Error> { 39 + Ok(()) 40 + } 41 + }
+41
crates/connect/src/players/mpris.rs
··· 1 + use super::Player; 2 + use anyhow::Error; 3 + use async_trait::async_trait; 4 + use tokio::sync::mpsc::Sender; 5 + 6 + pub struct MprisPlayer {} 7 + 8 + pub fn new() -> MprisPlayer { 9 + MprisPlayer {} 10 + } 11 + 12 + #[async_trait] 13 + impl Player for MprisPlayer { 14 + async fn play(&self) -> Result<(), Error> { 15 + Ok(()) 16 + } 17 + 18 + async fn pause(&self) -> Result<(), Error> { 19 + Ok(()) 20 + } 21 + 22 + async fn next(&self) -> Result<(), Error> { 23 + Ok(()) 24 + } 25 + 26 + async fn previous(&self) -> Result<(), Error> { 27 + Ok(()) 28 + } 29 + 30 + async fn seek(&self, _position: u64) -> Result<(), Error> { 31 + Ok(()) 32 + } 33 + 34 + async fn broadcast_now_playing(&self, _tx: Sender<String>) -> Result<(), Error> { 35 + Ok(()) 36 + } 37 + 38 + async fn broadcast_status(&self, _tx: Sender<String>) -> Result<(), Error> { 39 + Ok(()) 40 + } 41 + }
+41
crates/connect/src/players/vlc.rs
··· 1 + use super::Player; 2 + use anyhow::Error; 3 + use async_trait::async_trait; 4 + use tokio::sync::mpsc::Sender; 5 + 6 + pub struct VlcPlayer {} 7 + 8 + pub fn new() -> VlcPlayer { 9 + VlcPlayer {} 10 + } 11 + 12 + #[async_trait] 13 + impl Player for VlcPlayer { 14 + async fn play(&self) -> Result<(), Error> { 15 + Ok(()) 16 + } 17 + 18 + async fn pause(&self) -> Result<(), Error> { 19 + Ok(()) 20 + } 21 + 22 + async fn next(&self) -> Result<(), Error> { 23 + Ok(()) 24 + } 25 + 26 + async fn previous(&self) -> Result<(), Error> { 27 + Ok(()) 28 + } 29 + 30 + async fn seek(&self, _position: u64) -> Result<(), Error> { 31 + Ok(()) 32 + } 33 + 34 + async fn broadcast_now_playing(&self, _tx: Sender<String>) -> Result<(), Error> { 35 + Ok(()) 36 + } 37 + 38 + async fn broadcast_status(&self, _tx: Sender<String>) -> Result<(), Error> { 39 + Ok(()) 40 + } 41 + }
+130
crates/connect/src/websocket.rs
··· 1 + use std::{env, sync::Arc}; 2 + 3 + use anyhow::Error; 4 + use futures_util::{SinkExt, StreamExt}; 5 + use owo_colors::OwoColorize; 6 + use serde_json::{json, Value}; 7 + use tokio::sync::Mutex; 8 + use tokio_tungstenite::connect_async; 9 + 10 + use crate::players::{get_current_player, Player}; 11 + 12 + pub async fn connect_to_rocksky_websocket(token: String) -> Result<(), Error> { 13 + let rocksky_ws = 14 + env::var("ROCKSKY_WS").unwrap_or_else(|_| "wss://api.rocksky.app/ws".to_string()); 15 + let (ws_stream, _) = connect_async(&rocksky_ws).await?; 16 + println!("Connected to {}", rocksky_ws); 17 + 18 + let (mut write, mut read) = ws_stream.split(); 19 + let device_id = Arc::new(Mutex::new(String::new())); 20 + 21 + write 22 + .send( 23 + json!({ 24 + "type": "register", 25 + "clientName": "Rockbox", 26 + "token": token 27 + }) 28 + .to_string() 29 + .into(), 30 + ) 31 + .await?; 32 + 33 + let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(32); 34 + let tx_clone = tx.clone(); 35 + 36 + tokio::spawn(async move { 37 + let player: Box<dyn Player + Send + Sync> = get_current_player().map_err(|err| { 38 + println!("Error getting current player: {}", err); 39 + err 40 + })?; 41 + player 42 + .broadcast_now_playing(tx_clone) 43 + .await 44 + .unwrap_or_else(|err| eprintln!("Error broadcasting now playing: {}", err)); 45 + Ok::<(), Error>(()) 46 + }); 47 + 48 + tokio::spawn(async move { 49 + let player: Box<dyn Player + Send + Sync> = get_current_player().map_err(|err| { 50 + println!("Error getting current player: {}", err); 51 + err 52 + })?; 53 + player 54 + .broadcast_status(tx) 55 + .await 56 + .unwrap_or_else(|err| eprintln!("Error broadcasting status: {}", err)); 57 + Ok::<(), Error>(()) 58 + }); 59 + 60 + { 61 + let device_id = Arc::clone(&device_id); 62 + let token = token.clone(); 63 + tokio::spawn(async move { 64 + while let Some(msg) = rx.recv().await { 65 + println!("Sending message: {}", msg); 66 + let id = device_id.lock().await.clone(); 67 + if let Err(err) = write 68 + .send( 69 + json!({ 70 + "type": "message", 71 + "data": serde_json::from_str::<Value>(&msg).unwrap(), 72 + "device_id": id, 73 + "token": token 74 + }) 75 + .to_string() 76 + .into(), 77 + ) 78 + .await 79 + { 80 + eprintln!("Send error: {}", err); 81 + break; 82 + } 83 + } 84 + }); 85 + } 86 + 87 + while let Some(msg) = read.next().await { 88 + let msg = match msg { 89 + Ok(m) => m.to_string(), 90 + Err(e) => { 91 + eprintln!("Read error: {}", e); 92 + break; 93 + } 94 + }; 95 + 96 + let msg: Value = serde_json::from_str(&msg)?; 97 + if let Some(id) = msg["deviceId"].as_str() { 98 + println!("Device ID: {}", id); 99 + *device_id.lock().await = id.to_string(); 100 + } 101 + 102 + if let Some("command") = msg["type"].as_str() { 103 + if let Some(cmd) = msg["action"].as_str() { 104 + println!("Received command: {}", cmd); 105 + 106 + let player: Box<dyn Player> = get_current_player()?; 107 + 108 + if let Some("command") = msg["type"].as_str() { 109 + if let Some(cmd) = msg["action"].as_str() { 110 + match cmd { 111 + "play" => player.play().await?, 112 + "pause" => player.pause().await?, 113 + "next" => player.next().await?, 114 + "previous" => player.previous().await?, 115 + "seek" => player.seek(msg["position"].as_u64().unwrap_or(0)).await?, 116 + _ => { 117 + eprintln!("Unknown command: {}", cmd.magenta()); 118 + continue; 119 + } 120 + } 121 + } else { 122 + println!("No action specified in command message, ignoring."); 123 + } 124 + } 125 + } 126 + } 127 + } 128 + 129 + Ok(()) 130 + }
+89 -80
crates/dropbox/src/client.rs
··· 5 5 use reqwest::Client; 6 6 use serde_json::json; 7 7 8 - use crate::types::{file::{Entry, EntryList, TemporaryLink}, token::AccessToken}; 8 + use crate::types::{ 9 + file::{Entry, EntryList, TemporaryLink}, 10 + token::AccessToken, 11 + }; 9 12 10 13 pub const BASE_URL: &str = "https://api.dropboxapi.com/2"; 11 14 pub const CONTENT_URL: &str = "https://content.dropboxapi.com/2"; 12 15 13 16 pub async fn get_access_token(refresh_token: &str) -> Result<AccessToken, Error> { 14 - let client = Client::new(); 15 - let res = client.post("https://api.dropboxapi.com/oauth2/token") 16 - .header("Content-Type", "application/x-www-form-urlencoded") 17 - .query(&[ 18 - ("grant_type", "refresh_token"), 19 - ("refresh_token", refresh_token), 20 - ("client_id", &env::var("DROPBOX_CLIENT_ID")?), 21 - ("client_secret", &env::var("DROPBOX_CLIENT_SECRET")?), 22 - ]) 23 - .send() 24 - .await?; 17 + let client = Client::new(); 18 + let res = client 19 + .post("https://api.dropboxapi.com/oauth2/token") 20 + .header("Content-Type", "application/x-www-form-urlencoded") 21 + .query(&[ 22 + ("grant_type", "refresh_token"), 23 + ("refresh_token", refresh_token), 24 + ("client_id", &env::var("DROPBOX_CLIENT_ID")?), 25 + ("client_secret", &env::var("DROPBOX_CLIENT_SECRET")?), 26 + ]) 27 + .send() 28 + .await?; 25 29 26 - Ok(res.json::<AccessToken>().await?) 30 + Ok(res.json::<AccessToken>().await?) 27 31 } 28 32 29 33 pub struct DropboxClient { 30 - pub access_token: String, 34 + pub access_token: String, 31 35 } 32 36 33 37 impl DropboxClient { 34 - pub async fn new(refresh_token: &str) -> Result<Self, Error> { 35 - let res = get_access_token(refresh_token).await?; 36 - Ok(DropboxClient { 37 - access_token: res.access_token, 38 - }) 39 - } 38 + pub async fn new(refresh_token: &str) -> Result<Self, Error> { 39 + let res = get_access_token(refresh_token).await?; 40 + Ok(DropboxClient { 41 + access_token: res.access_token, 42 + }) 43 + } 40 44 41 - pub async fn create_music_folder(&self) -> Result<(), Error> { 42 - let client = Client::new(); 43 - client.post(&format!("{}/files/create_folder_v2", BASE_URL)) 44 - .bearer_auth(&self.access_token) 45 - .json(&json!({ "path": "/Music" })) 46 - .send() 47 - .await?; 45 + pub async fn create_music_folder(&self) -> Result<(), Error> { 46 + let client = Client::new(); 47 + client 48 + .post(&format!("{}/files/create_folder_v2", BASE_URL)) 49 + .bearer_auth(&self.access_token) 50 + .json(&json!({ "path": "/Music" })) 51 + .send() 52 + .await?; 48 53 49 - Ok(()) 50 - } 54 + Ok(()) 55 + } 51 56 52 - pub async fn get_metadata(&self, path: &str) -> Result<Entry, Error> { 53 - let client = Client::new(); 54 - let res = client.post(&format!("{}/files/get_metadata", BASE_URL)) 55 - .bearer_auth(&self.access_token) 56 - .json(&json!({ "path": path })) 57 - .send() 58 - .await?; 57 + pub async fn get_metadata(&self, path: &str) -> Result<Entry, Error> { 58 + let client = Client::new(); 59 + let res = client 60 + .post(&format!("{}/files/get_metadata", BASE_URL)) 61 + .bearer_auth(&self.access_token) 62 + .json(&json!({ "path": path })) 63 + .send() 64 + .await?; 59 65 60 - Ok(res.json::<Entry>().await?) 61 - } 66 + Ok(res.json::<Entry>().await?) 67 + } 62 68 63 - pub async fn get_files(&self, path: &str) -> Result<EntryList, Error> { 64 - let client = Client::new(); 65 - let res = client.post(&format!("{}/files/list_folder", BASE_URL)) 66 - .bearer_auth(&self.access_token) 67 - .json(&json!({ 68 - "path": path, 69 - "recursive": false, 70 - "include_media_info": true, 71 - "include_deleted": false, 72 - "include_has_explicit_shared_members": false, 73 - "include_mounted_folders": true, 74 - "include_non_downloadable_files": true, 75 - })) 76 - .send() 77 - .await?; 69 + pub async fn get_files(&self, path: &str) -> Result<EntryList, Error> { 70 + let client = Client::new(); 71 + let res = client 72 + .post(&format!("{}/files/list_folder", BASE_URL)) 73 + .bearer_auth(&self.access_token) 74 + .json(&json!({ 75 + "path": path, 76 + "recursive": false, 77 + "include_media_info": true, 78 + "include_deleted": false, 79 + "include_has_explicit_shared_members": false, 80 + "include_mounted_folders": true, 81 + "include_non_downloadable_files": true, 82 + })) 83 + .send() 84 + .await?; 78 85 79 - Ok(res.json::<EntryList>().await?) 80 - } 86 + Ok(res.json::<EntryList>().await?) 87 + } 81 88 82 - pub async fn download_file(&self, path: &str) -> Result<HttpResponse, Error> { 83 - let client = Client::new(); 84 - let res = client.post(&format!("{}/files/download", CONTENT_URL)) 85 - .bearer_auth(&self.access_token) 86 - .header("Dropbox-API-Arg", &json!({ "path": path }).to_string()) 87 - .send() 88 - .await?; 89 + pub async fn download_file(&self, path: &str) -> Result<HttpResponse, Error> { 90 + let client = Client::new(); 91 + let res = client 92 + .post(&format!("{}/files/download", CONTENT_URL)) 93 + .bearer_auth(&self.access_token) 94 + .header("Dropbox-API-Arg", &json!({ "path": path }).to_string()) 95 + .send() 96 + .await?; 89 97 90 - let mut actix_response = HttpResponse::Ok(); 98 + let mut actix_response = HttpResponse::Ok(); 91 99 92 - // Forward headers 93 - for (key, value) in res.headers().iter() { 94 - actix_response.append_header((key.as_str(), value.to_str().unwrap_or(""))); 95 - } 100 + // Forward headers 101 + for (key, value) in res.headers().iter() { 102 + actix_response.append_header((key.as_str(), value.to_str().unwrap_or(""))); 103 + } 96 104 97 - // Forward body 98 - let body = res.bytes_stream(); 105 + // Forward body 106 + let body = res.bytes_stream(); 99 107 100 - Ok(actix_response.streaming(body)) 101 - } 108 + Ok(actix_response.streaming(body)) 109 + } 102 110 103 - pub async fn get_temporary_link(&self, path: &str) -> Result<TemporaryLink, Error> { 104 - let client = Client::new(); 105 - let res = client.post(&format!("{}/files/get_temporary_link", BASE_URL)) 106 - .bearer_auth(&self.access_token) 107 - .json(&json!({ "path": path })) 108 - .send() 109 - .await?; 111 + pub async fn get_temporary_link(&self, path: &str) -> Result<TemporaryLink, Error> { 112 + let client = Client::new(); 113 + let res = client 114 + .post(&format!("{}/files/get_temporary_link", BASE_URL)) 115 + .bearer_auth(&self.access_token) 116 + .json(&json!({ "path": path })) 117 + .send() 118 + .await?; 110 119 111 - Ok(res.json::<TemporaryLink>().await?) 112 - } 120 + Ok(res.json::<TemporaryLink>().await?) 121 + } 113 122 }
+7 -4
crates/dropbox/src/cmd/scan.rs
··· 6 6 use crate::scan::scan_dropbox; 7 7 8 8 pub async fn scan() -> Result<(), Error> { 9 - let pool = PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; 10 - let conn = Arc::new(pool); 9 + let pool = PgPoolOptions::new() 10 + .max_connections(5) 11 + .connect(&env::var("XATA_POSTGRES_URL")?) 12 + .await?; 13 + let conn = Arc::new(pool); 11 14 12 - scan_dropbox(conn).await?; 15 + scan_dropbox(conn).await?; 13 16 14 - Ok(()) 17 + Ok(()) 15 18 }
-1
crates/dropbox/src/cmd/serve.rs
··· 63 63 64 64 Ok(()) 65 65 } 66 -
+129 -98
crates/dropbox/src/handlers/files.rs
··· 6 6 use tokio_stream::StreamExt; 7 7 8 8 use crate::{ 9 - client::DropboxClient, crypto::decrypt_aes_256_ctr, read_payload, repo::dropbox_token::find_dropbox_refresh_token, scan, types::file::{DownloadFileParams, GetFilesAtParams, GetFilesParams, ScanFolderParams} 9 + client::DropboxClient, 10 + crypto::decrypt_aes_256_ctr, 11 + read_payload, 12 + repo::dropbox_token::find_dropbox_refresh_token, 13 + scan, 14 + types::file::{DownloadFileParams, GetFilesAtParams, GetFilesParams, ScanFolderParams}, 10 15 }; 11 16 12 17 pub const MUSIC_DIR: &str = "/Music"; 13 18 14 - pub async fn get_files(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 15 - let body = read_payload!(payload); 16 - let params = serde_json::from_slice::<GetFilesParams>(&body)?; 17 - let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 19 + pub async fn get_files( 20 + payload: &mut web::Payload, 21 + _req: &HttpRequest, 22 + pool: Arc<Pool<Postgres>>, 23 + ) -> Result<HttpResponse, Error> { 24 + let body = read_payload!(payload); 25 + let params = serde_json::from_slice::<GetFilesParams>(&body)?; 26 + let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 18 27 19 - if refresh_token.is_none() { 20 - return Ok(HttpResponse::Unauthorized().finish()); 21 - } 28 + if refresh_token.is_none() { 29 + return Ok(HttpResponse::Unauthorized().finish()); 30 + } 22 31 23 - let refresh_token = decrypt_aes_256_ctr( 24 - &refresh_token.unwrap().0, 25 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 26 - )?; 32 + let refresh_token = decrypt_aes_256_ctr( 33 + &refresh_token.unwrap().0, 34 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 35 + )?; 27 36 28 - let client = DropboxClient::new(&refresh_token).await?; 29 - let entries = client.get_files(MUSIC_DIR).await?; 37 + let client = DropboxClient::new(&refresh_token).await?; 38 + let entries = client.get_files(MUSIC_DIR).await?; 30 39 31 - Ok(HttpResponse::Ok().json(web::Json(entries))) 40 + Ok(HttpResponse::Ok().json(web::Json(entries))) 32 41 } 33 42 34 - 35 - pub async fn create_music_folder(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 36 - let body = read_payload!(payload); 37 - let params = serde_json::from_slice::<GetFilesParams>(&body)?; 38 - let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 43 + pub async fn create_music_folder( 44 + payload: &mut web::Payload, 45 + _req: &HttpRequest, 46 + pool: Arc<Pool<Postgres>>, 47 + ) -> Result<HttpResponse, Error> { 48 + let body = read_payload!(payload); 49 + let params = serde_json::from_slice::<GetFilesParams>(&body)?; 50 + let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 39 51 40 - if refresh_token.is_none() { 41 - return Ok(HttpResponse::Unauthorized().finish()); 42 - } 52 + if refresh_token.is_none() { 53 + return Ok(HttpResponse::Unauthorized().finish()); 54 + } 43 55 44 - let refresh_token = decrypt_aes_256_ctr( 45 - &refresh_token.unwrap().0, 46 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 47 - )?; 56 + let refresh_token = decrypt_aes_256_ctr( 57 + &refresh_token.unwrap().0, 58 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 59 + )?; 48 60 49 - let client = DropboxClient::new(&refresh_token).await?; 50 - client.create_music_folder().await?; 61 + let client = DropboxClient::new(&refresh_token).await?; 62 + client.create_music_folder().await?; 51 63 52 - Ok(HttpResponse::Ok().finish()) 64 + Ok(HttpResponse::Ok().finish()) 53 65 } 54 66 55 - pub async fn get_files_at(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 56 - let body = read_payload!(payload); 57 - let params = serde_json::from_slice::<GetFilesAtParams>(&body)?; 58 - let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 67 + pub async fn get_files_at( 68 + payload: &mut web::Payload, 69 + _req: &HttpRequest, 70 + pool: Arc<Pool<Postgres>>, 71 + ) -> Result<HttpResponse, Error> { 72 + let body = read_payload!(payload); 73 + let params = serde_json::from_slice::<GetFilesAtParams>(&body)?; 74 + let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 59 75 60 - if refresh_token.is_none() { 61 - return Ok(HttpResponse::Unauthorized().finish()); 62 - } 76 + if refresh_token.is_none() { 77 + return Ok(HttpResponse::Unauthorized().finish()); 78 + } 63 79 64 - let refresh_token = decrypt_aes_256_ctr( 65 - &refresh_token.unwrap().0, 66 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 67 - )?; 80 + let refresh_token = decrypt_aes_256_ctr( 81 + &refresh_token.unwrap().0, 82 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 83 + )?; 68 84 69 - let client = DropboxClient::new(&refresh_token).await?; 70 - let entries = client.get_files(&params.path).await?; 85 + let client = DropboxClient::new(&refresh_token).await?; 86 + let entries = client.get_files(&params.path).await?; 71 87 72 - Ok(HttpResponse::Ok().json(web::Json(entries))) 88 + Ok(HttpResponse::Ok().json(web::Json(entries))) 73 89 } 74 90 75 - pub async fn download_file(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 76 - let body = read_payload!(payload); 77 - let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 78 - let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 91 + pub async fn download_file( 92 + payload: &mut web::Payload, 93 + _req: &HttpRequest, 94 + pool: Arc<Pool<Postgres>>, 95 + ) -> Result<HttpResponse, Error> { 96 + let body = read_payload!(payload); 97 + let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 98 + let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 79 99 80 - if refresh_token.is_none() { 81 - return Ok(HttpResponse::Unauthorized().finish()); 82 - } 100 + if refresh_token.is_none() { 101 + return Ok(HttpResponse::Unauthorized().finish()); 102 + } 83 103 84 - let refresh_token = decrypt_aes_256_ctr( 85 - &refresh_token.unwrap().0, 86 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 87 - )?; 104 + let refresh_token = decrypt_aes_256_ctr( 105 + &refresh_token.unwrap().0, 106 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 107 + )?; 88 108 89 - let client = DropboxClient::new(&refresh_token).await?; 90 - client.download_file(&params.path).await 109 + let client = DropboxClient::new(&refresh_token).await?; 110 + client.download_file(&params.path).await 91 111 } 92 112 93 - pub async fn get_temporary_link(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 94 - let body = read_payload!(payload); 95 - let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 96 - let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 113 + pub async fn get_temporary_link( 114 + payload: &mut web::Payload, 115 + _req: &HttpRequest, 116 + pool: Arc<Pool<Postgres>>, 117 + ) -> Result<HttpResponse, Error> { 118 + let body = read_payload!(payload); 119 + let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 120 + let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 97 121 98 - if refresh_token.is_none() { 99 - return Ok(HttpResponse::Unauthorized().finish()); 100 - } 122 + if refresh_token.is_none() { 123 + return Ok(HttpResponse::Unauthorized().finish()); 124 + } 101 125 102 - let refresh_token = decrypt_aes_256_ctr( 103 - &refresh_token.unwrap().0, 104 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 105 - )?; 126 + let refresh_token = decrypt_aes_256_ctr( 127 + &refresh_token.unwrap().0, 128 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 129 + )?; 106 130 107 - let client = DropboxClient::new(&refresh_token).await?; 108 - let temporary_link = client.get_temporary_link(&params.path).await?; 131 + let client = DropboxClient::new(&refresh_token).await?; 132 + let temporary_link = client.get_temporary_link(&params.path).await?; 109 133 110 - Ok(HttpResponse::Ok().json(web::Json(temporary_link))) 134 + Ok(HttpResponse::Ok().json(web::Json(temporary_link))) 111 135 } 112 136 113 - 114 - pub async fn get_metadata(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 115 - let body = read_payload!(payload); 116 - let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 117 - let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 137 + pub async fn get_metadata( 138 + payload: &mut web::Payload, 139 + _req: &HttpRequest, 140 + pool: Arc<Pool<Postgres>>, 141 + ) -> Result<HttpResponse, Error> { 142 + let body = read_payload!(payload); 143 + let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 144 + let refresh_token = find_dropbox_refresh_token(&pool.clone(), &params.did).await?; 118 145 119 - if refresh_token.is_none() { 120 - return Ok(HttpResponse::Unauthorized().finish()); 121 - } 146 + if refresh_token.is_none() { 147 + return Ok(HttpResponse::Unauthorized().finish()); 148 + } 122 149 123 - let refresh_token = decrypt_aes_256_ctr( 124 - &refresh_token.unwrap().0, 125 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 126 - )?; 150 + let refresh_token = decrypt_aes_256_ctr( 151 + &refresh_token.unwrap().0, 152 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 153 + )?; 127 154 128 - let client = DropboxClient::new(&refresh_token).await?; 129 - let metadata = client.get_metadata(&params.path).await?; 155 + let client = DropboxClient::new(&refresh_token).await?; 156 + let metadata = client.get_metadata(&params.path).await?; 130 157 131 - Ok(HttpResponse::Ok().json(web::Json(metadata))) 158 + Ok(HttpResponse::Ok().json(web::Json(metadata))) 132 159 } 133 160 134 - pub async fn scan_folder(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 135 - let body = read_payload!(payload); 136 - let params = serde_json::from_slice::<ScanFolderParams>(&body)?; 161 + pub async fn scan_folder( 162 + payload: &mut web::Payload, 163 + _req: &HttpRequest, 164 + pool: Arc<Pool<Postgres>>, 165 + ) -> Result<HttpResponse, Error> { 166 + let body = read_payload!(payload); 167 + let params = serde_json::from_slice::<ScanFolderParams>(&body)?; 137 168 138 - let pool = pool.clone(); 139 - thread::spawn(move || { 140 - let rt = tokio::runtime::Runtime::new().unwrap(); 141 - rt.block_on(scan::scan_folder(pool, &params.did, &params.path))?; 142 - Ok::<(), Error>(()) 143 - }); 169 + let pool = pool.clone(); 170 + thread::spawn(move || { 171 + let rt = tokio::runtime::Runtime::new().unwrap(); 172 + rt.block_on(scan::scan_folder(pool, &params.did, &params.path))?; 173 + Ok::<(), Error>(()) 174 + }); 144 175 145 - // sleep for 2 second to allow the thread to start 146 - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 176 + // sleep for 2 second to allow the thread to start 177 + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 147 178 148 - Ok(HttpResponse::Ok().finish()) 179 + Ok(HttpResponse::Ok().finish()) 149 180 }
+30 -22
crates/dropbox/src/handlers/mod.rs
··· 2 2 3 3 use actix_web::{web, HttpRequest, HttpResponse}; 4 4 use anyhow::Error; 5 - use files::{create_music_folder, download_file, get_files, get_files_at, get_metadata, get_temporary_link, scan_folder}; 5 + use files::{ 6 + create_music_folder, download_file, get_files, get_files_at, get_metadata, get_temporary_link, 7 + scan_folder, 8 + }; 6 9 use sqlx::{Pool, Postgres}; 7 10 8 11 pub mod files; 9 12 10 13 #[macro_export] 11 14 macro_rules! read_payload { 12 - ($payload:expr) => {{ 13 - let mut body = Vec::new(); 14 - while let Some(chunk) = $payload.next().await { 15 - match chunk { 16 - Ok(bytes) => body.extend_from_slice(&bytes), 17 - Err(err) => return Err(err.into()), 18 - } 19 - } 20 - body 21 - }}; 15 + ($payload:expr) => {{ 16 + let mut body = Vec::new(); 17 + while let Some(chunk) = $payload.next().await { 18 + match chunk { 19 + Ok(bytes) => body.extend_from_slice(&bytes), 20 + Err(err) => return Err(err.into()), 21 + } 22 + } 23 + body 24 + }}; 22 25 } 23 26 24 - pub async fn handle(method: &str, payload: &mut web::Payload, req: &HttpRequest, conn: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 25 - match method { 26 - "dropbox.getFiles" => get_files(payload, req, conn.clone()).await, 27 - "dropbox.createMusicFolder" => create_music_folder(payload, req, conn.clone()).await, 28 - "dropbox.getFilesAt" => get_files_at(payload, req, conn.clone()).await, 29 - "dropbox.downloadFile" => download_file(payload, req, conn.clone()).await, 30 - "dropbox.getTemporaryLink" => get_temporary_link(payload, req, conn.clone()).await, 31 - "dropbox.getMetadata" => get_metadata(payload, req, conn.clone()).await, 32 - "dropbox.scanFolder" => scan_folder(payload, req, conn.clone()).await, 33 - _ => return Err(anyhow::anyhow!("Method not found")), 34 - } 27 + pub async fn handle( 28 + method: &str, 29 + payload: &mut web::Payload, 30 + req: &HttpRequest, 31 + conn: Arc<Pool<Postgres>>, 32 + ) -> Result<HttpResponse, Error> { 33 + match method { 34 + "dropbox.getFiles" => get_files(payload, req, conn.clone()).await, 35 + "dropbox.createMusicFolder" => create_music_folder(payload, req, conn.clone()).await, 36 + "dropbox.getFilesAt" => get_files_at(payload, req, conn.clone()).await, 37 + "dropbox.downloadFile" => download_file(payload, req, conn.clone()).await, 38 + "dropbox.getTemporaryLink" => get_temporary_link(payload, req, conn.clone()).await, 39 + "dropbox.getMetadata" => get_metadata(payload, req, conn.clone()).await, 40 + "dropbox.scanFolder" => scan_folder(payload, req, conn.clone()).await, 41 + _ => return Err(anyhow::anyhow!("Method not found")), 42 + } 35 43 }
+9 -15
crates/dropbox/src/main.rs
··· 1 1 use clap::Command; 2 - use cmd::{serve::serve, scan::scan}; 2 + use cmd::{scan::scan, serve::serve}; 3 3 use dotenv::dotenv; 4 4 5 - pub mod types; 6 - pub mod xata; 5 + pub mod client; 7 6 pub mod cmd; 7 + pub mod consts; 8 + pub mod crypto; 8 9 pub mod handlers; 9 10 pub mod repo; 10 - pub mod client; 11 - pub mod crypto; 11 + pub mod scan; 12 12 pub mod token; 13 - pub mod consts; 14 - pub mod scan; 13 + pub mod types; 14 + pub mod xata; 15 15 16 16 fn cli() -> Command { 17 17 Command::new("dropbox") 18 18 .version(env!("CARGO_PKG_VERSION")) 19 19 .about("Rocksky Dropbox Service") 20 - .subcommand( 21 - Command::new("scan") 22 - .about("Scan Dropbox Music Folder") 23 - ) 24 - .subcommand( 25 - Command::new("serve") 26 - .about("Serve Rocksky Dropbox API") 27 - ) 20 + .subcommand(Command::new("scan").about("Scan Dropbox Music Folder")) 21 + .subcommand(Command::new("serve").about("Serve Rocksky Dropbox API")) 28 22 } 29 23 30 24 #[tokio::main]
+11 -9
crates/dropbox/src/repo/dropbox_path.rs
··· 1 1 use sqlx::{Pool, Postgres}; 2 2 3 - use crate::xata::track::Track; 4 3 use crate::types::file::Entry; 4 + use crate::xata::track::Track; 5 5 6 6 pub async fn create_dropbox_path( 7 - pool: &Pool<Postgres>, 8 - file: &Entry, 9 - track: &Track, 10 - dropbox_id: &str 7 + pool: &Pool<Postgres>, 8 + file: &Entry, 9 + track: &Track, 10 + dropbox_id: &str, 11 11 ) -> Result<(), sqlx::Error> { 12 - sqlx::query(r#" 12 + sqlx::query( 13 + r#" 13 14 INSERT INTO dropbox_paths (dropbox_id, path, file_id, track_id, name) 14 15 VALUES ($1, $2, $3, $4, $5) 15 16 ON CONFLICT DO NOTHING 16 - "#) 17 + "#, 18 + ) 17 19 .bind(dropbox_id) 18 20 .bind(&file.path_display) 19 21 .bind(&file.id) ··· 22 24 .execute(pool) 23 25 .await?; 24 26 25 - Ok(()) 26 - } 27 + Ok(()) 28 + }
+23 -11
crates/dropbox/src/repo/dropbox_token.rs
··· 3 3 4 4 use crate::xata::dropbox_token::DropboxTokenWithDid; 5 5 6 - pub async fn find_dropbox_refresh_token(pool: &Pool<Postgres>, did: &str) -> Result<Option<(String, String)>, Error> { 7 - let results: Vec<DropboxTokenWithDid> = sqlx::query_as(r#" 6 + pub async fn find_dropbox_refresh_token( 7 + pool: &Pool<Postgres>, 8 + did: &str, 9 + ) -> Result<Option<(String, String)>, Error> { 10 + let results: Vec<DropboxTokenWithDid> = sqlx::query_as( 11 + r#" 8 12 SELECT 9 13 d.xata_id, 10 14 d.xata_version, ··· 16 20 LEFT JOIN users u ON d.user_id = u.xata_id 17 21 LEFT JOIN dropbox_tokens dt ON d.dropbox_token_id = dt.xata_id 18 22 WHERE u.did = $1 19 - "#) 23 + "#, 24 + ) 20 25 .bind(did) 21 26 .fetch_all(pool) 22 27 .await?; 23 28 24 - if results.len() == 0 { 25 - return Ok(None); 26 - } 29 + if results.len() == 0 { 30 + return Ok(None); 31 + } 27 32 28 - Ok(Some((results[0].refresh_token.clone(), results[0].xata_id.clone()))) 33 + Ok(Some(( 34 + results[0].refresh_token.clone(), 35 + results[0].xata_id.clone(), 36 + ))) 29 37 } 30 38 31 - pub async fn find_dropbox_refresh_tokens(pool: &Pool<Postgres>) -> Result<Vec<DropboxTokenWithDid>, Error> { 32 - let results: Vec<DropboxTokenWithDid> = sqlx::query_as(r#" 39 + pub async fn find_dropbox_refresh_tokens( 40 + pool: &Pool<Postgres>, 41 + ) -> Result<Vec<DropboxTokenWithDid>, Error> { 42 + let results: Vec<DropboxTokenWithDid> = sqlx::query_as( 43 + r#" 33 44 SELECT 34 45 d.xata_id, 35 46 d.xata_version, ··· 40 51 FROM dropbox d 41 52 LEFT JOIN users u ON d.user_id = u.xata_id 42 53 LEFT JOIN dropbox_tokens dt ON d.dropbox_token_id = dt.xata_id 43 - "#) 54 + "#, 55 + ) 44 56 .fetch_all(pool) 45 57 .await?; 46 58 47 - Ok(results) 59 + Ok(results) 48 60 }
+13 -8
crates/dropbox/src/repo/track.rs
··· 3 3 4 4 use crate::xata::track::Track; 5 5 6 - pub async fn get_track_by_hash(pool: &Pool<Postgres>, sha256: &str) -> Result<Option<Track>, Error> { 7 - let results: Vec<Track> = sqlx::query_as(r#" 6 + pub async fn get_track_by_hash( 7 + pool: &Pool<Postgres>, 8 + sha256: &str, 9 + ) -> Result<Option<Track>, Error> { 10 + let results: Vec<Track> = sqlx::query_as( 11 + r#" 8 12 SELECT * FROM tracks WHERE sha256 = $1 9 - "#) 13 + "#, 14 + ) 10 15 .bind(sha256) 11 16 .fetch_all(pool) 12 17 .await?; 13 18 14 - if results.len() == 0 { 15 - return Ok(None); 16 - } 19 + if results.len() == 0 { 20 + return Ok(None); 21 + } 17 22 18 - Ok(Some(results[0].clone())) 19 - } 23 + Ok(Some(results[0].clone())) 24 + }
+318 -255
crates/dropbox/src/scan.rs
··· 2 2 3 3 use anyhow::Error; 4 4 use futures::future::BoxFuture; 5 - use lofty::{file::TaggedFileExt, picture::{MimeType, Picture}, probe::Probe, tag::Accessor}; 5 + use lofty::{ 6 + file::TaggedFileExt, 7 + picture::{MimeType, Picture}, 8 + probe::Probe, 9 + tag::Accessor, 10 + }; 6 11 use owo_colors::OwoColorize; 7 12 use reqwest::{multipart, Client}; 8 13 use serde_json::json; 9 14 use sqlx::{Pool, Postgres}; 10 - use symphonia::core::{formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint}; 15 + use symphonia::core::{ 16 + formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint, 17 + }; 11 18 use tempfile::TempDir; 12 19 13 20 use crate::{ 14 - client::{get_access_token, BASE_URL, CONTENT_URL}, 15 - consts::AUDIO_EXTENSIONS, crypto::decrypt_aes_256_ctr, 16 - repo::{dropbox_path::create_dropbox_path, dropbox_token::{find_dropbox_refresh_token, find_dropbox_refresh_tokens}, track::get_track_by_hash}, 17 - token::generate_token, 18 - types::file::{Entry, EntryList} 21 + client::{get_access_token, BASE_URL, CONTENT_URL}, 22 + consts::AUDIO_EXTENSIONS, 23 + crypto::decrypt_aes_256_ctr, 24 + repo::{ 25 + dropbox_path::create_dropbox_path, 26 + dropbox_token::{find_dropbox_refresh_token, find_dropbox_refresh_tokens}, 27 + track::get_track_by_hash, 28 + }, 29 + token::generate_token, 30 + types::file::{Entry, EntryList}, 19 31 }; 20 32 21 - pub async fn scan_dropbox(pool: Arc<Pool<Postgres>>) -> Result<(), Error>{ 22 - let refresh_tokens = find_dropbox_refresh_tokens(&pool).await?; 23 - for token in refresh_tokens { 24 - let refresh_token = decrypt_aes_256_ctr( 25 - &token.refresh_token, 26 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 27 - )?; 28 - scan_audio_files( 29 - pool.clone(), 30 - "/Music".to_string(), 31 - refresh_token, 32 - token.did, 33 - token.xata_id 34 - ).await?; 35 - } 36 - Ok(()) 33 + pub async fn scan_dropbox(pool: Arc<Pool<Postgres>>) -> Result<(), Error> { 34 + let refresh_tokens = find_dropbox_refresh_tokens(&pool).await?; 35 + for token in refresh_tokens { 36 + let refresh_token = decrypt_aes_256_ctr( 37 + &token.refresh_token, 38 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 39 + )?; 40 + scan_audio_files( 41 + pool.clone(), 42 + "/Music".to_string(), 43 + refresh_token, 44 + token.did, 45 + token.xata_id, 46 + ) 47 + .await?; 48 + } 49 + Ok(()) 37 50 } 38 51 39 - 40 - pub async fn scan_folder(pool: Arc<Pool<Postgres>>, did: &str, path: &str) -> Result<(), Error>{ 41 - let refresh_tokens = find_dropbox_refresh_token(&pool, did).await?; 42 - if let Some((refresh_token, dropbox_id)) = refresh_tokens { 43 - let refresh_token = decrypt_aes_256_ctr( 44 - &refresh_token, 45 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 46 - )?; 52 + pub async fn scan_folder(pool: Arc<Pool<Postgres>>, did: &str, path: &str) -> Result<(), Error> { 53 + let refresh_tokens = find_dropbox_refresh_token(&pool, did).await?; 54 + if let Some((refresh_token, dropbox_id)) = refresh_tokens { 55 + let refresh_token = decrypt_aes_256_ctr( 56 + &refresh_token, 57 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 58 + )?; 47 59 48 - scan_audio_files( 49 - pool.clone(), 50 - path.to_string(), 51 - refresh_token, 52 - did.to_string(), 53 - dropbox_id, 54 - ).await?; 55 - } 56 - Ok(()) 60 + scan_audio_files( 61 + pool.clone(), 62 + path.to_string(), 63 + refresh_token, 64 + did.to_string(), 65 + dropbox_id, 66 + ) 67 + .await?; 68 + } 69 + Ok(()) 57 70 } 58 - 59 71 60 72 pub fn scan_audio_files( 61 73 pool: Arc<Pool<Postgres>>, ··· 64 76 did: String, 65 77 dropbox_id: String, 66 78 ) -> BoxFuture<'static, Result<(), Error>> { 67 - Box::pin(async move { 68 - let res = get_access_token(&refresh_token).await?; 69 - let access_token = res.access_token; 79 + Box::pin(async move { 80 + let res = get_access_token(&refresh_token).await?; 81 + let access_token = res.access_token; 70 82 71 - let client = Client::new(); 83 + let client = Client::new(); 72 84 73 - let res = client.post(&format!("{}/files/get_metadata", BASE_URL)) 74 - .bearer_auth(&access_token) 75 - .json(&json!({ "path": path })) 76 - .send() 77 - .await?; 85 + let res = client 86 + .post(&format!("{}/files/get_metadata", BASE_URL)) 87 + .bearer_auth(&access_token) 88 + .json(&json!({ "path": path })) 89 + .send() 90 + .await?; 78 91 79 - if res.status().as_u16() == 400 || res.status().as_u16() == 409 { 80 - println!("Path not found: {}", path.bright_red()); 81 - return Ok(()); 82 - } 92 + if res.status().as_u16() == 400 || res.status().as_u16() == 409 { 93 + println!("Path not found: {}", path.bright_red()); 94 + return Ok(()); 95 + } 83 96 84 - let entry = res.json::<Entry>().await?; 97 + let entry = res.json::<Entry>().await?; 85 98 86 - if entry.tag.clone().unwrap().as_str() == "folder" { 87 - println!("Scanning folder: {}", path.bright_green()); 99 + if entry.tag.clone().unwrap().as_str() == "folder" { 100 + println!("Scanning folder: {}", path.bright_green()); 88 101 89 - let mut entries: Vec<Entry> = Vec::new(); 102 + let mut entries: Vec<Entry> = Vec::new(); 90 103 91 - let res = client.post(&format!("{}/files/list_folder", BASE_URL)) 92 - .bearer_auth(&access_token) 93 - .json(&json!({ "path": path })) 94 - .send() 95 - .await?; 104 + let res = client 105 + .post(&format!("{}/files/list_folder", BASE_URL)) 106 + .bearer_auth(&access_token) 107 + .json(&json!({ "path": path })) 108 + .send() 109 + .await?; 96 110 97 - let mut entry_list = res.json::<EntryList>().await?; 98 - entries.extend(entry_list.entries); 111 + let mut entry_list = res.json::<EntryList>().await?; 112 + entries.extend(entry_list.entries); 99 113 100 - // Handle pagination using list_folder/continue 101 - while entry_list.has_more { 102 - let res = client.post(&format!("{}/files/list_folder/continue", BASE_URL)) 103 - .bearer_auth(&access_token) 104 - .json(&json!({ "cursor": entry_list.cursor })) 105 - .send() 106 - .await?; 114 + // Handle pagination using list_folder/continue 115 + while entry_list.has_more { 116 + let res = client 117 + .post(&format!("{}/files/list_folder/continue", BASE_URL)) 118 + .bearer_auth(&access_token) 119 + .json(&json!({ "cursor": entry_list.cursor })) 120 + .send() 121 + .await?; 107 122 108 - tokio::time::sleep(std::time::Duration::from_secs(1)).await; 123 + tokio::time::sleep(std::time::Duration::from_secs(1)).await; 109 124 110 - entry_list = res.json::<EntryList>().await?; 111 - entries.extend(entry_list.entries); 112 - } 125 + entry_list = res.json::<EntryList>().await?; 126 + entries.extend(entry_list.entries); 127 + } 113 128 114 - for entry in entries { 115 - scan_audio_files( 116 - pool.clone(), 117 - entry.path_display, 118 - refresh_token.clone(), 119 - did.clone(), 120 - dropbox_id.clone() 121 - ).await?; 122 - tokio::time::sleep(std::time::Duration::from_secs(1)).await; 123 - } 129 + for entry in entries { 130 + scan_audio_files( 131 + pool.clone(), 132 + entry.path_display, 133 + refresh_token.clone(), 134 + did.clone(), 135 + dropbox_id.clone(), 136 + ) 137 + .await?; 138 + tokio::time::sleep(std::time::Duration::from_secs(1)).await; 139 + } 124 140 125 - return Ok(()); 126 - } 141 + return Ok(()); 142 + } 127 143 128 - if !AUDIO_EXTENSIONS 129 - .into_iter() 130 - .any(|ext| path.ends_with(&format!(".{}", ext))) 131 - { 132 - return Ok(()); 133 - } 144 + if !AUDIO_EXTENSIONS 145 + .into_iter() 146 + .any(|ext| path.ends_with(&format!(".{}", ext))) 147 + { 148 + return Ok(()); 149 + } 134 150 135 - let client = Client::new(); 151 + let client = Client::new(); 136 152 137 - println!("Downloading file: {}", path.bright_green()); 153 + println!("Downloading file: {}", path.bright_green()); 138 154 139 - let res = client.post(&format!("{}/files/download", CONTENT_URL)) 140 - .bearer_auth(&access_token) 141 - .header("Dropbox-API-Arg", &json!({ "path": path }).to_string()) 142 - .send() 143 - .await?; 155 + let res = client 156 + .post(&format!("{}/files/download", CONTENT_URL)) 157 + .bearer_auth(&access_token) 158 + .header("Dropbox-API-Arg", &json!({ "path": path }).to_string()) 159 + .send() 160 + .await?; 144 161 145 - let bytes = res.bytes().await?; 162 + let bytes = res.bytes().await?; 146 163 147 - let temp_dir = TempDir::new()?; 148 - let tmppath = temp_dir.path().join(&format!("{}", entry.name)); 149 - let mut tmpfile = File::create(&tmppath)?; 150 - tmpfile.write_all(&bytes)?; 164 + let temp_dir = TempDir::new()?; 165 + let tmppath = temp_dir.path().join(&format!("{}", entry.name)); 166 + let mut tmpfile = File::create(&tmppath)?; 167 + tmpfile.write_all(&bytes)?; 151 168 152 - println!("Reading file: {}", &tmppath.clone().display().to_string().bright_green()); 169 + println!( 170 + "Reading file: {}", 171 + &tmppath.clone().display().to_string().bright_green() 172 + ); 153 173 154 - let tagged_file = match Probe::open(&tmppath)?.read() 155 - { 156 - Ok(tagged_file) => tagged_file, 157 - Err(e) => { 158 - println!("Error opening file: {}", e); 159 - return Ok(()); 160 - } 161 - }; 174 + let tagged_file = match Probe::open(&tmppath)?.read() { 175 + Ok(tagged_file) => tagged_file, 176 + Err(e) => { 177 + println!("Error opening file: {}", e); 178 + return Ok(()); 179 + } 180 + }; 162 181 163 - let primary_tag = tagged_file.primary_tag(); 164 - let tag = match primary_tag { 165 - Some(tag) => tag, 166 - None => { 167 - println!("No tag found in file"); 168 - return Ok(()); 169 - } 170 - }; 182 + let primary_tag = tagged_file.primary_tag(); 183 + let tag = match primary_tag { 184 + Some(tag) => tag, 185 + None => { 186 + println!("No tag found in file"); 187 + return Ok(()); 188 + } 189 + }; 171 190 172 - let pictures = tag.pictures(); 191 + let pictures = tag.pictures(); 173 192 174 - println!("Title: {}", tag.get_string(&lofty::tag::ItemKey::TrackTitle).unwrap_or_default().bright_green()); 175 - println!("Artist: {}", tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default().bright_green()); 176 - println!("Album Artist: {}", tag.get_string(&lofty::tag::ItemKey::AlbumArtist).unwrap_or_default().bright_green()); 177 - println!("Album: {}", tag.get_string(&lofty::tag::ItemKey::AlbumTitle).unwrap_or_default().bright_green()); 178 - println!("Lyrics: {}", tag.get_string(&lofty::tag::ItemKey::Lyrics).unwrap_or_default().bright_green()); 179 - println!("Year: {}", tag.year().unwrap_or_default().bright_green()); 180 - println!("Track Number: {}", tag.track().unwrap_or_default().bright_green()); 181 - println!("Track Total: {}", tag.track_total().unwrap_or_default().bright_green()); 182 - println!("Release Date: {:?}", tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).unwrap_or_default().bright_green()); 183 - println!("Recording Date: {:?}", tag.get_string(&lofty::tag::ItemKey::RecordingDate).unwrap_or_default().bright_green()); 184 - println!("Copyright Message: {}", tag.get_string(&lofty::tag::ItemKey::CopyrightMessage).unwrap_or_default().bright_green()); 185 - println!("Pictures: {:?}", pictures); 193 + println!( 194 + "Title: {}", 195 + tag.get_string(&lofty::tag::ItemKey::TrackTitle) 196 + .unwrap_or_default() 197 + .bright_green() 198 + ); 199 + println!( 200 + "Artist: {}", 201 + tag.get_string(&lofty::tag::ItemKey::TrackArtist) 202 + .unwrap_or_default() 203 + .bright_green() 204 + ); 205 + println!( 206 + "Album Artist: {}", 207 + tag.get_string(&lofty::tag::ItemKey::AlbumArtist) 208 + .unwrap_or_default() 209 + .bright_green() 210 + ); 211 + println!( 212 + "Album: {}", 213 + tag.get_string(&lofty::tag::ItemKey::AlbumTitle) 214 + .unwrap_or_default() 215 + .bright_green() 216 + ); 217 + println!( 218 + "Lyrics: {}", 219 + tag.get_string(&lofty::tag::ItemKey::Lyrics) 220 + .unwrap_or_default() 221 + .bright_green() 222 + ); 223 + println!("Year: {}", tag.year().unwrap_or_default().bright_green()); 224 + println!( 225 + "Track Number: {}", 226 + tag.track().unwrap_or_default().bright_green() 227 + ); 228 + println!( 229 + "Track Total: {}", 230 + tag.track_total().unwrap_or_default().bright_green() 231 + ); 232 + println!( 233 + "Release Date: {:?}", 234 + tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate) 235 + .unwrap_or_default() 236 + .bright_green() 237 + ); 238 + println!( 239 + "Recording Date: {:?}", 240 + tag.get_string(&lofty::tag::ItemKey::RecordingDate) 241 + .unwrap_or_default() 242 + .bright_green() 243 + ); 244 + println!( 245 + "Copyright Message: {}", 246 + tag.get_string(&lofty::tag::ItemKey::CopyrightMessage) 247 + .unwrap_or_default() 248 + .bright_green() 249 + ); 250 + println!("Pictures: {:?}", pictures); 186 251 187 - let title = tag.get_string(&lofty::tag::ItemKey::TrackTitle).unwrap_or_default(); 188 - let artist = tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default(); 189 - let album = tag.get_string(&lofty::tag::ItemKey::AlbumTitle).unwrap_or_default(); 190 - let album_artist = tag.get_string(&lofty::tag::ItemKey::AlbumArtist).unwrap_or_default(); 252 + let title = tag 253 + .get_string(&lofty::tag::ItemKey::TrackTitle) 254 + .unwrap_or_default(); 255 + let artist = tag 256 + .get_string(&lofty::tag::ItemKey::TrackArtist) 257 + .unwrap_or_default(); 258 + let album = tag 259 + .get_string(&lofty::tag::ItemKey::AlbumTitle) 260 + .unwrap_or_default(); 261 + let album_artist = tag 262 + .get_string(&lofty::tag::ItemKey::AlbumArtist) 263 + .unwrap_or_default(); 191 264 192 - let access_token = generate_token(&did)?; 265 + let access_token = generate_token(&did)?; 193 266 194 - // check if track exists 195 - // 196 - // if not, create track 197 - // upload album art 198 - // 199 - // link path to track 267 + // check if track exists 268 + // 269 + // if not, create track 270 + // upload album art 271 + // 272 + // link path to track 200 273 201 - let hash = sha256::digest( 202 - format!("{} - {} - {}", title, artist, album).to_lowercase(), 203 - ); 274 + let hash = sha256::digest(format!("{} - {} - {}", title, artist, album).to_lowercase()); 204 275 205 - let track = get_track_by_hash(&pool, &hash).await?; 206 - let duration = get_track_duration(&tmppath).await?; 207 - let albumart_id = md5::compute(&format!("{} - {}", album_artist, album).to_lowercase()); 208 - let albumart_id = format!("{:x}", albumart_id); 276 + let track = get_track_by_hash(&pool, &hash).await?; 277 + let duration = get_track_duration(&tmppath).await?; 278 + let albumart_id = md5::compute(&format!("{} - {}", album_artist, album).to_lowercase()); 279 + let albumart_id = format!("{:x}", albumart_id); 209 280 210 - match track { 211 - Some(track) => { 212 - println!("Track exists: {}", title.bright_green()); 213 - let status = create_dropbox_path( 214 - &pool, 215 - &entry, 216 - &track, 217 - &dropbox_id, 218 - ) 219 - .await; 220 - println!("status: {:?}", status); 221 - }, 222 - None => { 223 - println!("Creating track: {}", title.bright_green()); 224 - let album_art = upload_album_cover(albumart_id.into(), pictures, &access_token).await?; 225 - let client = Client::new(); 226 - const URL: &str = "https://api.rocksky.app/tracks"; 227 - let response = client 281 + match track { 282 + Some(track) => { 283 + println!("Track exists: {}", title.bright_green()); 284 + let status = create_dropbox_path(&pool, &entry, &track, &dropbox_id).await; 285 + println!("status: {:?}", status); 286 + } 287 + None => { 288 + println!("Creating track: {}", title.bright_green()); 289 + let album_art = 290 + upload_album_cover(albumart_id.into(), pictures, &access_token).await?; 291 + let client = Client::new(); 292 + const URL: &str = "https://api.rocksky.app/tracks"; 293 + let response = client 228 294 .post(URL) 229 295 .header("Authorization", format!("Bearer {}", access_token)) 230 296 .json(&serde_json::json!({ ··· 256 322 })) 257 323 .send() 258 324 .await?; 259 - println!("Track Saved: {} {}", title, response.status()); 260 - tokio::time::sleep(std::time::Duration::from_secs(3)).await; 325 + println!("Track Saved: {} {}", title, response.status()); 326 + tokio::time::sleep(std::time::Duration::from_secs(3)).await; 261 327 328 + let track = get_track_by_hash(&pool, &hash).await?; 329 + if let Some(track) = track { 330 + create_dropbox_path(&pool, &entry, &track, &dropbox_id).await?; 331 + return Ok(()); 332 + } 262 333 263 - let track = get_track_by_hash(&pool, &hash).await?; 264 - if let Some(track) = track { 265 - create_dropbox_path( 266 - &pool, 267 - &entry, 268 - &track, 269 - &dropbox_id, 270 - ) 271 - .await?; 272 - return Ok(()); 334 + println!("Failed to create track: {}", title.bright_green()); 335 + } 273 336 } 274 337 275 - println!("Failed to create track: {}", title.bright_green()); 276 - } 277 - } 278 - 279 - Ok(()) 280 - }) 338 + Ok(()) 339 + }) 281 340 } 282 341 283 - pub async fn upload_album_cover(name: String, pictures: &[Picture], token: &str) -> Result<Option<String>, Error> { 284 - if pictures.is_empty() { 285 - return Ok(None); 286 - } 342 + pub async fn upload_album_cover( 343 + name: String, 344 + pictures: &[Picture], 345 + token: &str, 346 + ) -> Result<Option<String>, Error> { 347 + if pictures.is_empty() { 348 + return Ok(None); 349 + } 287 350 288 - let picture = &pictures[0]; 351 + let picture = &pictures[0]; 289 352 290 - let buffer = match picture.mime_type() { 291 - Some(MimeType::Jpeg) => Some(picture.data().to_vec()), 292 - Some(MimeType::Png) => Some(picture.data().to_vec()), 293 - Some(MimeType::Gif) => Some(picture.data().to_vec()), 294 - Some(MimeType::Bmp) => Some(picture.data().to_vec()), 295 - Some(MimeType::Tiff) => Some(picture.data().to_vec()), 296 - _ => None 297 - }; 353 + let buffer = match picture.mime_type() { 354 + Some(MimeType::Jpeg) => Some(picture.data().to_vec()), 355 + Some(MimeType::Png) => Some(picture.data().to_vec()), 356 + Some(MimeType::Gif) => Some(picture.data().to_vec()), 357 + Some(MimeType::Bmp) => Some(picture.data().to_vec()), 358 + Some(MimeType::Tiff) => Some(picture.data().to_vec()), 359 + _ => None, 360 + }; 298 361 299 - if buffer.is_none() { 300 - return Ok(None); 301 - } 362 + if buffer.is_none() { 363 + return Ok(None); 364 + } 302 365 303 - let buffer = buffer.unwrap(); 366 + let buffer = buffer.unwrap(); 304 367 305 - let ext = match picture.mime_type() { 306 - Some(MimeType::Jpeg) => "jpg", 307 - Some(MimeType::Png) => "png", 308 - Some(MimeType::Gif) => "gif", 309 - Some(MimeType::Bmp) => "bmp", 310 - Some(MimeType::Tiff) => "tiff", 311 - _ => { 312 - return Ok(None); 313 - } 314 - }; 368 + let ext = match picture.mime_type() { 369 + Some(MimeType::Jpeg) => "jpg", 370 + Some(MimeType::Png) => "png", 371 + Some(MimeType::Gif) => "gif", 372 + Some(MimeType::Bmp) => "bmp", 373 + Some(MimeType::Tiff) => "tiff", 374 + _ => { 375 + return Ok(None); 376 + } 377 + }; 315 378 316 - let name = format!("{}.{}", name, ext); 379 + let name = format!("{}.{}", name, ext); 317 380 318 - let part = multipart::Part::bytes(buffer).file_name(name.clone()); 319 - let form = multipart::Form::new().part("file", part); 320 - let client = Client::new(); 381 + let part = multipart::Part::bytes(buffer).file_name(name.clone()); 382 + let form = multipart::Form::new().part("file", part); 383 + let client = Client::new(); 321 384 322 - const URL: &str = "https://uploads.rocksky.app"; 385 + const URL: &str = "https://uploads.rocksky.app"; 323 386 324 - let response = client 325 - .post(URL) 326 - .header("Authorization", format!("Bearer {}", token)) 327 - .multipart(form) 328 - .send() 329 - .await?; 387 + let response = client 388 + .post(URL) 389 + .header("Authorization", format!("Bearer {}", token)) 390 + .multipart(form) 391 + .send() 392 + .await?; 330 393 331 - println!("Cover uploaded: {}", response.status()); 394 + println!("Cover uploaded: {}", response.status()); 332 395 333 - Ok(Some(name)) 396 + Ok(Some(name)) 334 397 } 335 - 336 - 337 398 338 399 pub async fn get_track_duration(path: &Path) -> Result<u64, Error> { 339 - let duration = 0; 340 - let media_source = MediaSourceStream::new(Box::new(std::fs::File::open(path)?), Default::default()); 341 - let mut hint = Hint::new(); 400 + let duration = 0; 401 + let media_source = 402 + MediaSourceStream::new(Box::new(std::fs::File::open(path)?), Default::default()); 403 + let mut hint = Hint::new(); 342 404 343 - if let Some(extension) = path.extension() { 344 - if let Some(extension) = extension.to_str() { 345 - hint.with_extension(extension); 405 + if let Some(extension) = path.extension() { 406 + if let Some(extension) = extension.to_str() { 407 + hint.with_extension(extension); 408 + } 346 409 } 347 - } 348 410 411 + let meta_opts = MetadataOptions::default(); 412 + let format_opts = FormatOptions::default(); 349 413 350 - let meta_opts = MetadataOptions::default(); 351 - let format_opts = FormatOptions::default(); 414 + let probed = 415 + match symphonia::default::get_probe().format(&hint, media_source, &format_opts, &meta_opts) 416 + { 417 + Ok(probed) => probed, 418 + Err(_) => { 419 + println!("Error probing file"); 420 + return Ok(duration); 421 + } 422 + }; 352 423 353 - let probed = match symphonia::default::get_probe().format(&hint, media_source, &format_opts, &meta_opts) { 354 - Ok(probed) => probed, 355 - Err(_) => { 356 - println!("Error probing file"); 357 - return Ok(duration); 358 - }, 359 - }; 360 - 361 - if let Some(track) = probed.format.tracks().first() { 362 - if let Some(duration) = track.codec_params.n_frames { 363 - if let Some(sample_rate) = track.codec_params.sample_rate { 364 - return Ok((duration as f64 / sample_rate as f64) as u64 * 1000); 424 + if let Some(track) = probed.format.tracks().first() { 425 + if let Some(duration) = track.codec_params.n_frames { 426 + if let Some(sample_rate) = track.codec_params.sample_rate { 427 + return Ok((duration as f64 / sample_rate as f64) as u64 * 1000); 428 + } 365 429 } 366 430 } 431 + Ok(duration) 367 432 } 368 - Ok(duration) 369 - }
+122 -111
crates/googledrive/src/client.rs
··· 5 5 use reqwest::Client; 6 6 use serde_json::json; 7 7 8 - use crate::types::{file::{File, FileList}, token::AccessToken}; 8 + use crate::types::{ 9 + file::{File, FileList}, 10 + token::AccessToken, 11 + }; 9 12 10 13 pub const BASE_URL: &str = "https://www.googleapis.com/drive/v3"; 11 14 12 15 pub async fn get_access_token(refresh_token: &str) -> Result<AccessToken, Error> { 13 - let client = Client::new(); 16 + let client = Client::new(); 14 17 15 - let params = [ 16 - ("grant_type", "refresh_token"), 17 - ("refresh_token", refresh_token), 18 - ("client_id", &env::var("GOOGLE_CLIENT_ID")?), 19 - ("client_secret", &env::var("GOOGLE_CLIENT_SECRET")?), 20 - ]; 18 + let params = [ 19 + ("grant_type", "refresh_token"), 20 + ("refresh_token", refresh_token), 21 + ("client_id", &env::var("GOOGLE_CLIENT_ID")?), 22 + ("client_secret", &env::var("GOOGLE_CLIENT_SECRET")?), 23 + ]; 21 24 22 - let body = serde_urlencoded::to_string(&params)?; 23 - let res = client.post("https://oauth2.googleapis.com/token") 24 - .header("Content-Type", "application/x-www-form-urlencoded") 25 - .header("Content-Length", body.len()) 26 - .body(body) 27 - .send() 28 - .await?; 25 + let body = serde_urlencoded::to_string(&params)?; 26 + let res = client 27 + .post("https://oauth2.googleapis.com/token") 28 + .header("Content-Type", "application/x-www-form-urlencoded") 29 + .header("Content-Length", body.len()) 30 + .body(body) 31 + .send() 32 + .await?; 29 33 30 - Ok(res.json::<AccessToken>().await?) 34 + Ok(res.json::<AccessToken>().await?) 31 35 } 32 36 33 37 pub struct GoogleDriveClient { 34 - pub access_token: String, 38 + pub access_token: String, 35 39 } 36 40 37 41 impl GoogleDriveClient { 38 - pub async fn new(refresh_token: &str) -> Result<Self, Error> { 39 - let res = get_access_token(refresh_token).await?; 40 - Ok(Self { 41 - access_token: res.access_token, 42 - }) 43 - } 42 + pub async fn new(refresh_token: &str) -> Result<Self, Error> { 43 + let res = get_access_token(refresh_token).await?; 44 + Ok(Self { 45 + access_token: res.access_token, 46 + }) 47 + } 44 48 45 - pub async fn get_files(&self, name: &str) -> Result<FileList, Error> { 46 - let client = Client::new(); 47 - let url = format!("{}/files", BASE_URL); 48 - let res = client.get(&url) 49 - .bearer_auth(&self.access_token) 50 - .query(&[ 51 - ("q", format!("name='{}' and mimeType='application/vnd.google-apps.folder'", name).as_str()), 52 - ("fields", "files(id, name, mimeType, parents)"), 53 - ("orderBy", "name"), 54 - ]) 55 - .send() 56 - .await?; 49 + pub async fn get_files(&self, name: &str) -> Result<FileList, Error> { 50 + let client = Client::new(); 51 + let url = format!("{}/files", BASE_URL); 52 + let res = client 53 + .get(&url) 54 + .bearer_auth(&self.access_token) 55 + .query(&[ 56 + ( 57 + "q", 58 + format!( 59 + "name='{}' and mimeType='application/vnd.google-apps.folder'", 60 + name 61 + ) 62 + .as_str(), 63 + ), 64 + ("fields", "files(id, name, mimeType, parents)"), 65 + ("orderBy", "name"), 66 + ]) 67 + .send() 68 + .await?; 57 69 58 - Ok(res.json::<FileList>().await?) 59 - } 70 + Ok(res.json::<FileList>().await?) 71 + } 60 72 61 - pub async fn create_music_directory(&self) -> Result<File, Error> { 62 - let client = Client::new(); 63 - let url = format!("{}/files", BASE_URL); 64 - let res = client.post(&url) 65 - .bearer_auth(&self.access_token) 66 - .json(&json!({ 67 - "name": "Music", 68 - "mimeType": "application/vnd.google-apps.folder", 69 - })) 70 - .send() 71 - .await?; 73 + pub async fn create_music_directory(&self) -> Result<File, Error> { 74 + let client = Client::new(); 75 + let url = format!("{}/files", BASE_URL); 76 + let res = client 77 + .post(&url) 78 + .bearer_auth(&self.access_token) 79 + .json(&json!({ 80 + "name": "Music", 81 + "mimeType": "application/vnd.google-apps.folder", 82 + })) 83 + .send() 84 + .await?; 72 85 73 - Ok(res.json::<File>().await?) 74 - } 86 + Ok(res.json::<File>().await?) 87 + } 75 88 76 - pub async fn get_music_directory(&self) -> Result<FileList, Error> { 77 - let client = Client::new(); 78 - let url = format!("{}/files", BASE_URL); 79 - let res = client.get(&url) 89 + pub async fn get_music_directory(&self) -> Result<FileList, Error> { 90 + let client = Client::new(); 91 + let url = format!("{}/files", BASE_URL); 92 + let res = client.get(&url) 80 93 .bearer_auth(&self.access_token) 81 94 .query(&[ 82 95 ("q", "name='Music' and mimeType='application/vnd.google-apps.folder' and 'root' in parents"), ··· 86 99 .send() 87 100 .await?; 88 101 89 - let files = res.json::<FileList>().await?; 102 + let files = res.json::<FileList>().await?; 90 103 91 - if files.files.len() == 0 { 92 - let music_dir = self.create_music_directory().await?; 93 - return Ok(FileList { 94 - files: vec![music_dir], 95 - next_page_token: None, 96 - }); 97 - } 104 + if files.files.len() == 0 { 105 + let music_dir = self.create_music_directory().await?; 106 + return Ok(FileList { 107 + files: vec![music_dir], 108 + next_page_token: None, 109 + }); 110 + } 98 111 99 - Ok(files) 100 - } 101 - 102 - pub async fn get_files_in_parents(&self, parent_id: &str) -> Result<FileList, Error> { 103 - let client = Client::new(); 104 - let url = format!("{}/files", BASE_URL); 105 - let res = client.get(&url) 106 - .bearer_auth(&self.access_token) 107 - .query(&[ 108 - ("q", format!("'{}' in parents", parent_id).as_str()), 109 - ("fields", "files(id, name, mimeType, parents)"), 110 - ("orderBy", "name"), 111 - ]) 112 - .send() 113 - .await?; 114 - Ok(res.json::<FileList>().await?) 115 - } 112 + Ok(files) 113 + } 116 114 117 - pub async fn get_file(&self, file_id: &str) -> Result<File, Error> { 118 - let client = Client::new(); 119 - let url = format!("{}/files/{}", BASE_URL, file_id); 120 - let res = client.get(&url) 121 - .bearer_auth(&self.access_token) 122 - .query(&[ 123 - ("fields", "id, name, mimeType, parents"), 124 - ]) 125 - .send() 126 - .await?; 115 + pub async fn get_files_in_parents(&self, parent_id: &str) -> Result<FileList, Error> { 116 + let client = Client::new(); 117 + let url = format!("{}/files", BASE_URL); 118 + let res = client 119 + .get(&url) 120 + .bearer_auth(&self.access_token) 121 + .query(&[ 122 + ("q", format!("'{}' in parents", parent_id).as_str()), 123 + ("fields", "files(id, name, mimeType, parents)"), 124 + ("orderBy", "name"), 125 + ]) 126 + .send() 127 + .await?; 128 + Ok(res.json::<FileList>().await?) 129 + } 127 130 128 - Ok(res.json::<File>().await?) 129 - } 131 + pub async fn get_file(&self, file_id: &str) -> Result<File, Error> { 132 + let client = Client::new(); 133 + let url = format!("{}/files/{}", BASE_URL, file_id); 134 + let res = client 135 + .get(&url) 136 + .bearer_auth(&self.access_token) 137 + .query(&[("fields", "id, name, mimeType, parents")]) 138 + .send() 139 + .await?; 130 140 131 - pub async fn download_file(&self, file_id: &str) -> Result<HttpResponse, Error> { 132 - let client = Client::new(); 133 - let url = format!("{}/files/{}", BASE_URL, file_id); 134 - let res = client.get(&url) 135 - .bearer_auth(&self.access_token) 136 - .query(&[ 137 - ("alt", "media"), 138 - ]) 139 - .send() 140 - .await?; 141 + Ok(res.json::<File>().await?) 142 + } 141 143 144 + pub async fn download_file(&self, file_id: &str) -> Result<HttpResponse, Error> { 145 + let client = Client::new(); 146 + let url = format!("{}/files/{}", BASE_URL, file_id); 147 + let res = client 148 + .get(&url) 149 + .bearer_auth(&self.access_token) 150 + .query(&[("alt", "media")]) 151 + .send() 152 + .await?; 142 153 143 - let mut actix_response = HttpResponse::Ok(); 154 + let mut actix_response = HttpResponse::Ok(); 144 155 145 - // Forward headers 146 - for (key, value) in res.headers().iter() { 147 - actix_response.append_header((key.as_str(), value.to_str().unwrap_or(""))); 148 - } 156 + // Forward headers 157 + for (key, value) in res.headers().iter() { 158 + actix_response.append_header((key.as_str(), value.to_str().unwrap_or(""))); 159 + } 149 160 150 - // Forward body 151 - let body = res.bytes_stream(); 161 + // Forward body 162 + let body = res.bytes_stream(); 152 163 153 - Ok(actix_response.streaming(body)) 154 - } 155 - } 164 + Ok(actix_response.streaming(body)) 165 + } 166 + }
+7 -4
crates/googledrive/src/cmd/scan.rs
··· 6 6 use crate::scan::scan_googledrive; 7 7 8 8 pub async fn scan() -> Result<(), Error> { 9 - let pool = PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; 10 - let conn = Arc::new(pool); 9 + let pool = PgPoolOptions::new() 10 + .max_connections(5) 11 + .connect(&env::var("XATA_POSTGRES_URL")?) 12 + .await?; 13 + let conn = Arc::new(pool); 11 14 12 - scan_googledrive(conn).await?; 15 + scan_googledrive(conn).await?; 13 16 14 - Ok(()) 17 + Ok(()) 15 18 }
-1
crates/googledrive/src/cmd/serve.rs
··· 63 63 64 64 Ok(()) 65 65 } 66 -
+111 -83
crates/googledrive/src/handlers/files.rs
··· 8 8 pub const MUSIC_DIR: &str = "Music"; 9 9 10 10 use crate::{ 11 - client::GoogleDriveClient, crypto::decrypt_aes_256_ctr, read_payload, repo::google_drive_token::find_google_drive_refresh_token, scan, types::file::{DownloadFileParams, GetFilesInParentsParams, GetFilesParams, ScanFolderParams} 11 + client::GoogleDriveClient, 12 + crypto::decrypt_aes_256_ctr, 13 + read_payload, 14 + repo::google_drive_token::find_google_drive_refresh_token, 15 + scan, 16 + types::file::{DownloadFileParams, GetFilesInParentsParams, GetFilesParams, ScanFolderParams}, 12 17 }; 13 18 14 - pub async fn create_music_directory(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 15 - let body = read_payload!(payload); 16 - let params = serde_json::from_slice::<GetFilesParams>(&body)?; 19 + pub async fn create_music_directory( 20 + payload: &mut web::Payload, 21 + _req: &HttpRequest, 22 + pool: Arc<Pool<Postgres>>, 23 + ) -> Result<HttpResponse, Error> { 24 + let body = read_payload!(payload); 25 + let params = serde_json::from_slice::<GetFilesParams>(&body)?; 17 26 18 - let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 27 + let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 19 28 20 - if refresh_token.is_none() { 21 - return Ok(HttpResponse::Unauthorized().finish()); 22 - } 29 + if refresh_token.is_none() { 30 + return Ok(HttpResponse::Unauthorized().finish()); 31 + } 23 32 24 - let refresh_token = decrypt_aes_256_ctr( 25 - &refresh_token.unwrap().0, 26 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 27 - )?; 33 + let refresh_token = decrypt_aes_256_ctr( 34 + &refresh_token.unwrap().0, 35 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 36 + )?; 28 37 29 - let client = GoogleDriveClient::new(&refresh_token).await?; 30 - let file = client.create_music_directory().await?; 38 + let client = GoogleDriveClient::new(&refresh_token).await?; 39 + let file = client.create_music_directory().await?; 31 40 32 - Ok(HttpResponse::Ok().json(web::Json(file))) 41 + Ok(HttpResponse::Ok().json(web::Json(file))) 33 42 } 34 43 35 - pub async fn get_music_directory(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 36 - let body = read_payload!(payload); 37 - let params = serde_json::from_slice::<GetFilesParams>(&body)?; 44 + pub async fn get_music_directory( 45 + payload: &mut web::Payload, 46 + _req: &HttpRequest, 47 + pool: Arc<Pool<Postgres>>, 48 + ) -> Result<HttpResponse, Error> { 49 + let body = read_payload!(payload); 50 + let params = serde_json::from_slice::<GetFilesParams>(&body)?; 38 51 39 - let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 52 + let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 40 53 41 - if refresh_token.is_none() { 42 - return Ok(HttpResponse::Unauthorized().finish()); 43 - } 54 + if refresh_token.is_none() { 55 + return Ok(HttpResponse::Unauthorized().finish()); 56 + } 44 57 45 - let refresh_token = decrypt_aes_256_ctr( 46 - &refresh_token.unwrap().0, 47 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 48 - )?; 58 + let refresh_token = decrypt_aes_256_ctr( 59 + &refresh_token.unwrap().0, 60 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 61 + )?; 49 62 50 - let client = GoogleDriveClient::new(&refresh_token).await?; 51 - let files = client.get_music_directory().await?; 63 + let client = GoogleDriveClient::new(&refresh_token).await?; 64 + let files = client.get_music_directory().await?; 52 65 53 - Ok(HttpResponse::Ok().json(web::Json(files))) 66 + Ok(HttpResponse::Ok().json(web::Json(files))) 54 67 } 55 68 56 - pub async fn get_files_in_parents(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 57 - let body = read_payload!(payload); 58 - let params = serde_json::from_slice::<GetFilesInParentsParams>(&body)?; 69 + pub async fn get_files_in_parents( 70 + payload: &mut web::Payload, 71 + _req: &HttpRequest, 72 + pool: Arc<Pool<Postgres>>, 73 + ) -> Result<HttpResponse, Error> { 74 + let body = read_payload!(payload); 75 + let params = serde_json::from_slice::<GetFilesInParentsParams>(&body)?; 59 76 60 - let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 77 + let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 61 78 62 - if refresh_token.is_none() { 63 - return Ok(HttpResponse::Unauthorized().finish()); 64 - } 79 + if refresh_token.is_none() { 80 + return Ok(HttpResponse::Unauthorized().finish()); 81 + } 65 82 66 - let refresh_token = decrypt_aes_256_ctr( 67 - &refresh_token.unwrap().0, 68 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 69 - )?; 83 + let refresh_token = decrypt_aes_256_ctr( 84 + &refresh_token.unwrap().0, 85 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 86 + )?; 70 87 71 - let client = GoogleDriveClient::new(&refresh_token).await?; 72 - let files = client.get_files_in_parents(&params.parent_id).await?; 88 + let client = GoogleDriveClient::new(&refresh_token).await?; 89 + let files = client.get_files_in_parents(&params.parent_id).await?; 73 90 74 - Ok(HttpResponse::Ok().json(web::Json(files))) 91 + Ok(HttpResponse::Ok().json(web::Json(files))) 75 92 } 76 93 77 - pub async fn get_file(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 78 - let body = read_payload!(payload); 79 - let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 80 - let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 94 + pub async fn get_file( 95 + payload: &mut web::Payload, 96 + _req: &HttpRequest, 97 + pool: Arc<Pool<Postgres>>, 98 + ) -> Result<HttpResponse, Error> { 99 + let body = read_payload!(payload); 100 + let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 101 + let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 81 102 82 - if refresh_token.is_none() { 83 - return Ok(HttpResponse::Unauthorized().finish()); 84 - } 103 + if refresh_token.is_none() { 104 + return Ok(HttpResponse::Unauthorized().finish()); 105 + } 85 106 86 - let refresh_token = decrypt_aes_256_ctr( 87 - &refresh_token.unwrap().0, 88 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 89 - )?; 107 + let refresh_token = decrypt_aes_256_ctr( 108 + &refresh_token.unwrap().0, 109 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 110 + )?; 90 111 91 - let client = GoogleDriveClient::new(&refresh_token).await?; 92 - let file = client.get_file(&params.file_id).await?; 112 + let client = GoogleDriveClient::new(&refresh_token).await?; 113 + let file = client.get_file(&params.file_id).await?; 93 114 94 - Ok(HttpResponse::Ok().json(web::Json(file))) 115 + Ok(HttpResponse::Ok().json(web::Json(file))) 95 116 } 96 117 118 + pub async fn download_file( 119 + payload: &mut web::Payload, 120 + _req: &HttpRequest, 121 + pool: Arc<Pool<Postgres>>, 122 + ) -> Result<HttpResponse, Error> { 123 + let body = read_payload!(payload); 124 + let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 97 125 98 - pub async fn download_file(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 99 - let body = read_payload!(payload); 100 - let params = serde_json::from_slice::<DownloadFileParams>(&body)?; 126 + let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 101 127 102 - let refresh_token = find_google_drive_refresh_token(&pool.clone(), &params.did).await?; 103 - 104 - if refresh_token.is_none() { 105 - return Ok(HttpResponse::Unauthorized().finish()); 106 - } 128 + if refresh_token.is_none() { 129 + return Ok(HttpResponse::Unauthorized().finish()); 130 + } 107 131 108 - let refresh_token = decrypt_aes_256_ctr( 109 - &refresh_token.unwrap().0, 110 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 111 - )?; 132 + let refresh_token = decrypt_aes_256_ctr( 133 + &refresh_token.unwrap().0, 134 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 135 + )?; 112 136 113 - let client = GoogleDriveClient::new(&refresh_token).await?; 114 - client.download_file(&params.file_id).await 137 + let client = GoogleDriveClient::new(&refresh_token).await?; 138 + client.download_file(&params.file_id).await 115 139 } 116 140 117 - pub async fn scan_folder(payload: &mut web::Payload, _req: &HttpRequest, pool: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 118 - let body = read_payload!(payload); 119 - let params = serde_json::from_slice::<ScanFolderParams>(&body)?; 141 + pub async fn scan_folder( 142 + payload: &mut web::Payload, 143 + _req: &HttpRequest, 144 + pool: Arc<Pool<Postgres>>, 145 + ) -> Result<HttpResponse, Error> { 146 + let body = read_payload!(payload); 147 + let params = serde_json::from_slice::<ScanFolderParams>(&body)?; 120 148 121 - let pool = pool.clone(); 122 - thread::spawn(move || { 123 - let rt = tokio::runtime::Runtime::new().unwrap(); 124 - rt.block_on(scan::scan_folder(pool, &params.did, &params.folder_id))?; 125 - Ok::<(), Error>(()) 126 - }); 149 + let pool = pool.clone(); 150 + thread::spawn(move || { 151 + let rt = tokio::runtime::Runtime::new().unwrap(); 152 + rt.block_on(scan::scan_folder(pool, &params.did, &params.folder_id))?; 153 + Ok::<(), Error>(()) 154 + }); 127 155 128 - // sleep for 2 second to allow the thread to start 129 - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 156 + // sleep for 2 second to allow the thread to start 157 + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; 130 158 131 - Ok(HttpResponse::Ok().finish()) 159 + Ok(HttpResponse::Ok().finish()) 132 160 }
+32 -22
crates/googledrive/src/handlers/mod.rs
··· 2 2 3 3 use actix_web::{web, HttpRequest, HttpResponse}; 4 4 use anyhow::Error; 5 - use files::{create_music_directory, download_file, get_file, get_files_in_parents, get_music_directory, scan_folder}; 5 + use files::{ 6 + create_music_directory, download_file, get_file, get_files_in_parents, get_music_directory, 7 + scan_folder, 8 + }; 6 9 use sqlx::{Pool, Postgres}; 7 10 8 11 pub mod files; 9 12 10 13 #[macro_export] 11 14 macro_rules! read_payload { 12 - ($payload:expr) => {{ 13 - let mut body = Vec::new(); 14 - while let Some(chunk) = $payload.next().await { 15 - // skip if None 16 - match chunk { 17 - Ok(bytes) => body.extend_from_slice(&bytes), 18 - Err(err) => return Err(err.into()), 19 - } 20 - } 21 - body 22 - }}; 15 + ($payload:expr) => {{ 16 + let mut body = Vec::new(); 17 + while let Some(chunk) = $payload.next().await { 18 + // skip if None 19 + match chunk { 20 + Ok(bytes) => body.extend_from_slice(&bytes), 21 + Err(err) => return Err(err.into()), 22 + } 23 + } 24 + body 25 + }}; 23 26 } 24 27 25 - pub async fn handle(method: &str, payload: &mut web::Payload, req: &HttpRequest, conn: Arc<Pool<Postgres>>) -> Result<HttpResponse, Error> { 26 - match method { 27 - "googledrive.getFilesInParents" => get_files_in_parents(payload, req, conn.clone()).await, 28 - "googledrive.createMusicDirectory" => create_music_directory(payload, req, conn.clone()).await, 29 - "googledrive.getMusicDirectory" => get_music_directory(payload, req, conn.clone()).await, 30 - "googledrive.getFile" => get_file(payload, req, conn.clone()).await, 31 - "googledrive.downloadFile" => download_file(payload, req, conn.clone()).await, 32 - "googledrive.scanFolder" => scan_folder(payload, req, conn.clone()).await, 33 - _ => return Err(anyhow::anyhow!("Method not found")), 34 - } 28 + pub async fn handle( 29 + method: &str, 30 + payload: &mut web::Payload, 31 + req: &HttpRequest, 32 + conn: Arc<Pool<Postgres>>, 33 + ) -> Result<HttpResponse, Error> { 34 + match method { 35 + "googledrive.getFilesInParents" => get_files_in_parents(payload, req, conn.clone()).await, 36 + "googledrive.createMusicDirectory" => { 37 + create_music_directory(payload, req, conn.clone()).await 38 + } 39 + "googledrive.getMusicDirectory" => get_music_directory(payload, req, conn.clone()).await, 40 + "googledrive.getFile" => get_file(payload, req, conn.clone()).await, 41 + "googledrive.downloadFile" => download_file(payload, req, conn.clone()).await, 42 + "googledrive.scanFolder" => scan_folder(payload, req, conn.clone()).await, 43 + _ => return Err(anyhow::anyhow!("Method not found")), 44 + } 35 45 }
+9 -15
crates/googledrive/src/main.rs
··· 1 1 use clap::Command; 2 - use cmd::{serve::serve, scan::scan}; 2 + use cmd::{scan::scan, serve::serve}; 3 3 use dotenv::dotenv; 4 4 5 - pub mod types; 6 - pub mod xata; 5 + pub mod client; 7 6 pub mod cmd; 7 + pub mod consts; 8 + pub mod crypto; 8 9 pub mod handlers; 9 10 pub mod repo; 10 - pub mod client; 11 - pub mod crypto; 11 + pub mod scan; 12 12 pub mod token; 13 - pub mod consts; 14 - pub mod scan; 13 + pub mod types; 14 + pub mod xata; 15 15 16 16 fn cli() -> Command { 17 17 Command::new("googledrive") 18 18 .version(env!("CARGO_PKG_VERSION")) 19 19 .about("Rocksky Google Drive Service") 20 - .subcommand( 21 - Command::new("scan") 22 - .about("Scan Google Drive Music Folder") 23 - ) 24 - .subcommand( 25 - Command::new("serve") 26 - .about("Serve Rocksky Google Drive API") 27 - ) 20 + .subcommand(Command::new("scan").about("Scan Google Drive Music Folder")) 21 + .subcommand(Command::new("serve").about("Serve Rocksky Google Drive API")) 28 22 } 29 23 30 24 #[tokio::main]
+11 -9
crates/googledrive/src/repo/google_drive_path.rs
··· 3 3 use crate::{types::file::File, xata::track::Track}; 4 4 5 5 pub async fn create_google_drive_path( 6 - pool: &Pool<Postgres>, 7 - file: &File, 8 - track: &Track, 9 - google_drive_id: &str 6 + pool: &Pool<Postgres>, 7 + file: &File, 8 + track: &Track, 9 + google_drive_id: &str, 10 10 ) -> Result<(), sqlx::Error> { 11 - let result = sqlx::query(r#" 11 + let result = sqlx::query( 12 + r#" 12 13 INSERT INTO google_drive_paths (google_drive_id, file_id, track_id, name) 13 14 VALUES ($1, $2, $3, $4) 14 15 ON CONFLICT DO NOTHING 15 - "#) 16 + "#, 17 + ) 16 18 .bind(google_drive_id) 17 19 .bind(&file.id) 18 20 .bind(&track.xata_id) ··· 20 22 .execute(pool) 21 23 .await?; 22 24 23 - println!("{:?}", result); 25 + println!("{:?}", result); 24 26 25 - Ok(()) 26 - } 27 + Ok(()) 28 + }
+24 -12
crates/googledrive/src/repo/google_drive_token.rs
··· 1 - use sqlx::{Pool, Postgres}; 2 1 use anyhow::Error; 2 + use sqlx::{Pool, Postgres}; 3 3 4 4 use crate::xata::google_drive_token::GoogleDriveTokenWithDid; 5 5 6 - pub async fn find_google_drive_refresh_token(pool: &Pool<Postgres>, did: &str) -> Result<Option<(String, String)>, Error> { 7 - let results: Vec<GoogleDriveTokenWithDid> = sqlx::query_as(r#" 6 + pub async fn find_google_drive_refresh_token( 7 + pool: &Pool<Postgres>, 8 + did: &str, 9 + ) -> Result<Option<(String, String)>, Error> { 10 + let results: Vec<GoogleDriveTokenWithDid> = sqlx::query_as( 11 + r#" 8 12 SELECT 9 13 gd.xata_id, 10 14 gd.xata_version, ··· 16 20 LEFT JOIN users u ON gd.user_id = u.xata_id 17 21 LEFT JOIN google_drive_tokens gt ON gd.google_drive_token_id = gt.xata_id 18 22 WHERE u.did = $1 19 - "#) 23 + "#, 24 + ) 20 25 .bind(did) 21 26 .fetch_all(pool) 22 27 .await?; 23 28 24 - if results.len() == 0 { 25 - return Ok(None); 26 - } 29 + if results.len() == 0 { 30 + return Ok(None); 31 + } 27 32 28 - Ok(Some((results[0].refresh_token.clone(), results[0].xata_id.clone()))) 33 + Ok(Some(( 34 + results[0].refresh_token.clone(), 35 + results[0].xata_id.clone(), 36 + ))) 29 37 } 30 38 31 - pub async fn find_google_drive_refresh_tokens(pool: &Pool<Postgres>) -> Result<Vec<GoogleDriveTokenWithDid>, Error> { 32 - let results: Vec<GoogleDriveTokenWithDid> = sqlx::query_as(r#" 39 + pub async fn find_google_drive_refresh_tokens( 40 + pool: &Pool<Postgres>, 41 + ) -> Result<Vec<GoogleDriveTokenWithDid>, Error> { 42 + let results: Vec<GoogleDriveTokenWithDid> = sqlx::query_as( 43 + r#" 33 44 SELECT 34 45 gd.xata_id, 35 46 gd.xata_version, ··· 40 51 FROM google_drive gd 41 52 LEFT JOIN users u ON gd.user_id = u.xata_id 42 53 LEFT JOIN google_drive_tokens gt ON gd.google_drive_token_id = gt.xata_id 43 - "#) 54 + "#, 55 + ) 44 56 .fetch_all(pool) 45 57 .await?; 46 58 47 - Ok(results) 59 + Ok(results) 48 60 }
+13 -8
crates/googledrive/src/repo/track.rs
··· 3 3 4 4 use crate::xata::track::Track; 5 5 6 - pub async fn get_track_by_hash(pool: &Pool<Postgres>, sha256: &str) -> Result<Option<Track>, Error> { 7 - let results: Vec<Track> = sqlx::query_as(r#" 6 + pub async fn get_track_by_hash( 7 + pool: &Pool<Postgres>, 8 + sha256: &str, 9 + ) -> Result<Option<Track>, Error> { 10 + let results: Vec<Track> = sqlx::query_as( 11 + r#" 8 12 SELECT * FROM tracks WHERE sha256 = $1 9 - "#) 13 + "#, 14 + ) 10 15 .bind(sha256) 11 16 .fetch_all(pool) 12 17 .await?; 13 18 14 - if results.len() == 0 { 15 - return Ok(None); 16 - } 19 + if results.len() == 0 { 20 + return Ok(None); 21 + } 17 22 18 - Ok(Some(results[0].clone())) 19 - } 23 + Ok(Some(results[0].clone())) 24 + }
+334 -260
crates/googledrive/src/scan.rs
··· 2 2 3 3 use anyhow::Error; 4 4 use futures::future::BoxFuture; 5 - use lofty::{file::TaggedFileExt, picture::{MimeType, Picture}, probe::Probe, tag::Accessor}; 5 + use lofty::{ 6 + file::TaggedFileExt, 7 + picture::{MimeType, Picture}, 8 + probe::Probe, 9 + tag::Accessor, 10 + }; 6 11 use owo_colors::OwoColorize; 7 12 use reqwest::{multipart, Client}; 8 13 use sqlx::{Pool, Postgres}; 9 - use symphonia::core::{formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint}; 14 + use symphonia::core::{ 15 + formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint, 16 + }; 10 17 use tempfile::TempDir; 11 18 12 - use crate::{client::{GoogleDriveClient, BASE_URL}, consts::AUDIO_EXTENSIONS, crypto::decrypt_aes_256_ctr, repo::{google_drive_path::create_google_drive_path, google_drive_token::{find_google_drive_refresh_token, find_google_drive_refresh_tokens}, track::get_track_by_hash}, token::generate_token, types::file::{File, FileList}}; 19 + use crate::{ 20 + client::{GoogleDriveClient, BASE_URL}, 21 + consts::AUDIO_EXTENSIONS, 22 + crypto::decrypt_aes_256_ctr, 23 + repo::{ 24 + google_drive_path::create_google_drive_path, 25 + google_drive_token::{find_google_drive_refresh_token, find_google_drive_refresh_tokens}, 26 + track::get_track_by_hash, 27 + }, 28 + token::generate_token, 29 + types::file::{File, FileList}, 30 + }; 13 31 14 32 pub async fn scan_googledrive(pool: Arc<Pool<Postgres>>) -> Result<(), Error> { 15 - let refresh_tokens = find_google_drive_refresh_tokens(&pool).await?; 16 - for token in refresh_tokens { 17 - let refresh_token = decrypt_aes_256_ctr( 18 - &token.refresh_token, 19 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 20 - )?; 33 + let refresh_tokens = find_google_drive_refresh_tokens(&pool).await?; 34 + for token in refresh_tokens { 35 + let refresh_token = decrypt_aes_256_ctr( 36 + &token.refresh_token, 37 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 38 + )?; 21 39 22 - let client = GoogleDriveClient::new(&refresh_token).await?; 23 - let filelist = client.get_music_directory().await?; 24 - let music_dir = filelist.files.first().unwrap(); 25 - scan_audio_files( 26 - pool.clone(), 27 - music_dir.id.clone(), 28 - refresh_token.clone(), 29 - token.did.clone(), 30 - token.xata_id.clone() 31 - ).await?; 32 - } 33 - Ok(()) 40 + let client = GoogleDriveClient::new(&refresh_token).await?; 41 + let filelist = client.get_music_directory().await?; 42 + let music_dir = filelist.files.first().unwrap(); 43 + scan_audio_files( 44 + pool.clone(), 45 + music_dir.id.clone(), 46 + refresh_token.clone(), 47 + token.did.clone(), 48 + token.xata_id.clone(), 49 + ) 50 + .await?; 51 + } 52 + Ok(()) 34 53 } 35 54 36 - pub async fn scan_folder(pool: Arc<Pool<Postgres>>, did: &str, folder_id: &str) -> Result<(), Error> { 37 - let refresh_token = find_google_drive_refresh_token(&pool, did).await?; 38 - if let Some((refresh_token, google_drive_id)) = refresh_token { 39 - let refresh_token = decrypt_aes_256_ctr( 40 - &refresh_token, 41 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 42 - )?; 55 + pub async fn scan_folder( 56 + pool: Arc<Pool<Postgres>>, 57 + did: &str, 58 + folder_id: &str, 59 + ) -> Result<(), Error> { 60 + let refresh_token = find_google_drive_refresh_token(&pool, did).await?; 61 + if let Some((refresh_token, google_drive_id)) = refresh_token { 62 + let refresh_token = decrypt_aes_256_ctr( 63 + &refresh_token, 64 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 65 + )?; 43 66 44 - scan_audio_files( 45 - pool.clone(), 46 - folder_id.to_string(), 47 - refresh_token, 48 - did.to_string(), 49 - google_drive_id.clone() 50 - ).await?; 51 - } 67 + scan_audio_files( 68 + pool.clone(), 69 + folder_id.to_string(), 70 + refresh_token, 71 + did.to_string(), 72 + google_drive_id.clone(), 73 + ) 74 + .await?; 75 + } 52 76 53 - Ok(()) 77 + Ok(()) 54 78 } 55 - 56 79 57 80 pub fn scan_audio_files( 58 81 pool: Arc<Pool<Postgres>>, ··· 61 84 did: String, 62 85 google_drive_id: String, 63 86 ) -> BoxFuture<'static, Result<(), Error>> { 64 - Box::pin(async move { 65 - let client = GoogleDriveClient::new(&refresh_token).await?; 66 - let access_token = client.access_token.clone(); 87 + Box::pin(async move { 88 + let client = GoogleDriveClient::new(&refresh_token).await?; 89 + let access_token = client.access_token.clone(); 67 90 68 - let client = Client::new(); 69 - let url = format!("{}/files/{}", BASE_URL, file_id); 70 - let res = client.get(&url) 71 - .bearer_auth(&access_token) 72 - .query(&[ 73 - ("fields", "id, name, mimeType, parents"), 74 - ]) 75 - .send() 76 - .await?; 91 + let client = Client::new(); 92 + let url = format!("{}/files/{}", BASE_URL, file_id); 93 + let res = client 94 + .get(&url) 95 + .bearer_auth(&access_token) 96 + .query(&[("fields", "id, name, mimeType, parents")]) 97 + .send() 98 + .await?; 77 99 78 - let file = res.json::<File>().await?; 100 + let file = res.json::<File>().await?; 79 101 80 - if file.mime_type == "application/vnd.google-apps.folder" { 81 - println!("Scanning folder: {}", file.name.bright_green()); 102 + if file.mime_type == "application/vnd.google-apps.folder" { 103 + println!("Scanning folder: {}", file.name.bright_green()); 82 104 83 - let mut page_token: Option<String> = None; 84 - let mut files: Vec<File> = Vec::new(); 105 + let mut page_token: Option<String> = None; 106 + let mut files: Vec<File> = Vec::new(); 85 107 86 - loop { 87 - let mut req = client.get(&format!("{}/files", BASE_URL)) 88 - .bearer_auth(&access_token) 89 - .query(&[ 90 - ("q", format!("'{}' in parents", file.id).as_str()), 91 - ("fields", "nextPageToken, files(id, name, mimeType, parents)"), 92 - ("orderBy", "name"), 93 - ("pageSize", "1000"), // max is 1000 94 - ]); 108 + loop { 109 + let mut req = client 110 + .get(&format!("{}/files", BASE_URL)) 111 + .bearer_auth(&access_token) 112 + .query(&[ 113 + ("q", format!("'{}' in parents", file.id).as_str()), 114 + ( 115 + "fields", 116 + "nextPageToken, files(id, name, mimeType, parents)", 117 + ), 118 + ("orderBy", "name"), 119 + ("pageSize", "1000"), // max is 1000 120 + ]); 95 121 96 - if let Some(token) = &page_token { 97 - req = req.query(&[("pageToken", token)]); 98 - } 122 + if let Some(token) = &page_token { 123 + req = req.query(&[("pageToken", token)]); 124 + } 99 125 100 - let res = req.send().await?; 101 - let filelist = res.json::<FileList>().await?; 126 + let res = req.send().await?; 127 + let filelist = res.json::<FileList>().await?; 102 128 103 - files.extend(filelist.files); 129 + files.extend(filelist.files); 104 130 105 - if let Some(token) = filelist.next_page_token { 106 - page_token = Some(token); 107 - } else { 108 - break; 109 - } 110 - } 111 - 131 + if let Some(token) = filelist.next_page_token { 132 + page_token = Some(token); 133 + } else { 134 + break; 135 + } 136 + } 112 137 113 - for file in files { 114 - scan_audio_files( 115 - pool.clone(), 116 - file.id, 117 - refresh_token.clone(), 118 - did.clone(), 119 - google_drive_id.clone() 120 - ).await?; 121 - tokio::time::sleep(std::time::Duration::from_secs(3)).await; 122 - } 138 + for file in files { 139 + scan_audio_files( 140 + pool.clone(), 141 + file.id, 142 + refresh_token.clone(), 143 + did.clone(), 144 + google_drive_id.clone(), 145 + ) 146 + .await?; 147 + tokio::time::sleep(std::time::Duration::from_secs(3)).await; 148 + } 123 149 124 - return Ok(()); 125 - } 150 + return Ok(()); 151 + } 126 152 127 - if !AUDIO_EXTENSIONS 128 - .into_iter() 129 - .any(|ext| file.name.ends_with(&format!(".{}", ext))) 130 - { 131 - return Ok(()); 132 - } 153 + if !AUDIO_EXTENSIONS 154 + .into_iter() 155 + .any(|ext| file.name.ends_with(&format!(".{}", ext))) 156 + { 157 + return Ok(()); 158 + } 133 159 134 - println!("Downloading file: {}", file.name.bright_green()); 160 + println!("Downloading file: {}", file.name.bright_green()); 135 161 136 - let client = Client::new(); 162 + let client = Client::new(); 137 163 138 - let url = format!("{}/files/{}", BASE_URL, file_id); 139 - let res = client.get(&url) 140 - .bearer_auth(&access_token) 141 - .query(&[ 142 - ("alt", "media"), 143 - ]) 144 - .send() 145 - .await?; 164 + let url = format!("{}/files/{}", BASE_URL, file_id); 165 + let res = client 166 + .get(&url) 167 + .bearer_auth(&access_token) 168 + .query(&[("alt", "media")]) 169 + .send() 170 + .await?; 146 171 147 - let bytes = res.bytes().await?; 172 + let bytes = res.bytes().await?; 148 173 149 - let temp_dir = TempDir::new()?; 150 - let tmppath = temp_dir.path().join(&format!("{}", file.name)); 151 - let mut tmpfile = std::fs::File::create(&tmppath)?; 152 - tmpfile.write_all(&bytes)?; 174 + let temp_dir = TempDir::new()?; 175 + let tmppath = temp_dir.path().join(&format!("{}", file.name)); 176 + let mut tmpfile = std::fs::File::create(&tmppath)?; 177 + tmpfile.write_all(&bytes)?; 153 178 154 - println!("Reading file: {}", &tmppath.clone().display().to_string().bright_green()); 179 + println!( 180 + "Reading file: {}", 181 + &tmppath.clone().display().to_string().bright_green() 182 + ); 155 183 156 - let tagged_file = match Probe::open(&tmppath)?.read() 157 - { 158 - Ok(tagged_file) => tagged_file, 159 - Err(e) => { 160 - println!("Error opening file: {}", e); 161 - return Ok(()); 162 - } 163 - }; 184 + let tagged_file = match Probe::open(&tmppath)?.read() { 185 + Ok(tagged_file) => tagged_file, 186 + Err(e) => { 187 + println!("Error opening file: {}", e); 188 + return Ok(()); 189 + } 190 + }; 164 191 165 - let primary_tag = tagged_file.primary_tag(); 166 - let tag = match primary_tag { 167 - Some(tag) => tag, 168 - None => { 169 - println!("No tag found in file"); 170 - return Ok(()); 171 - } 172 - }; 192 + let primary_tag = tagged_file.primary_tag(); 193 + let tag = match primary_tag { 194 + Some(tag) => tag, 195 + None => { 196 + println!("No tag found in file"); 197 + return Ok(()); 198 + } 199 + }; 173 200 174 - let pictures = tag.pictures(); 201 + let pictures = tag.pictures(); 175 202 176 - println!("Title: {}", tag.get_string(&lofty::tag::ItemKey::TrackTitle).unwrap_or_default().bright_green()); 177 - println!("Artist: {}", tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default().bright_green()); 178 - println!("Album Artist: {}", tag.get_string(&lofty::tag::ItemKey::AlbumArtist).unwrap_or_default().bright_green()); 179 - println!("Album: {}", tag.get_string(&lofty::tag::ItemKey::AlbumTitle).unwrap_or_default().bright_green()); 180 - println!("Lyrics: {}", tag.get_string(&lofty::tag::ItemKey::Lyrics).unwrap_or_default().bright_green()); 181 - println!("Year: {}", tag.year().unwrap_or_default().bright_green()); 182 - println!("Track Number: {}", tag.track().unwrap_or_default().bright_green()); 183 - println!("Track Total: {}", tag.track_total().unwrap_or_default().bright_green()); 184 - println!("Release Date: {:?}", tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).unwrap_or_default().bright_green()); 185 - println!("Recording Date: {:?}", tag.get_string(&lofty::tag::ItemKey::RecordingDate).unwrap_or_default().bright_green()); 186 - println!("Copyright Message: {}", tag.get_string(&lofty::tag::ItemKey::CopyrightMessage).unwrap_or_default().bright_green()); 187 - println!("Pictures: {:?}", pictures); 203 + println!( 204 + "Title: {}", 205 + tag.get_string(&lofty::tag::ItemKey::TrackTitle) 206 + .unwrap_or_default() 207 + .bright_green() 208 + ); 209 + println!( 210 + "Artist: {}", 211 + tag.get_string(&lofty::tag::ItemKey::TrackArtist) 212 + .unwrap_or_default() 213 + .bright_green() 214 + ); 215 + println!( 216 + "Album Artist: {}", 217 + tag.get_string(&lofty::tag::ItemKey::AlbumArtist) 218 + .unwrap_or_default() 219 + .bright_green() 220 + ); 221 + println!( 222 + "Album: {}", 223 + tag.get_string(&lofty::tag::ItemKey::AlbumTitle) 224 + .unwrap_or_default() 225 + .bright_green() 226 + ); 227 + println!( 228 + "Lyrics: {}", 229 + tag.get_string(&lofty::tag::ItemKey::Lyrics) 230 + .unwrap_or_default() 231 + .bright_green() 232 + ); 233 + println!("Year: {}", tag.year().unwrap_or_default().bright_green()); 234 + println!( 235 + "Track Number: {}", 236 + tag.track().unwrap_or_default().bright_green() 237 + ); 238 + println!( 239 + "Track Total: {}", 240 + tag.track_total().unwrap_or_default().bright_green() 241 + ); 242 + println!( 243 + "Release Date: {:?}", 244 + tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate) 245 + .unwrap_or_default() 246 + .bright_green() 247 + ); 248 + println!( 249 + "Recording Date: {:?}", 250 + tag.get_string(&lofty::tag::ItemKey::RecordingDate) 251 + .unwrap_or_default() 252 + .bright_green() 253 + ); 254 + println!( 255 + "Copyright Message: {}", 256 + tag.get_string(&lofty::tag::ItemKey::CopyrightMessage) 257 + .unwrap_or_default() 258 + .bright_green() 259 + ); 260 + println!("Pictures: {:?}", pictures); 188 261 189 - let title = tag.get_string(&lofty::tag::ItemKey::TrackTitle).unwrap_or_default(); 190 - let artist = tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default(); 191 - let album_artist = tag.get_string(&lofty::tag::ItemKey::AlbumArtist).unwrap_or_default(); 192 - let album = tag.get_string(&lofty::tag::ItemKey::AlbumTitle).unwrap_or_default(); 193 - let access_token = generate_token(&did)?; 262 + let title = tag 263 + .get_string(&lofty::tag::ItemKey::TrackTitle) 264 + .unwrap_or_default(); 265 + let artist = tag 266 + .get_string(&lofty::tag::ItemKey::TrackArtist) 267 + .unwrap_or_default(); 268 + let album_artist = tag 269 + .get_string(&lofty::tag::ItemKey::AlbumArtist) 270 + .unwrap_or_default(); 271 + let album = tag 272 + .get_string(&lofty::tag::ItemKey::AlbumTitle) 273 + .unwrap_or_default(); 274 + let access_token = generate_token(&did)?; 194 275 195 - // check if track exists 196 - // 197 - // if not, create track 198 - // upload album artist 199 - // 200 - // link path to track 276 + // check if track exists 277 + // 278 + // if not, create track 279 + // upload album artist 280 + // 281 + // link path to track 201 282 202 - let hash = sha256::digest( 203 - format!("{} - {} - {}", title, artist, album).to_lowercase(), 204 - ); 283 + let hash = sha256::digest(format!("{} - {} - {}", title, artist, album).to_lowercase()); 205 284 206 - let track = get_track_by_hash(&pool, &hash).await?; 207 - let duration = get_track_duration(&tmppath).await?; 208 - let albumart_id = md5::compute(&format!("{} - {}", album_artist, album).to_lowercase()); 209 - let albumart_id = format!("{:x}", albumart_id); 285 + let track = get_track_by_hash(&pool, &hash).await?; 286 + let duration = get_track_duration(&tmppath).await?; 287 + let albumart_id = md5::compute(&format!("{} - {}", album_artist, album).to_lowercase()); 288 + let albumart_id = format!("{:x}", albumart_id); 210 289 211 - match track { 212 - Some(track) => { 213 - println!("Track exists: {}", title.bright_green()); 214 - let status = create_google_drive_path( 215 - &pool, 216 - &file, 217 - &track, 218 - &google_drive_id, 219 - ) 220 - .await?; 290 + match track { 291 + Some(track) => { 292 + println!("Track exists: {}", title.bright_green()); 293 + let status = 294 + create_google_drive_path(&pool, &file, &track, &google_drive_id).await?; 221 295 222 - println!("status: {:?}", status); 223 - }, 224 - None => { 225 - println!("Creating track: {}", title.bright_green()); 296 + println!("status: {:?}", status); 297 + } 298 + None => { 299 + println!("Creating track: {}", title.bright_green()); 226 300 227 - let albumart = upload_album_cover(albumart_id.into(), pictures, &access_token).await?; 301 + let albumart = 302 + upload_album_cover(albumart_id.into(), pictures, &access_token).await?; 228 303 229 - let client = Client::new(); 230 - const URL: &str = "https://api.rocksky.app/tracks"; 231 - let response = client 304 + let client = Client::new(); 305 + const URL: &str = "https://api.rocksky.app/tracks"; 306 + let response = client 232 307 .post(URL) 233 308 .header("Authorization", format!("Bearer {}", access_token)) 234 309 .json(&serde_json::json!({ ··· 260 335 })) 261 336 .send() 262 337 .await?; 263 - println!("Track Saved: {} {}", title, response.status()); 264 - tokio::time::sleep(std::time::Duration::from_secs(3)).await; 338 + println!("Track Saved: {} {}", title, response.status()); 339 + tokio::time::sleep(std::time::Duration::from_secs(3)).await; 265 340 341 + let track = get_track_by_hash(&pool, &hash).await?; 342 + if let Some(track) = track { 343 + let status = 344 + create_google_drive_path(&pool, &file, &track, &google_drive_id).await; 266 345 267 - let track = get_track_by_hash(&pool, &hash).await?; 268 - if let Some(track) = track { 269 - let status = create_google_drive_path( 270 - &pool, 271 - &file, 272 - &track, 273 - &google_drive_id, 274 - ) 275 - .await; 346 + println!("status: {:?}", status); 276 347 277 - println!("status: {:?}", status); 348 + tokio::time::sleep(std::time::Duration::from_secs(1)).await; 278 349 279 - tokio::time::sleep(std::time::Duration::from_secs(1)).await; 350 + return Ok(()); 351 + } 280 352 281 - return Ok(()); 353 + println!("Failed to create track: {}", title.bright_green()); 354 + } 282 355 } 283 356 284 - println!("Failed to create track: {}", title.bright_green()); 285 - } 286 - } 287 - 288 - Ok(()) 289 - }) 357 + Ok(()) 358 + }) 290 359 } 291 360 292 - pub async fn upload_album_cover(name: String, pictures: &[Picture], token: &str) -> Result<Option<String>, Error> { 293 - if pictures.is_empty() { 294 - return Ok(None); 295 - } 296 - 297 - let picture = &pictures[0]; 361 + pub async fn upload_album_cover( 362 + name: String, 363 + pictures: &[Picture], 364 + token: &str, 365 + ) -> Result<Option<String>, Error> { 366 + if pictures.is_empty() { 367 + return Ok(None); 368 + } 298 369 299 - let buffer = match picture.mime_type() { 300 - Some(MimeType::Jpeg) => Some(picture.data().to_vec()), 301 - Some(MimeType::Png) => Some(picture.data().to_vec()), 302 - Some(MimeType::Gif) => Some(picture.data().to_vec()), 303 - Some(MimeType::Bmp) => Some(picture.data().to_vec()), 304 - Some(MimeType::Tiff) => Some(picture.data().to_vec()), 305 - _ => None 306 - }; 370 + let picture = &pictures[0]; 307 371 308 - if buffer.is_none() { 309 - return Ok(None); 310 - } 372 + let buffer = match picture.mime_type() { 373 + Some(MimeType::Jpeg) => Some(picture.data().to_vec()), 374 + Some(MimeType::Png) => Some(picture.data().to_vec()), 375 + Some(MimeType::Gif) => Some(picture.data().to_vec()), 376 + Some(MimeType::Bmp) => Some(picture.data().to_vec()), 377 + Some(MimeType::Tiff) => Some(picture.data().to_vec()), 378 + _ => None, 379 + }; 311 380 312 - let buffer = buffer.unwrap(); 381 + if buffer.is_none() { 382 + return Ok(None); 383 + } 313 384 385 + let buffer = buffer.unwrap(); 314 386 315 - let ext = match picture.mime_type() { 316 - Some(MimeType::Jpeg) => "jpg", 317 - Some(MimeType::Png) => "png", 318 - Some(MimeType::Gif) => "gif", 319 - Some(MimeType::Bmp) => "bmp", 320 - Some(MimeType::Tiff) => "tiff", 321 - _ => { 322 - return Ok(None); 323 - } 324 - }; 387 + let ext = match picture.mime_type() { 388 + Some(MimeType::Jpeg) => "jpg", 389 + Some(MimeType::Png) => "png", 390 + Some(MimeType::Gif) => "gif", 391 + Some(MimeType::Bmp) => "bmp", 392 + Some(MimeType::Tiff) => "tiff", 393 + _ => { 394 + return Ok(None); 395 + } 396 + }; 325 397 326 - let name = format!("{}.{}", name, ext); 398 + let name = format!("{}.{}", name, ext); 327 399 328 - let part = multipart::Part::bytes(buffer).file_name(name.clone()); 329 - let form = multipart::Form::new().part("file", part); 330 - let client = Client::new(); 400 + let part = multipart::Part::bytes(buffer).file_name(name.clone()); 401 + let form = multipart::Form::new().part("file", part); 402 + let client = Client::new(); 331 403 332 - const URL: &str = "https://uploads.rocksky.app"; 404 + const URL: &str = "https://uploads.rocksky.app"; 333 405 334 - let response = client 335 - .post(URL) 336 - .header("Authorization", format!("Bearer {}", token)) 337 - .multipart(form) 338 - .send() 339 - .await?; 406 + let response = client 407 + .post(URL) 408 + .header("Authorization", format!("Bearer {}", token)) 409 + .multipart(form) 410 + .send() 411 + .await?; 340 412 341 - println!("Cover uploaded: {}", response.status()); 413 + println!("Cover uploaded: {}", response.status()); 342 414 343 - Ok(Some(name)) 415 + Ok(Some(name)) 344 416 } 345 417 346 418 pub async fn get_track_duration(path: &Path) -> Result<u64, Error> { 347 - let duration = 0; 348 - let media_source = MediaSourceStream::new(Box::new(std::fs::File::open(path)?), Default::default()); 349 - let mut hint = Hint::new(); 419 + let duration = 0; 420 + let media_source = 421 + MediaSourceStream::new(Box::new(std::fs::File::open(path)?), Default::default()); 422 + let mut hint = Hint::new(); 350 423 351 - if let Some(extension) = path.extension() { 352 - if let Some(extension) = extension.to_str() { 353 - hint.with_extension(extension); 424 + if let Some(extension) = path.extension() { 425 + if let Some(extension) = extension.to_str() { 426 + hint.with_extension(extension); 427 + } 354 428 } 355 - } 356 429 430 + let meta_opts = MetadataOptions::default(); 431 + let format_opts = FormatOptions::default(); 357 432 358 - let meta_opts = MetadataOptions::default(); 359 - let format_opts = FormatOptions::default(); 433 + let probed = 434 + match symphonia::default::get_probe().format(&hint, media_source, &format_opts, &meta_opts) 435 + { 436 + Ok(probed) => probed, 437 + Err(_) => { 438 + println!("Error probing file"); 439 + return Ok(duration); 440 + } 441 + }; 360 442 361 - let probed = match symphonia::default::get_probe().format(&hint, media_source, &format_opts, &meta_opts) { 362 - Ok(probed) => probed, 363 - Err(_) => { 364 - println!("Error probing file"); 365 - return Ok(duration); 366 - }, 367 - }; 368 - 369 - if let Some(track) = probed.format.tracks().first() { 370 - if let Some(duration) = track.codec_params.n_frames { 371 - if let Some(sample_rate) = track.codec_params.sample_rate { 372 - return Ok((duration as f64 / sample_rate as f64) as u64 * 1000); 443 + if let Some(track) = probed.format.tracks().first() { 444 + if let Some(duration) = track.codec_params.n_frames { 445 + if let Some(sample_rate) = track.codec_params.sample_rate { 446 + return Ok((duration as f64 / sample_rate as f64) as u64 * 1000); 447 + } 373 448 } 374 449 } 450 + Ok(duration) 375 451 } 376 - Ok(duration) 377 - }
+9 -5
crates/jetstream/src/main.rs
··· 1 1 use std::env; 2 2 3 - use subscriber::ScrobbleSubscriber; 4 3 use dotenv::dotenv; 4 + use subscriber::ScrobbleSubscriber; 5 5 6 + pub mod profile; 7 + pub mod repo; 6 8 pub mod subscriber; 7 9 pub mod types; 8 10 pub mod xata; 9 - pub mod repo; 10 - pub mod profile; 11 11 12 12 #[tokio::main] 13 13 async fn main() -> Result<(), anyhow::Error> { 14 14 dotenv()?; 15 - let jetstream_server = env::var("JETSTREAM_SERVER").unwrap_or_else(|_| "wss://jetstream2.us-east.bsky.network".to_string()); 16 - let url = format!("{}/subscribe?wantedCollections=app.rocksky.*", jetstream_server); 15 + let jetstream_server = env::var("JETSTREAM_SERVER") 16 + .unwrap_or_else(|_| "wss://jetstream2.us-east.bsky.network".to_string()); 17 + let url = format!( 18 + "{}/subscribe?wantedCollections=app.rocksky.*", 19 + jetstream_server 20 + ); 17 21 let subscriber = ScrobbleSubscriber::new(&url); 18 22 19 23 subscriber.run().await?;
+56 -43
crates/jetstream/src/profile.rs
··· 3 3 use crate::types::{Profile, ProfileResponse}; 4 4 5 5 pub async fn did_to_profile(did: &str) -> Result<Profile, Error> { 6 - let client = reqwest::Client::new(); 7 - let response = client.get(format!("https://plc.directory/{}", did)) 8 - .header("Accept", "application/json") 9 - .send() 10 - .await? 11 - .json::<serde_json::Value>() 12 - .await?; 6 + let client = reqwest::Client::new(); 7 + let response = client 8 + .get(format!("https://plc.directory/{}", did)) 9 + .header("Accept", "application/json") 10 + .send() 11 + .await? 12 + .json::<serde_json::Value>() 13 + .await?; 13 14 14 - let handle = response["alsoKnownAs"][0].as_str() 15 - .unwrap_or("") 16 - .split("at://") 17 - .last() 18 - .unwrap_or(""); 15 + let handle = response["alsoKnownAs"][0] 16 + .as_str() 17 + .unwrap_or("") 18 + .split("at://") 19 + .last() 20 + .unwrap_or(""); 19 21 20 - let service_endpoint = response["service"][0]["serviceEndpoint"].as_str().unwrap_or(""); 22 + let service_endpoint = response["service"][0]["serviceEndpoint"] 23 + .as_str() 24 + .unwrap_or(""); 21 25 22 - if service_endpoint.is_empty() { 23 - return Err(Error::msg("Invalid did")); 24 - } 26 + if service_endpoint.is_empty() { 27 + return Err(Error::msg("Invalid did")); 28 + } 25 29 26 - let client = reqwest::Client::new(); 27 - let mut response = client.get(format!("{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.actor.profile&rkey=self", service_endpoint, did)) 30 + let client = reqwest::Client::new(); 31 + let mut response = client.get(format!("{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.actor.profile&rkey=self", service_endpoint, did)) 28 32 .header("Accept", "application/json") 29 33 .send() 30 34 .await? 31 35 .json::<ProfileResponse>() 32 36 .await?; 33 37 34 - response.value.handle = Some(handle.to_string()); 35 - Ok(response.value) 38 + response.value.handle = Some(handle.to_string()); 39 + Ok(response.value) 36 40 } 37 41 38 42 #[cfg(test)] 39 43 mod tests { 40 - use super::*; 41 - use anyhow::Result; 44 + use super::*; 45 + use anyhow::Result; 42 46 43 - #[tokio::test] 44 - async fn test_did_to_profile() -> Result<()> { 45 - let did = "did:plc:7vdlgi2bflelz7mmuxoqjfcr"; 46 - let profile = did_to_profile(did).await?; 47 + #[tokio::test] 48 + async fn test_did_to_profile() -> Result<()> { 49 + let did = "did:plc:7vdlgi2bflelz7mmuxoqjfcr"; 50 + let profile = did_to_profile(did).await?; 47 51 48 - assert_eq!(profile.r#type, "app.bsky.actor.profile"); 49 - assert!(profile.display_name.map(|s| s.starts_with("Tsiry Sandratraina")).unwrap_or(false)); 50 - assert!(profile.handle.map(|s| s == "tsiry-sandratraina.com").unwrap_or(false)); 52 + assert_eq!(profile.r#type, "app.bsky.actor.profile"); 53 + assert!(profile 54 + .display_name 55 + .map(|s| s.starts_with("Tsiry Sandratraina")) 56 + .unwrap_or(false)); 57 + assert!(profile 58 + .handle 59 + .map(|s| s == "tsiry-sandratraina.com") 60 + .unwrap_or(false)); 51 61 52 - let did = "did:plc:fgvx5xqinqoqgpfhito5er3s"; 53 - let profile = did_to_profile(did).await?; 62 + let did = "did:plc:fgvx5xqinqoqgpfhito5er3s"; 63 + let profile = did_to_profile(did).await?; 54 64 55 - assert_eq!(profile.r#type, "app.bsky.actor.profile"); 56 - assert!(profile.display_name.map(|s| s.starts_with("Lixtrix")).unwrap_or(false)); 57 - assert!(profile.handle.map(|s| s == "lixtrix.art").unwrap_or(false)); 65 + assert_eq!(profile.r#type, "app.bsky.actor.profile"); 66 + assert!(profile 67 + .display_name 68 + .map(|s| s.starts_with("Lixtrix")) 69 + .unwrap_or(false)); 70 + assert!(profile.handle.map(|s| s == "lixtrix.art").unwrap_or(false)); 58 71 59 - let did = "did:plc:d5jvs7uo4z6lw63zzreukgt4"; 60 - let profile = did_to_profile(did).await?; 61 - assert_eq!(profile.r#type, "app.bsky.actor.profile"); 72 + let did = "did:plc:d5jvs7uo4z6lw63zzreukgt4"; 73 + let profile = did_to_profile(did).await?; 74 + assert_eq!(profile.r#type, "app.bsky.actor.profile"); 62 75 63 - let did = "did:plc:gwxwdfmun3aqaiu5mx7nnyof"; 64 - let profile = did_to_profile(did).await?; 65 - assert_eq!(profile.r#type, "app.bsky.actor.profile"); 76 + let did = "did:plc:gwxwdfmun3aqaiu5mx7nnyof"; 77 + let profile = did_to_profile(did).await?; 78 + assert_eq!(profile.r#type, "app.bsky.actor.profile"); 66 79 67 - Ok(()) 68 - } 69 - } 80 + Ok(()) 81 + } 82 + }
+752 -539
crates/jetstream/src/repo.rs
··· 6 6 use sqlx::{Pool, Postgres}; 7 7 use tokio::sync::Mutex; 8 8 9 - use crate::{profile::did_to_profile, subscriber::{ALBUM_NSID, ARTIST_NSID, SCROBBLE_NSID, SONG_NSID}, types::{AlbumRecord, ArtistRecord, Commit, ScrobbleRecord, SongRecord}, xata::{album::Album, album_track::AlbumTrack, artist::Artist, artist_album::ArtistAlbum, artist_track::ArtistTrack, track::Track, user::User, user_album::UserAlbum, user_artist::UserArtist, user_track::UserTrack}}; 9 + use crate::{ 10 + profile::did_to_profile, 11 + subscriber::{ALBUM_NSID, ARTIST_NSID, SCROBBLE_NSID, SONG_NSID}, 12 + types::{AlbumRecord, ArtistRecord, Commit, ScrobbleRecord, SongRecord}, 13 + xata::{ 14 + album::Album, album_track::AlbumTrack, artist::Artist, artist_album::ArtistAlbum, 15 + artist_track::ArtistTrack, track::Track, user::User, user_album::UserAlbum, 16 + user_artist::UserArtist, user_track::UserTrack, 17 + }, 18 + }; 10 19 11 - pub async fn save_scrobble(pool: Arc<Mutex<Pool<Postgres>>>, did: &str, commit: Commit) -> Result<(), Error> { 12 - // skip unknown collection 13 - if !vec![ 14 - SCROBBLE_NSID, 15 - ARTIST_NSID, 16 - ALBUM_NSID, 17 - SONG_NSID, 18 - ].contains(&commit.collection.as_str()) { 19 - return Ok(()); 20 - } 20 + pub async fn save_scrobble( 21 + pool: Arc<Mutex<Pool<Postgres>>>, 22 + did: &str, 23 + commit: Commit, 24 + ) -> Result<(), Error> { 25 + // skip unknown collection 26 + if !vec![SCROBBLE_NSID, ARTIST_NSID, ALBUM_NSID, SONG_NSID] 27 + .contains(&commit.collection.as_str()) 28 + { 29 + return Ok(()); 30 + } 21 31 22 - let pool = pool.lock().await; 32 + let pool = pool.lock().await; 23 33 24 - match commit.operation.as_str() { 25 - "create" => { 26 - if commit.collection == SCROBBLE_NSID { 27 - let mut tx = pool.begin().await?; 28 - let scrobble_record: ScrobbleRecord = serde_json::from_value(commit.record.clone())?; 34 + match commit.operation.as_str() { 35 + "create" => { 36 + if commit.collection == SCROBBLE_NSID { 37 + let mut tx = pool.begin().await?; 38 + let scrobble_record: ScrobbleRecord = 39 + serde_json::from_value(commit.record.clone())?; 29 40 30 - let album_id = save_album(&mut tx, scrobble_record.clone(), did).await?; 31 - let artist_id = save_artist(&mut tx, scrobble_record.clone()).await?; 32 - let track_id = save_track(&mut tx, scrobble_record.clone(), did).await?; 41 + let album_id = save_album(&mut tx, scrobble_record.clone(), did).await?; 42 + let artist_id = save_artist(&mut tx, scrobble_record.clone()).await?; 43 + let track_id = save_track(&mut tx, scrobble_record.clone(), did).await?; 33 44 34 - save_album_track(&mut tx, &album_id, &track_id).await?; 35 - save_artist_track(&mut tx, &artist_id, &track_id).await?; 36 - save_artist_album(&mut tx, &artist_id, &album_id).await?; 45 + save_album_track(&mut tx, &album_id, &track_id).await?; 46 + save_artist_track(&mut tx, &artist_id, &track_id).await?; 47 + save_artist_album(&mut tx, &artist_id, &album_id).await?; 37 48 38 - let uri = format!("at://{}/app.rocksky.scrobble/{}", did, commit.rkey); 49 + let uri = format!("at://{}/app.rocksky.scrobble/{}", did, commit.rkey); 39 50 40 - let user_id = save_user(&mut tx, did).await?; 51 + let user_id = save_user(&mut tx, did).await?; 41 52 42 - println!("Saving scrobble: {} ", format!("{} - {} - {}", scrobble_record.title, scrobble_record.artist, scrobble_record.album).magenta()); 53 + println!( 54 + "Saving scrobble: {} ", 55 + format!( 56 + "{} - {} - {}", 57 + scrobble_record.title, scrobble_record.artist, scrobble_record.album 58 + ) 59 + .magenta() 60 + ); 43 61 44 - sqlx::query(r#" 62 + sqlx::query( 63 + r#" 45 64 INSERT INTO scrobbles ( 46 65 album_id, 47 66 artist_id, ··· 50 69 user_id, 51 70 timestamp 52 71 ) VALUES ($1, $2, $3, $4, $5, $6) 53 - "#) 54 - .bind(album_id) 55 - .bind(artist_id) 56 - .bind(track_id) 57 - .bind(uri) 58 - .bind(user_id) 59 - .bind(DateTime::parse_from_rfc3339(&scrobble_record.created_at).unwrap().with_timezone(&chrono::Utc)) 60 - .execute(&mut *tx).await?; 72 + "#, 73 + ) 74 + .bind(album_id) 75 + .bind(artist_id) 76 + .bind(track_id) 77 + .bind(uri) 78 + .bind(user_id) 79 + .bind( 80 + DateTime::parse_from_rfc3339(&scrobble_record.created_at) 81 + .unwrap() 82 + .with_timezone(&chrono::Utc), 83 + ) 84 + .execute(&mut *tx) 85 + .await?; 61 86 62 - tx.commit().await?; 63 - } 87 + tx.commit().await?; 88 + } 64 89 65 - if commit.collection == ARTIST_NSID { 66 - let mut tx = pool.begin().await?; 90 + if commit.collection == ARTIST_NSID { 91 + let mut tx = pool.begin().await?; 67 92 68 - let user_id = save_user(&mut tx, did).await?; 69 - let uri = format!("at://{}/app.rocksky.artist/{}", did, commit.rkey); 93 + let user_id = save_user(&mut tx, did).await?; 94 + let uri = format!("at://{}/app.rocksky.artist/{}", did, commit.rkey); 70 95 71 - let artist_record: ArtistRecord = serde_json::from_value(commit.record.clone())?; 72 - save_user_artist(&mut tx, &user_id, artist_record.clone(), &uri).await?; 73 - update_artist_uri(&mut tx, &user_id, artist_record, &uri).await?; 96 + let artist_record: ArtistRecord = serde_json::from_value(commit.record.clone())?; 97 + save_user_artist(&mut tx, &user_id, artist_record.clone(), &uri).await?; 98 + update_artist_uri(&mut tx, &user_id, artist_record, &uri).await?; 74 99 75 - tx.commit().await?; 76 - } 100 + tx.commit().await?; 101 + } 77 102 78 - if commit.collection == ALBUM_NSID { 79 - let mut tx = pool.begin().await?; 80 - let user_id = save_user(&mut tx, did).await?; 81 - let uri = format!("at://{}/app.rocksky.album/{}", did, commit.rkey); 103 + if commit.collection == ALBUM_NSID { 104 + let mut tx = pool.begin().await?; 105 + let user_id = save_user(&mut tx, did).await?; 106 + let uri = format!("at://{}/app.rocksky.album/{}", did, commit.rkey); 82 107 83 - let album_record: AlbumRecord = serde_json::from_value(commit.record.clone())?; 84 - save_user_album(&mut tx, &user_id, album_record.clone(), &uri).await?; 85 - update_album_uri(&mut tx, &user_id, album_record, &uri).await?; 86 - 87 - tx.commit().await?; 88 - } 108 + let album_record: AlbumRecord = serde_json::from_value(commit.record.clone())?; 109 + save_user_album(&mut tx, &user_id, album_record.clone(), &uri).await?; 110 + update_album_uri(&mut tx, &user_id, album_record, &uri).await?; 89 111 90 - if commit.collection == SONG_NSID { 91 - let mut tx = pool.begin().await?; 112 + tx.commit().await?; 113 + } 92 114 93 - let user_id = save_user(&mut tx, did).await?; 94 - let uri = format!("at://{}/app.rocksky.song/{}", did, commit.rkey); 115 + if commit.collection == SONG_NSID { 116 + let mut tx = pool.begin().await?; 95 117 96 - let song_record: SongRecord = serde_json::from_value(commit.record.clone())?; 97 - save_user_track(&mut tx, &user_id, song_record.clone(), &uri).await?; 98 - update_track_uri(&mut tx, &user_id, song_record, &uri).await?; 118 + let user_id = save_user(&mut tx, did).await?; 119 + let uri = format!("at://{}/app.rocksky.song/{}", did, commit.rkey); 99 120 100 - tx.commit().await?; 101 - } 121 + let song_record: SongRecord = serde_json::from_value(commit.record.clone())?; 122 + save_user_track(&mut tx, &user_id, song_record.clone(), &uri).await?; 123 + update_track_uri(&mut tx, &user_id, song_record, &uri).await?; 102 124 103 - }, 104 - _ => { 105 - println!("Unsupported operation: {}", commit.operation); 125 + tx.commit().await?; 126 + } 127 + } 128 + _ => { 129 + println!("Unsupported operation: {}", commit.operation); 130 + } 106 131 } 107 - } 108 - Ok(()) 132 + Ok(()) 109 133 } 110 134 135 + pub async fn save_user( 136 + tx: &mut sqlx::Transaction<'_, Postgres>, 137 + did: &str, 138 + ) -> Result<String, Error> { 139 + let profile = did_to_profile(did).await?; 111 140 112 - pub async fn save_user(tx: &mut sqlx::Transaction<'_, Postgres>, did: &str) -> Result<String, Error> { 113 - let profile = did_to_profile(did).await?; 141 + // Check if the user exists in the database 142 + let mut users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE did = $1") 143 + .bind(did) 144 + .fetch_all(&mut **tx) 145 + .await?; 114 146 115 - // Check if the user exists in the database 116 - let mut users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE did = $1") 117 - .bind(did) 118 - .fetch_all(&mut **tx) 119 - .await?; 147 + // If the user does not exist, create a new user 148 + if users.is_empty() { 149 + let avatar = profile.avatar.map(|blob| { 150 + format!( 151 + "https://cdn.bsky.app/img/avatar/plain/{}/{}@{}", 152 + did, 153 + blob.r#ref.link, 154 + blob.mime_type.split('/').last().unwrap_or("jpeg") 155 + ) 156 + }); 157 + sqlx::query( 158 + "INSERT INTO users (display_name, did, handle, avatar) VALUES ($1, $2, $3, $4)", 159 + ) 160 + .bind(profile.display_name) 161 + .bind(did) 162 + .bind(profile.handle) 163 + .bind(avatar) 164 + .execute(&mut **tx) 165 + .await?; 120 166 121 - // If the user does not exist, create a new user 122 - if users.is_empty() { 123 - let avatar = profile.avatar.map(|blob| format!("https://cdn.bsky.app/img/avatar/plain/{}/{}@{}", did, blob.r#ref.link, blob.mime_type.split('/').last().unwrap_or("jpeg"))); 124 - sqlx::query("INSERT INTO users (display_name, did, handle, avatar) VALUES ($1, $2, $3, $4)") 125 - .bind(profile.display_name) 126 - .bind(did) 127 - .bind(profile.handle) 128 - .bind(avatar) 129 - .execute(&mut **tx).await?; 130 - 131 - users = sqlx::query_as("SELECT * FROM users WHERE did = $1") 132 - .bind(did) 133 - .fetch_all(&mut **tx) 134 - .await?; 135 - } 167 + users = sqlx::query_as("SELECT * FROM users WHERE did = $1") 168 + .bind(did) 169 + .fetch_all(&mut **tx) 170 + .await?; 171 + } 136 172 137 - Ok(users[0].xata_id.clone()) 173 + Ok(users[0].xata_id.clone()) 138 174 } 139 175 140 - pub async fn save_track(tx: &mut sqlx::Transaction<'_, Postgres>, scrobble_record: ScrobbleRecord, did: &str) -> Result<String, Error> { 141 - let uri: Option<String> = None; 142 - let hash = sha256::digest( 143 - format!( 144 - "{} - {} - {}", 145 - scrobble_record.title, 146 - scrobble_record.artist, 147 - scrobble_record.album) 148 - .to_lowercase() 149 - ); 176 + pub async fn save_track( 177 + tx: &mut sqlx::Transaction<'_, Postgres>, 178 + scrobble_record: ScrobbleRecord, 179 + did: &str, 180 + ) -> Result<String, Error> { 181 + let uri: Option<String> = None; 182 + let hash = sha256::digest( 183 + format!( 184 + "{} - {} - {}", 185 + scrobble_record.title, scrobble_record.artist, scrobble_record.album 186 + ) 187 + .to_lowercase(), 188 + ); 150 189 151 - let tracks: Vec<Track> = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 152 - .bind(&hash) 153 - .fetch_all(&mut **tx) 154 - .await?; 190 + let tracks: Vec<Track> = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 191 + .bind(&hash) 192 + .fetch_all(&mut **tx) 193 + .await?; 155 194 156 - if !tracks.is_empty() { 157 - return Ok(tracks[0].xata_id.clone()); 158 - } 195 + if !tracks.is_empty() { 196 + return Ok(tracks[0].xata_id.clone()); 197 + } 159 198 160 - sqlx::query(r#" 199 + sqlx::query( 200 + r#" 161 201 INSERT INTO tracks ( 162 202 title, 163 203 artist, ··· 181 221 ) VALUES ( 182 222 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19 183 223 ) 184 - "#) 185 - .bind(scrobble_record.title) 186 - .bind(scrobble_record.artist) 187 - .bind(scrobble_record.album) 188 - .bind(scrobble_record.album_art.map(|x| format!("https://cdn.bsky.app/img/feed_thumbnail/plain/{}/{}@{}", did, x.r#ref.link, x.mime_type.split('/').last().unwrap_or("jpeg")))) 189 - .bind(scrobble_record.album_artist) 190 - .bind(scrobble_record.track_number) 191 - .bind(scrobble_record.duration) 192 - .bind(scrobble_record.mbid) 193 - .bind(scrobble_record.composer) 194 - .bind(scrobble_record.lyrics) 195 - .bind(scrobble_record.disc_number) 196 - .bind(&hash) 197 - .bind(scrobble_record.copyright_message) 198 - .bind(uri) 199 - .bind(scrobble_record.spotify_link) 200 - .bind(scrobble_record.apple_music_link) 201 - .bind(scrobble_record.tidal_link) 202 - .bind(scrobble_record.youtube_link) 203 - .bind(scrobble_record.label) 204 - .execute(&mut **tx).await?; 205 - 206 - let tracks: Vec<Track> = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 224 + "#, 225 + ) 226 + .bind(scrobble_record.title) 227 + .bind(scrobble_record.artist) 228 + .bind(scrobble_record.album) 229 + .bind(scrobble_record.album_art.map(|x| { 230 + format!( 231 + "https://cdn.bsky.app/img/feed_thumbnail/plain/{}/{}@{}", 232 + did, 233 + x.r#ref.link, 234 + x.mime_type.split('/').last().unwrap_or("jpeg") 235 + ) 236 + })) 237 + .bind(scrobble_record.album_artist) 238 + .bind(scrobble_record.track_number) 239 + .bind(scrobble_record.duration) 240 + .bind(scrobble_record.mbid) 241 + .bind(scrobble_record.composer) 242 + .bind(scrobble_record.lyrics) 243 + .bind(scrobble_record.disc_number) 207 244 .bind(&hash) 208 - .fetch_all(&mut **tx) 245 + .bind(scrobble_record.copyright_message) 246 + .bind(uri) 247 + .bind(scrobble_record.spotify_link) 248 + .bind(scrobble_record.apple_music_link) 249 + .bind(scrobble_record.tidal_link) 250 + .bind(scrobble_record.youtube_link) 251 + .bind(scrobble_record.label) 252 + .execute(&mut **tx) 209 253 .await?; 210 254 211 - Ok(tracks[0].xata_id.clone()) 255 + let tracks: Vec<Track> = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 256 + .bind(&hash) 257 + .fetch_all(&mut **tx) 258 + .await?; 259 + 260 + Ok(tracks[0].xata_id.clone()) 212 261 } 213 262 214 - pub async fn save_album(tx: &mut sqlx::Transaction<'_, Postgres>, scrobble_record: ScrobbleRecord, did: &str) -> Result<String, Error> { 215 - let hash = sha256::digest(format!( 216 - "{} - {}", 217 - scrobble_record.album, 218 - scrobble_record.album_artist 219 - ) 220 - .to_lowercase() 221 - ); 263 + pub async fn save_album( 264 + tx: &mut sqlx::Transaction<'_, Postgres>, 265 + scrobble_record: ScrobbleRecord, 266 + did: &str, 267 + ) -> Result<String, Error> { 268 + let hash = sha256::digest( 269 + format!( 270 + "{} - {}", 271 + scrobble_record.album, scrobble_record.album_artist 272 + ) 273 + .to_lowercase(), 274 + ); 222 275 223 - let albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 224 - .bind(&hash) 225 - .fetch_all(&mut **tx) 226 - .await?; 276 + let albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 277 + .bind(&hash) 278 + .fetch_all(&mut **tx) 279 + .await?; 227 280 228 - if !albums.is_empty() { 229 - println!("Album already exists: {}", albums[0].title.magenta()); 230 - return Ok(albums[0].xata_id.clone()); 231 - } 281 + if !albums.is_empty() { 282 + println!("Album already exists: {}", albums[0].title.magenta()); 283 + return Ok(albums[0].xata_id.clone()); 284 + } 232 285 233 - println!("Saving album: {}", scrobble_record.album.magenta()); 286 + println!("Saving album: {}", scrobble_record.album.magenta()); 234 287 235 - let uri: Option<String> = None; 236 - let artist_uri: Option<String> = None; 237 - sqlx::query(r#" 288 + let uri: Option<String> = None; 289 + let artist_uri: Option<String> = None; 290 + sqlx::query( 291 + r#" 238 292 INSERT INTO albums ( 239 293 title, 240 294 artist, ··· 247 301 ) VALUES ( 248 302 $1, $2, $3, $4, $5, $6, $7, $8 249 303 ) 250 - "#) 251 - .bind(scrobble_record.album) 252 - .bind(scrobble_record.album_artist) 253 - .bind(scrobble_record.album_art.map(|x| format!("https://cdn.bsky.app/img/feed_thumbnail/plain/{}/{}@{}", did, x.r#ref.link, x.mime_type.split('/').last().unwrap_or("jpeg")))) 254 - .bind(scrobble_record.year) 255 - .bind(scrobble_record.release_date) 256 - .bind(&hash) 257 - .bind(uri) 258 - .bind(artist_uri) 259 - .execute(&mut **tx).await?; 260 - 261 - let albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 304 + "#, 305 + ) 306 + .bind(scrobble_record.album) 307 + .bind(scrobble_record.album_artist) 308 + .bind(scrobble_record.album_art.map(|x| { 309 + format!( 310 + "https://cdn.bsky.app/img/feed_thumbnail/plain/{}/{}@{}", 311 + did, 312 + x.r#ref.link, 313 + x.mime_type.split('/').last().unwrap_or("jpeg") 314 + ) 315 + })) 316 + .bind(scrobble_record.year) 317 + .bind(scrobble_record.release_date) 262 318 .bind(&hash) 263 - .fetch_all(&mut **tx) 319 + .bind(uri) 320 + .bind(artist_uri) 321 + .execute(&mut **tx) 264 322 .await?; 265 323 266 - Ok(albums[0].xata_id.clone()) 324 + let albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 325 + .bind(&hash) 326 + .fetch_all(&mut **tx) 327 + .await?; 328 + 329 + Ok(albums[0].xata_id.clone()) 267 330 } 268 331 269 - pub async fn save_artist(tx: &mut sqlx::Transaction<'_, Postgres>, scrobble_record: ScrobbleRecord) -> Result<String, Error> { 270 - let hash = sha256::digest(scrobble_record.album_artist.to_lowercase()); 271 - let artists: Vec<Artist> = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 272 - .bind(&hash) 273 - .fetch_all(&mut **tx) 274 - .await?; 332 + pub async fn save_artist( 333 + tx: &mut sqlx::Transaction<'_, Postgres>, 334 + scrobble_record: ScrobbleRecord, 335 + ) -> Result<String, Error> { 336 + let hash = sha256::digest(scrobble_record.album_artist.to_lowercase()); 337 + let artists: Vec<Artist> = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 338 + .bind(&hash) 339 + .fetch_all(&mut **tx) 340 + .await?; 275 341 276 - if !artists.is_empty() { 277 - println!("Artist already exists: {}", artists[0].name.magenta()); 278 - return Ok(artists[0].xata_id.clone()); 279 - } 342 + if !artists.is_empty() { 343 + println!("Artist already exists: {}", artists[0].name.magenta()); 344 + return Ok(artists[0].xata_id.clone()); 345 + } 280 346 281 - println!("Saving artist: {}", scrobble_record.album_artist.magenta()); 347 + println!("Saving artist: {}", scrobble_record.album_artist.magenta()); 282 348 283 - let uri: Option<String> = None; 284 - let picture = ""; 285 - sqlx::query(r#" 349 + let uri: Option<String> = None; 350 + let picture = ""; 351 + sqlx::query( 352 + r#" 286 353 INSERT INTO artists ( 287 354 name, 288 355 sha256, ··· 291 358 ) VALUES ( 292 359 $1, $2, $3, $4 293 360 ) 294 - "#) 295 - .bind(scrobble_record.artist) 296 - .bind(&hash) 297 - .bind(uri) 298 - .bind(picture) 299 - .execute(&mut **tx).await?; 300 - 301 - let artists: Vec<Artist> = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 361 + "#, 362 + ) 363 + .bind(scrobble_record.artist) 302 364 .bind(&hash) 303 - .fetch_all(&mut **tx) 365 + .bind(uri) 366 + .bind(picture) 367 + .execute(&mut **tx) 304 368 .await?; 305 369 306 - Ok(artists[0].xata_id.clone()) 370 + let artists: Vec<Artist> = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 371 + .bind(&hash) 372 + .fetch_all(&mut **tx) 373 + .await?; 374 + 375 + Ok(artists[0].xata_id.clone()) 307 376 } 308 377 309 - pub async fn save_album_track(tx: &mut sqlx::Transaction<'_, Postgres>, album_id: &str, track_id: &str) -> Result<(), Error> { 310 - let album_tracks : Vec<AlbumTrack> = sqlx::query_as("SELECT * FROM album_tracks WHERE album_id = $1 AND track_id = $2") 311 - .bind(album_id) 312 - .bind(track_id) 313 - .fetch_all(&mut **tx) 314 - .await?; 378 + pub async fn save_album_track( 379 + tx: &mut sqlx::Transaction<'_, Postgres>, 380 + album_id: &str, 381 + track_id: &str, 382 + ) -> Result<(), Error> { 383 + let album_tracks: Vec<AlbumTrack> = 384 + sqlx::query_as("SELECT * FROM album_tracks WHERE album_id = $1 AND track_id = $2") 385 + .bind(album_id) 386 + .bind(track_id) 387 + .fetch_all(&mut **tx) 388 + .await?; 315 389 316 - if !album_tracks.is_empty() { 317 - println!("Album track already exists: {}", format!("{} - {}", album_id, track_id).magenta()); 318 - return Ok(()); 319 - } 390 + if !album_tracks.is_empty() { 391 + println!( 392 + "Album track already exists: {}", 393 + format!("{} - {}", album_id, track_id).magenta() 394 + ); 395 + return Ok(()); 396 + } 320 397 321 - println!("Saving album track: {}", format!("{} - {}", album_id, track_id).magenta()); 398 + println!( 399 + "Saving album track: {}", 400 + format!("{} - {}", album_id, track_id).magenta() 401 + ); 322 402 323 - sqlx::query(r#" 403 + sqlx::query( 404 + r#" 324 405 INSERT INTO album_tracks ( 325 406 album_id, 326 407 track_id 327 408 ) VALUES ( 328 409 $1, $2 329 410 ) 330 - "#) 331 - .bind(album_id) 332 - .bind(track_id) 333 - .execute(&mut **tx).await?; 334 - Ok(()) 411 + "#, 412 + ) 413 + .bind(album_id) 414 + .bind(track_id) 415 + .execute(&mut **tx) 416 + .await?; 417 + Ok(()) 335 418 } 336 419 337 - pub async fn save_artist_track(tx: &mut sqlx::Transaction<'_, Postgres>, artist_id: &str, track_id: &str) -> Result<(), Error> { 338 - let artist_tracks : Vec<ArtistTrack> = sqlx::query_as("SELECT * FROM artist_tracks WHERE artist_id = $1 AND track_id = $2") 339 - .bind(artist_id) 340 - .bind(track_id) 341 - .fetch_all(&mut **tx) 342 - .await?; 420 + pub async fn save_artist_track( 421 + tx: &mut sqlx::Transaction<'_, Postgres>, 422 + artist_id: &str, 423 + track_id: &str, 424 + ) -> Result<(), Error> { 425 + let artist_tracks: Vec<ArtistTrack> = 426 + sqlx::query_as("SELECT * FROM artist_tracks WHERE artist_id = $1 AND track_id = $2") 427 + .bind(artist_id) 428 + .bind(track_id) 429 + .fetch_all(&mut **tx) 430 + .await?; 343 431 344 - if !artist_tracks.is_empty() { 345 - println!("Artist track already exists: {}", format!("{} - {}", artist_id, track_id).magenta()); 346 - return Ok(()); 347 - } 432 + if !artist_tracks.is_empty() { 433 + println!( 434 + "Artist track already exists: {}", 435 + format!("{} - {}", artist_id, track_id).magenta() 436 + ); 437 + return Ok(()); 438 + } 348 439 349 - println!("Saving artist track: {}", format!("{} - {}", artist_id, track_id).magenta()); 440 + println!( 441 + "Saving artist track: {}", 442 + format!("{} - {}", artist_id, track_id).magenta() 443 + ); 350 444 351 - sqlx::query(r#" 445 + sqlx::query( 446 + r#" 352 447 INSERT INTO artist_tracks ( 353 448 artist_id, 354 449 track_id 355 450 ) VALUES ( 356 451 $1, $2 357 452 ) 358 - "#) 359 - .bind(artist_id) 360 - .bind(track_id) 361 - .execute(&mut **tx).await?; 362 - Ok(()) 453 + "#, 454 + ) 455 + .bind(artist_id) 456 + .bind(track_id) 457 + .execute(&mut **tx) 458 + .await?; 459 + Ok(()) 363 460 } 364 461 365 - pub async fn save_artist_album(tx: &mut sqlx::Transaction<'_, Postgres>, artist_id: &str, album_id: &str) -> Result<(), Error> { 366 - let artist_albums : Vec<ArtistAlbum> = sqlx::query_as("SELECT * FROM artist_albums WHERE artist_id = $1 AND album_id = $2") 367 - .bind(artist_id) 368 - .bind(album_id) 369 - .fetch_all(&mut **tx) 370 - .await?; 462 + pub async fn save_artist_album( 463 + tx: &mut sqlx::Transaction<'_, Postgres>, 464 + artist_id: &str, 465 + album_id: &str, 466 + ) -> Result<(), Error> { 467 + let artist_albums: Vec<ArtistAlbum> = 468 + sqlx::query_as("SELECT * FROM artist_albums WHERE artist_id = $1 AND album_id = $2") 469 + .bind(artist_id) 470 + .bind(album_id) 471 + .fetch_all(&mut **tx) 472 + .await?; 371 473 372 - if !artist_albums.is_empty() { 373 - println!("Artist album already exists: {}", format!("{} - {}", artist_id, album_id).magenta()); 374 - return Ok(()); 375 - } 474 + if !artist_albums.is_empty() { 475 + println!( 476 + "Artist album already exists: {}", 477 + format!("{} - {}", artist_id, album_id).magenta() 478 + ); 479 + return Ok(()); 480 + } 376 481 377 - println!("Saving artist album: {}", format!("{} - {}", artist_id, album_id).magenta()); 482 + println!( 483 + "Saving artist album: {}", 484 + format!("{} - {}", artist_id, album_id).magenta() 485 + ); 378 486 379 - sqlx::query(r#" 487 + sqlx::query( 488 + r#" 380 489 INSERT INTO artist_albums ( 381 490 artist_id, 382 491 album_id 383 492 ) VALUES ( 384 493 $1, $2 385 494 ) 386 - "#) 387 - .bind(artist_id) 388 - .bind(album_id) 389 - .execute(&mut **tx).await?; 390 - Ok(()) 495 + "#, 496 + ) 497 + .bind(artist_id) 498 + .bind(album_id) 499 + .execute(&mut **tx) 500 + .await?; 501 + Ok(()) 391 502 } 392 503 393 - 394 - pub async fn save_user_artist(tx: &mut sqlx::Transaction<'_, Postgres>, user_id: &str, record: ArtistRecord, uri: &str) -> Result<(), Error> { 395 - let hash = sha256::digest(record.name.to_lowercase()); 504 + pub async fn save_user_artist( 505 + tx: &mut sqlx::Transaction<'_, Postgres>, 506 + user_id: &str, 507 + record: ArtistRecord, 508 + uri: &str, 509 + ) -> Result<(), Error> { 510 + let hash = sha256::digest(record.name.to_lowercase()); 396 511 397 - let mut artists: Vec<Artist> = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 398 - .bind(&hash) 399 - .fetch_all(&mut **tx) 400 - .await?; 512 + let mut artists: Vec<Artist> = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 513 + .bind(&hash) 514 + .fetch_all(&mut **tx) 515 + .await?; 401 516 402 - let users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE xata_id = $1") 403 - .bind(user_id) 404 - .fetch_all(&mut **tx) 405 - .await?; 517 + let users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE xata_id = $1") 518 + .bind(user_id) 519 + .fetch_all(&mut **tx) 520 + .await?; 406 521 407 - let artist_id: &str; 522 + let artist_id: &str; 408 523 409 - match artists.is_empty() { 410 - true => { 411 - println!("Saving artist: {}", record.name.magenta()); 412 - let did = users[0].did.clone(); 413 - sqlx::query(r#" 524 + match artists.is_empty() { 525 + true => { 526 + println!("Saving artist: {}", record.name.magenta()); 527 + let did = users[0].did.clone(); 528 + sqlx::query( 529 + r#" 414 530 INSERT INTO artists ( 415 531 name, 416 532 sha256, ··· 419 535 ) VALUES ( 420 536 $1, $2, $3, $4 421 537 ) 422 - "#) 423 - .bind(record.name) 424 - .bind(&hash) 425 - .bind(uri) 426 - .bind(record.picture.map(|x| format!("https://cdn.bsky.app/img/avatar/plain/{}/{}@{}", did, x.r#ref.link, x.mime_type.split('/').last().unwrap_or("jpeg")))) 427 - .execute(&mut **tx).await?; 538 + "#, 539 + ) 540 + .bind(record.name) 541 + .bind(&hash) 542 + .bind(uri) 543 + .bind(record.picture.map(|x| { 544 + format!( 545 + "https://cdn.bsky.app/img/avatar/plain/{}/{}@{}", 546 + did, 547 + x.r#ref.link, 548 + x.mime_type.split('/').last().unwrap_or("jpeg") 549 + ) 550 + })) 551 + .execute(&mut **tx) 552 + .await?; 428 553 429 - artists = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 430 - .bind(&hash) 431 - .fetch_all(&mut **tx) 432 - .await?; 433 - artist_id = &artists[0].xata_id; 434 - }, 435 - false => { 436 - artist_id = &artists[0].xata_id; 437 - } 438 - }; 554 + artists = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 555 + .bind(&hash) 556 + .fetch_all(&mut **tx) 557 + .await?; 558 + artist_id = &artists[0].xata_id; 559 + } 560 + false => { 561 + artist_id = &artists[0].xata_id; 562 + } 563 + }; 439 564 440 - let user_artists: Vec<UserArtist> = sqlx::query_as("SELECT * FROM user_artists WHERE user_id = $1 AND artist_id = $2") 441 - .bind(user_id) 442 - .bind(artist_id) 443 - .fetch_all(&mut **tx) 444 - .await?; 565 + let user_artists: Vec<UserArtist> = 566 + sqlx::query_as("SELECT * FROM user_artists WHERE user_id = $1 AND artist_id = $2") 567 + .bind(user_id) 568 + .bind(artist_id) 569 + .fetch_all(&mut **tx) 570 + .await?; 445 571 446 - if !user_artists.is_empty() { 447 - println!("User artist already exists: {}", format!("{} - {}", user_id, artist_id).magenta()); 448 - sqlx::query(r#" 572 + if !user_artists.is_empty() { 573 + println!( 574 + "User artist already exists: {}", 575 + format!("{} - {}", user_id, artist_id).magenta() 576 + ); 577 + sqlx::query( 578 + r#" 449 579 UPDATE user_artists 450 580 SET scrobbles = scrobbles + 1, 451 581 uri = $3 452 582 WHERE user_id = $1 AND artist_id = $2 453 - "#) 454 - .bind(user_id) 455 - .bind(artist_id) 456 - .bind(uri) 457 - .execute(&mut **tx).await?; 458 - return Ok(()); 459 - } 583 + "#, 584 + ) 585 + .bind(user_id) 586 + .bind(artist_id) 587 + .bind(uri) 588 + .execute(&mut **tx) 589 + .await?; 590 + return Ok(()); 591 + } 460 592 461 - println!("Saving user artist: {}", format!("{} - {}", user_id, artist_id).magenta()); 593 + println!( 594 + "Saving user artist: {}", 595 + format!("{} - {}", user_id, artist_id).magenta() 596 + ); 462 597 463 - sqlx::query(r#" 598 + sqlx::query( 599 + r#" 464 600 INSERT INTO user_artists ( 465 601 user_id, 466 602 artist_id, ··· 469 605 ) VALUES ( 470 606 $1, $2, $3, $4 471 607 ) 472 - "#) 473 - .bind(user_id) 474 - .bind(artist_id) 475 - .bind(uri) 476 - .bind(1) 477 - .execute(&mut **tx).await?; 478 - Ok(()) 608 + "#, 609 + ) 610 + .bind(user_id) 611 + .bind(artist_id) 612 + .bind(uri) 613 + .bind(1) 614 + .execute(&mut **tx) 615 + .await?; 616 + Ok(()) 479 617 } 480 618 481 - pub async fn save_user_album(tx: &mut sqlx::Transaction<'_, Postgres>, user_id: &str, record: AlbumRecord, uri: &str) -> Result<(), Error> { 482 - let users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE xata_id = $1") 483 - .bind(user_id) 484 - .fetch_all(&mut **tx) 485 - .await?; 619 + pub async fn save_user_album( 620 + tx: &mut sqlx::Transaction<'_, Postgres>, 621 + user_id: &str, 622 + record: AlbumRecord, 623 + uri: &str, 624 + ) -> Result<(), Error> { 625 + let users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE xata_id = $1") 626 + .bind(user_id) 627 + .fetch_all(&mut **tx) 628 + .await?; 486 629 487 - let hash = sha256::digest(format!( 488 - "{} - {}", 489 - record.title, 490 - record.artist 491 - ) 492 - .to_lowercase() 493 - ); 494 - let mut albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 495 - .bind(&hash) 496 - .fetch_all(&mut **tx) 497 - .await?; 630 + let hash = sha256::digest(format!("{} - {}", record.title, record.artist).to_lowercase()); 631 + let mut albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 632 + .bind(&hash) 633 + .fetch_all(&mut **tx) 634 + .await?; 498 635 499 - let album_id: &str; 636 + let album_id: &str; 500 637 501 - match albums.is_empty() { 502 - true => { 503 - println!("Saving album: {}", record.title.magenta()); 504 - let did = users[0].did.clone(); 505 - sqlx::query(r#" 638 + match albums.is_empty() { 639 + true => { 640 + println!("Saving album: {}", record.title.magenta()); 641 + let did = users[0].did.clone(); 642 + sqlx::query( 643 + r#" 506 644 INSERT INTO albums ( 507 645 title, 508 646 artist, ··· 514 652 ) VALUES ( 515 653 $1, $2, $3, $4, $5, $6, $7 516 654 ) 517 - "#) 518 - .bind(record.title) 519 - .bind(record.artist) 520 - .bind(record.album_art.map(|x| format!("https://cdn.bsky.app/img/feed_thumbnail/plain/{}/{}@{}", did, x.r#ref.link, x.mime_type.split('/').last().unwrap_or("jpeg")))) 521 - .bind(record.year) 522 - .bind(record.release_date) 523 - .bind(&hash) 524 - .bind(uri) 525 - .execute(&mut **tx).await?; 655 + "#, 656 + ) 657 + .bind(record.title) 658 + .bind(record.artist) 659 + .bind(record.album_art.map(|x| { 660 + format!( 661 + "https://cdn.bsky.app/img/feed_thumbnail/plain/{}/{}@{}", 662 + did, 663 + x.r#ref.link, 664 + x.mime_type.split('/').last().unwrap_or("jpeg") 665 + ) 666 + })) 667 + .bind(record.year) 668 + .bind(record.release_date) 669 + .bind(&hash) 670 + .bind(uri) 671 + .execute(&mut **tx) 672 + .await?; 526 673 527 - albums = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 528 - .bind(&hash) 529 - .fetch_all(&mut **tx) 530 - .await?; 531 - album_id = &albums[0].xata_id; 532 - }, 533 - false => { 534 - album_id = &albums[0].xata_id; 535 - } 536 - }; 674 + albums = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 675 + .bind(&hash) 676 + .fetch_all(&mut **tx) 677 + .await?; 678 + album_id = &albums[0].xata_id; 679 + } 680 + false => { 681 + album_id = &albums[0].xata_id; 682 + } 683 + }; 537 684 538 - let user_albums: Vec<UserAlbum> = sqlx::query_as("SELECT * FROM user_albums WHERE user_id = $1 AND album_id = $2") 539 - .bind(user_id) 540 - .bind(album_id) 541 - .fetch_all(&mut **tx) 542 - .await?; 685 + let user_albums: Vec<UserAlbum> = 686 + sqlx::query_as("SELECT * FROM user_albums WHERE user_id = $1 AND album_id = $2") 687 + .bind(user_id) 688 + .bind(album_id) 689 + .fetch_all(&mut **tx) 690 + .await?; 543 691 544 - if !user_albums.is_empty() { 545 - println!("User album already exists: {}", format!("{} - {}", user_id, album_id).magenta()); 546 - sqlx::query(r#" 692 + if !user_albums.is_empty() { 693 + println!( 694 + "User album already exists: {}", 695 + format!("{} - {}", user_id, album_id).magenta() 696 + ); 697 + sqlx::query( 698 + r#" 547 699 UPDATE user_albums 548 700 SET scrobbles = scrobbles + 1, 549 701 uri = $3 550 702 WHERE user_id = $1 AND album_id = $2 551 - "#) 552 - .bind(user_id) 553 - .bind(album_id) 554 - .bind(uri) 555 - .execute(&mut **tx).await?; 556 - return Ok(()); 557 - } 703 + "#, 704 + ) 705 + .bind(user_id) 706 + .bind(album_id) 707 + .bind(uri) 708 + .execute(&mut **tx) 709 + .await?; 710 + return Ok(()); 711 + } 558 712 559 - println!("Saving user album: {}", format!("{} - {}", user_id, album_id).magenta()); 713 + println!( 714 + "Saving user album: {}", 715 + format!("{} - {}", user_id, album_id).magenta() 716 + ); 560 717 561 - sqlx::query(r#" 718 + sqlx::query( 719 + r#" 562 720 INSERT INTO user_albums ( 563 721 user_id, 564 722 album_id, ··· 567 725 ) VALUES ( 568 726 $1, $2, $3, $4 569 727 ) 570 - "#) 571 - .bind(user_id) 572 - .bind(album_id) 573 - .bind(uri) 574 - .bind(1) 575 - .execute(&mut **tx).await?; 576 - Ok(()) 728 + "#, 729 + ) 730 + .bind(user_id) 731 + .bind(album_id) 732 + .bind(uri) 733 + .bind(1) 734 + .execute(&mut **tx) 735 + .await?; 736 + Ok(()) 577 737 } 578 738 579 - pub async fn save_user_track(tx: &mut sqlx::Transaction<'_, Postgres>, user_id: &str, record: SongRecord, uri: &str) -> Result<(), Error> { 580 - let hash = sha256::digest(format!( 581 - "{} - {} - {}", 582 - record.title, 583 - record.artist, 584 - record.album 585 - ) 586 - .to_lowercase() 587 - ); 739 + pub async fn save_user_track( 740 + tx: &mut sqlx::Transaction<'_, Postgres>, 741 + user_id: &str, 742 + record: SongRecord, 743 + uri: &str, 744 + ) -> Result<(), Error> { 745 + let hash = sha256::digest( 746 + format!("{} - {} - {}", record.title, record.artist, record.album).to_lowercase(), 747 + ); 588 748 589 - let mut tracks: Vec<Track> = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 590 - .bind(&hash) 591 - .fetch_all(&mut **tx) 592 - .await?; 749 + let mut tracks: Vec<Track> = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 750 + .bind(&hash) 751 + .fetch_all(&mut **tx) 752 + .await?; 593 753 594 - let users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE xata_id = $1") 595 - .bind(user_id) 596 - .fetch_all(&mut **tx) 597 - .await?; 754 + let users: Vec<User> = sqlx::query_as("SELECT * FROM users WHERE xata_id = $1") 755 + .bind(user_id) 756 + .fetch_all(&mut **tx) 757 + .await?; 598 758 599 - let track_id: &str; 759 + let track_id: &str; 600 760 601 - match tracks.is_empty() { 602 - true => { 603 - println!("Saving track: {}", record.title.magenta()); 604 - let did = users[0].did.clone(); 605 - sqlx::query(r#" 761 + match tracks.is_empty() { 762 + true => { 763 + println!("Saving track: {}", record.title.magenta()); 764 + let did = users[0].did.clone(); 765 + sqlx::query( 766 + r#" 606 767 INSERT INTO tracks ( 607 768 title, 608 769 artist, ··· 623 784 ) VALUES ( 624 785 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 625 786 ) 626 - "#) 627 - .bind(record.title) 628 - .bind(record.artist) 629 - .bind(record.album) 630 - .bind(record.album_art.map(|x| format!("https://cdn.bsky.app/img/feed_thumbnail/plain/{}/{}@{}", did, x.r#ref.link, x.mime_type.split('/').last().unwrap_or("jpeg")))) 631 - .bind(record.album_artist) 632 - .bind(record.track_number) 633 - .bind(record.duration) 634 - .bind(record.mbid) 635 - .bind(record.composer) 636 - .bind(record.lyrics) 637 - .bind(record.disc_number) 638 - .bind(&hash) 639 - .bind(record.copyright_message) 640 - .bind(uri) 641 - .bind(record.spotify_link) 642 - .bind(record.label) 643 - .execute(&mut **tx).await?; 787 + "#, 788 + ) 789 + .bind(record.title) 790 + .bind(record.artist) 791 + .bind(record.album) 792 + .bind(record.album_art.map(|x| { 793 + format!( 794 + "https://cdn.bsky.app/img/feed_thumbnail/plain/{}/{}@{}", 795 + did, 796 + x.r#ref.link, 797 + x.mime_type.split('/').last().unwrap_or("jpeg") 798 + ) 799 + })) 800 + .bind(record.album_artist) 801 + .bind(record.track_number) 802 + .bind(record.duration) 803 + .bind(record.mbid) 804 + .bind(record.composer) 805 + .bind(record.lyrics) 806 + .bind(record.disc_number) 807 + .bind(&hash) 808 + .bind(record.copyright_message) 809 + .bind(uri) 810 + .bind(record.spotify_link) 811 + .bind(record.label) 812 + .execute(&mut **tx) 813 + .await?; 644 814 645 - tracks = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 646 - .bind(&hash) 647 - .fetch_all(&mut **tx) 648 - .await?; 815 + tracks = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 816 + .bind(&hash) 817 + .fetch_all(&mut **tx) 818 + .await?; 649 819 650 - track_id = &tracks[0].xata_id; 651 - }, 652 - false => { 653 - track_id = &tracks[0].xata_id; 820 + track_id = &tracks[0].xata_id; 821 + } 822 + false => { 823 + track_id = &tracks[0].xata_id; 824 + } 654 825 } 655 - } 656 826 657 - let user_tracks: Vec<UserTrack> = sqlx::query_as("SELECT * FROM user_tracks WHERE user_id = $1 AND track_id = $2") 658 - .bind(user_id) 659 - .bind(track_id) 660 - .fetch_all(&mut **tx) 661 - .await?; 827 + let user_tracks: Vec<UserTrack> = 828 + sqlx::query_as("SELECT * FROM user_tracks WHERE user_id = $1 AND track_id = $2") 829 + .bind(user_id) 830 + .bind(track_id) 831 + .fetch_all(&mut **tx) 832 + .await?; 662 833 663 - if !user_tracks.is_empty() { 664 - println!("User track already exists: {}", format!("{} - {}", user_id, track_id).magenta()); 665 - sqlx::query(r#" 834 + if !user_tracks.is_empty() { 835 + println!( 836 + "User track already exists: {}", 837 + format!("{} - {}", user_id, track_id).magenta() 838 + ); 839 + sqlx::query( 840 + r#" 666 841 UPDATE user_tracks 667 842 SET scrobbles = scrobbles + 1, 668 843 uri = $3 669 844 WHERE user_id = $1 AND track_id = $2 670 - "#) 671 - .bind(user_id) 672 - .bind(track_id) 673 - .bind(uri) 674 - .execute(&mut **tx).await?; 675 - return Ok(()); 676 - } 845 + "#, 846 + ) 847 + .bind(user_id) 848 + .bind(track_id) 849 + .bind(uri) 850 + .execute(&mut **tx) 851 + .await?; 852 + return Ok(()); 853 + } 677 854 678 - println!("Saving user track: {}", format!("{} - {}", user_id, track_id).magenta()); 855 + println!( 856 + "Saving user track: {}", 857 + format!("{} - {}", user_id, track_id).magenta() 858 + ); 679 859 680 - sqlx::query(r#" 860 + sqlx::query( 861 + r#" 681 862 INSERT INTO user_tracks ( 682 863 user_id, 683 864 track_id, ··· 686 867 ) VALUES ( 687 868 $1, $2, $3, $4 688 869 ) 689 - "#) 690 - .bind(user_id) 691 - .bind(track_id) 692 - .bind(uri) 693 - .bind(1) 694 - .execute(&mut **tx).await?; 870 + "#, 871 + ) 872 + .bind(user_id) 873 + .bind(track_id) 874 + .bind(uri) 875 + .bind(1) 876 + .execute(&mut **tx) 877 + .await?; 695 878 696 - Ok(()) 879 + Ok(()) 697 880 } 698 881 699 - pub async fn update_artist_uri(tx: &mut sqlx::Transaction<'_, Postgres>, user_id: &str, record: ArtistRecord, uri: &str) -> Result<(), Error> { 700 - let hash = sha256::digest(record.name.to_lowercase()); 701 - let artists: Vec<Artist> = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 702 - .bind(&hash) 703 - .fetch_all(&mut **tx) 704 - .await?; 882 + pub async fn update_artist_uri( 883 + tx: &mut sqlx::Transaction<'_, Postgres>, 884 + user_id: &str, 885 + record: ArtistRecord, 886 + uri: &str, 887 + ) -> Result<(), Error> { 888 + let hash = sha256::digest(record.name.to_lowercase()); 889 + let artists: Vec<Artist> = sqlx::query_as("SELECT * FROM artists WHERE sha256 = $1") 890 + .bind(&hash) 891 + .fetch_all(&mut **tx) 892 + .await?; 705 893 706 - if artists.is_empty() { 707 - println!("Artist not found: {}", record.name.magenta()); 708 - return Ok(()); 709 - } 894 + if artists.is_empty() { 895 + println!("Artist not found: {}", record.name.magenta()); 896 + return Ok(()); 897 + } 710 898 711 - let artist_id = &artists[0].xata_id; 899 + let artist_id = &artists[0].xata_id; 712 900 713 - sqlx::query(r#" 901 + sqlx::query( 902 + r#" 714 903 UPDATE user_artists 715 904 SET uri = $3 716 905 WHERE user_id = $1 AND artist_id = $2 717 - "#) 718 - .bind(user_id) 719 - .bind(artist_id) 720 - .bind(uri) 721 - .execute(&mut **tx).await?; 906 + "#, 907 + ) 908 + .bind(user_id) 909 + .bind(artist_id) 910 + .bind(uri) 911 + .execute(&mut **tx) 912 + .await?; 722 913 723 - sqlx::query(r#" 914 + sqlx::query( 915 + r#" 724 916 UPDATE tracks 725 917 SET artist_uri = $2 726 918 WHERE artist_uri IS NULL AND album_artist = $1 727 - "#) 728 - .bind(&record.name) 729 - .bind(uri) 730 - .execute(&mut **tx).await?; 919 + "#, 920 + ) 921 + .bind(&record.name) 922 + .bind(uri) 923 + .execute(&mut **tx) 924 + .await?; 731 925 732 - sqlx::query(r#" 926 + sqlx::query( 927 + r#" 733 928 UPDATE artists 734 929 SET uri = $2 735 930 WHERE sha256 = $1 AND uri IS NULL 736 - "#) 737 - .bind(&hash) 738 - .bind(uri) 739 - .execute(&mut **tx).await?; 931 + "#, 932 + ) 933 + .bind(&hash) 934 + .bind(uri) 935 + .execute(&mut **tx) 936 + .await?; 740 937 741 - sqlx::query(r#" 938 + sqlx::query( 939 + r#" 742 940 UPDATE albums 743 941 SET artist_uri = $2 744 942 WHERE artist_uri IS NULL AND artist = $1 745 - "#) 746 - .bind(&record.name) 747 - .bind(uri) 748 - .execute(&mut **tx).await?; 749 - Ok(()) 943 + "#, 944 + ) 945 + .bind(&record.name) 946 + .bind(uri) 947 + .execute(&mut **tx) 948 + .await?; 949 + Ok(()) 750 950 } 751 951 752 - pub async fn update_album_uri(tx: &mut sqlx::Transaction<'_, Postgres>, user_id: &str, record: AlbumRecord, uri: &str) -> Result<(), Error> { 753 - let hash = sha256::digest(format!( 754 - "{} - {}", 755 - record.title, 756 - record.artist 757 - ) 758 - .to_lowercase() 759 - ); 760 - let albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 761 - .bind(&hash) 762 - .fetch_all(&mut **tx) 763 - .await?; 764 - if albums.is_empty() { 765 - println!("Album not found: {}", record.title.magenta()); 766 - return Ok(()); 767 - } 768 - let album_id = &albums[0].xata_id; 769 - sqlx::query(r#" 952 + pub async fn update_album_uri( 953 + tx: &mut sqlx::Transaction<'_, Postgres>, 954 + user_id: &str, 955 + record: AlbumRecord, 956 + uri: &str, 957 + ) -> Result<(), Error> { 958 + let hash = sha256::digest(format!("{} - {}", record.title, record.artist).to_lowercase()); 959 + let albums: Vec<Album> = sqlx::query_as("SELECT * FROM albums WHERE sha256 = $1") 960 + .bind(&hash) 961 + .fetch_all(&mut **tx) 962 + .await?; 963 + if albums.is_empty() { 964 + println!("Album not found: {}", record.title.magenta()); 965 + return Ok(()); 966 + } 967 + let album_id = &albums[0].xata_id; 968 + sqlx::query( 969 + r#" 770 970 UPDATE user_albums 771 971 SET uri = $3 772 972 WHERE user_id = $1 AND album_id = $2 773 - "#) 774 - .bind(user_id) 775 - .bind(album_id) 776 - .bind(uri) 777 - .execute(&mut **tx).await?; 973 + "#, 974 + ) 975 + .bind(user_id) 976 + .bind(album_id) 977 + .bind(uri) 978 + .execute(&mut **tx) 979 + .await?; 778 980 779 - sqlx::query(r#" 981 + sqlx::query( 982 + r#" 780 983 UPDATE tracks 781 984 SET album_uri = $2 782 985 WHERE album_uri IS NULL AND album = $1 783 - "#) 784 - .bind(record.title) 785 - .bind(uri) 786 - .execute(&mut **tx).await?; 986 + "#, 987 + ) 988 + .bind(record.title) 989 + .bind(uri) 990 + .execute(&mut **tx) 991 + .await?; 787 992 788 - sqlx::query(r#" 993 + sqlx::query( 994 + r#" 789 995 UPDATE albums 790 996 SET uri = $2 791 997 WHERE sha256 = $1 AND uri IS NULL 792 - "#) 793 - .bind(&hash) 794 - .bind(uri) 795 - .execute(&mut **tx).await?; 796 - 797 - Ok(()) 798 - } 799 - 800 - pub async fn update_track_uri(tx: &mut sqlx::Transaction<'_, Postgres>, user_id: &str, record: SongRecord, uri: &str) -> Result<(), Error> { 801 - let hash = sha256::digest(format!( 802 - "{} - {} - {}", 803 - record.title, 804 - record.artist, 805 - record.album 998 + "#, 806 999 ) 807 - .to_lowercase() 808 - ); 809 - let tracks: Vec<Track> = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 810 1000 .bind(&hash) 811 - .fetch_all(&mut **tx) 1001 + .bind(uri) 1002 + .execute(&mut **tx) 812 1003 .await?; 813 1004 814 - if tracks.is_empty() { 815 - println!("Track not found: {}", record.title.magenta()); 816 - return Ok(()); 817 - } 1005 + Ok(()) 1006 + } 818 1007 819 - let track_id = &tracks[0].xata_id; 820 - sqlx::query(r#" 1008 + pub async fn update_track_uri( 1009 + tx: &mut sqlx::Transaction<'_, Postgres>, 1010 + user_id: &str, 1011 + record: SongRecord, 1012 + uri: &str, 1013 + ) -> Result<(), Error> { 1014 + let hash = sha256::digest( 1015 + format!("{} - {} - {}", record.title, record.artist, record.album).to_lowercase(), 1016 + ); 1017 + let tracks: Vec<Track> = sqlx::query_as("SELECT * FROM tracks WHERE sha256 = $1") 1018 + .bind(&hash) 1019 + .fetch_all(&mut **tx) 1020 + .await?; 1021 + 1022 + if tracks.is_empty() { 1023 + println!("Track not found: {}", record.title.magenta()); 1024 + return Ok(()); 1025 + } 1026 + 1027 + let track_id = &tracks[0].xata_id; 1028 + sqlx::query( 1029 + r#" 821 1030 UPDATE user_tracks 822 1031 SET uri = $3 823 1032 WHERE user_id = $1 AND track_id = $2 824 - "#) 825 - .bind(user_id) 826 - .bind(track_id) 827 - .bind(uri) 828 - .execute(&mut **tx).await?; 1033 + "#, 1034 + ) 1035 + .bind(user_id) 1036 + .bind(track_id) 1037 + .bind(uri) 1038 + .execute(&mut **tx) 1039 + .await?; 829 1040 830 - sqlx::query(r#" 1041 + sqlx::query( 1042 + r#" 831 1043 UPDATE tracks 832 1044 SET uri = $2 833 1045 WHERE sha256 = $1 AND uri IS NULL 834 - "#) 835 - .bind(&hash) 836 - .bind(uri) 837 - .execute(&mut **tx).await?; 1046 + "#, 1047 + ) 1048 + .bind(&hash) 1049 + .bind(uri) 1050 + .execute(&mut **tx) 1051 + .await?; 838 1052 839 - Ok(()) 1053 + Ok(()) 840 1054 } 841 -
+10 -11
crates/jetstream/src/subscriber.rs
··· 1 1 use std::{env, sync::Arc}; 2 2 3 - use anyhow::{Error, Context}; 3 + use anyhow::{Context, Error}; 4 4 use futures_util::StreamExt; 5 5 use owo_colors::OwoColorize; 6 6 use sqlx::postgres::PgPoolOptions; 7 7 use tokio::sync::Mutex; 8 8 use tokio_tungstenite::{connect_async, tungstenite::Message}; 9 - 10 9 11 10 use crate::{repo::save_scrobble, types::Root}; 12 11 ··· 18 17 pub const LIKE_NSID: &str = "app.rocksky.like"; 19 18 pub const SHOUT_NSID: &str = "app.rocksky.shout"; 20 19 21 - 22 20 pub struct ScrobbleSubscriber { 23 21 pub service_url: String, 24 22 } ··· 35 33 let db_url = env::var("XATA_POSTGRES_URL") 36 34 .context("Failed to get XATA_POSTGRES_URL environment variable")?; 37 35 38 - let pool = PgPoolOptions::new().max_connections(5) 39 - .connect(&db_url).await?; 36 + let pool = PgPoolOptions::new() 37 + .max_connections(5) 38 + .connect(&db_url) 39 + .await?; 40 40 let pool = Arc::new(Mutex::new(pool)); 41 41 42 42 let (mut ws_stream, _) = connect_async(&self.service_url).await?; 43 - println!("Connected to jetstream at {}", self.service_url.bright_green()); 43 + println!( 44 + "Connected to jetstream at {}", 45 + self.service_url.bright_green() 46 + ); 44 47 45 48 while let Some(msg) = ws_stream.next().await { 46 49 match msg { ··· 56 59 } 57 60 } 58 61 59 - 60 62 Ok(()) 61 63 } 62 64 } 63 65 64 - async fn handle_message( 65 - pool: Arc<Mutex<sqlx::PgPool>>, 66 - msg: Message, 67 - ) -> Result<(), Error> { 66 + async fn handle_message(pool: Arc<Mutex<sqlx::PgPool>>, msg: Message) -> Result<(), Error> { 68 67 tokio::spawn(async move { 69 68 if let Message::Text(text) = msg { 70 69 let message: Root = serde_json::from_str(&text)?;
+189 -127
crates/playlists/src/core.rs
··· 1 - use std::{env, sync::{Arc, Mutex}}; 1 + use std::{ 2 + env, 3 + sync::{Arc, Mutex}, 4 + }; 2 5 3 6 use anyhow::Error; 4 7 use duckdb::{params, Connection}; ··· 8 11 use sha2::Digest; 9 12 use sqlx::{Pool, Postgres}; 10 13 11 - use crate::{crypto::{decrypt_aes_256_ctr, generate_token}, types::{self, spotify_token::SpotifyTokenWithEmail}, xata::{self, track::Track}}; 14 + use crate::{ 15 + crypto::{decrypt_aes_256_ctr, generate_token}, 16 + types::{self, spotify_token::SpotifyTokenWithEmail}, 17 + xata::{self, track::Track}, 18 + }; 12 19 13 20 const ROCKSKY_API: &str = "https://api.rocksky.app"; 14 21 ··· 84 91 Ok(()) 85 92 } 86 93 87 - 88 94 pub async fn load_users(conn: Arc<Mutex<Connection>>, pool: &Pool<Postgres>) -> Result<(), Error> { 89 - let conn = conn.lock().unwrap(); 90 - let users: Vec<xata::user::User> = sqlx::query_as(r#" 95 + let conn = conn.lock().unwrap(); 96 + let users: Vec<xata::user::User> = sqlx::query_as( 97 + r#" 91 98 SELECT * FROM users 92 - "#) 93 - .fetch_all(pool) 94 - .await?; 99 + "#, 100 + ) 101 + .fetch_all(pool) 102 + .await?; 95 103 96 - for (i, user) in users.clone().into_iter().enumerate() { 97 - println!("user {} - {}", i, user.display_name.bright_green()); 98 - match conn.execute( 99 - "INSERT INTO users ( 104 + for (i, user) in users.clone().into_iter().enumerate() { 105 + println!("user {} - {}", i, user.display_name.bright_green()); 106 + match conn.execute( 107 + "INSERT INTO users ( 100 108 id, 101 109 display_name, 102 110 did, ··· 107 115 ?, 108 116 ?, 109 117 ?) ON CONFLICT DO NOTHING", 110 - params![ 111 - user.xata_id, 112 - user.display_name, 113 - user.did, 114 - user.handle, 115 - user.avatar, 116 - ], 117 - ) { 118 - Ok(_) => (), 119 - Err(e) => println!("error: {}", e), 120 - } 121 - } 118 + params![ 119 + user.xata_id, 120 + user.display_name, 121 + user.did, 122 + user.handle, 123 + user.avatar, 124 + ], 125 + ) { 126 + Ok(_) => (), 127 + Err(e) => println!("error: {}", e), 128 + } 129 + } 122 130 123 - println!("users: {:?}", users.len()); 124 - Ok(()) 131 + println!("users: {:?}", users.len()); 132 + Ok(()) 125 133 } 126 134 127 135 pub async fn find_spotify_users( 128 - pool: &Pool<Postgres>, 129 - offset: usize, 130 - limit: usize 136 + pool: &Pool<Postgres>, 137 + offset: usize, 138 + limit: usize, 131 139 ) -> Result<Vec<(String, String, String, String)>, Error> { 132 - let results: Vec<SpotifyTokenWithEmail> = sqlx::query_as(r#" 140 + let results: Vec<SpotifyTokenWithEmail> = sqlx::query_as( 141 + r#" 133 142 SELECT * FROM spotify_tokens 134 143 LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id 135 144 LEFT JOIN users ON spotify_accounts.user_id = users.xata_id 136 145 LIMIT $1 OFFSET $2 137 - "#) 146 + "#, 147 + ) 138 148 .bind(limit as i64) 139 149 .bind(offset as i64) 140 150 .fetch_all(pool) 141 151 .await?; 142 152 143 - let mut user_tokens = vec![]; 153 + let mut user_tokens = vec![]; 144 154 145 - for result in &results { 146 - let token = decrypt_aes_256_ctr( 147 - &result.refresh_token, 148 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 149 - )?; 150 - user_tokens.push((result.email.clone(), token, result.did.clone(), result.user_id.clone())); 151 - } 155 + for result in &results { 156 + let token = decrypt_aes_256_ctr( 157 + &result.refresh_token, 158 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 159 + )?; 160 + user_tokens.push(( 161 + result.email.clone(), 162 + token, 163 + result.did.clone(), 164 + result.user_id.clone(), 165 + )); 166 + } 152 167 153 - Ok(user_tokens) 168 + Ok(user_tokens) 154 169 } 155 170 156 - pub async fn save_playlists(pool: &Pool<Postgres>, conn: Arc<Mutex<Connection>>, nc: Arc<Mutex<async_nats::Client>>, playlists: Vec<types::playlist::Playlist>, user_id: &str, did: &str) -> Result<(), Error> { 157 - let token = generate_token(did)?; 158 - for playlist in playlists { 159 - println!("Saving playlist: {} - {} tracks", playlist.name.bright_green(), playlist.tracks.total); 171 + pub async fn save_playlists( 172 + pool: &Pool<Postgres>, 173 + conn: Arc<Mutex<Connection>>, 174 + nc: Arc<Mutex<async_nats::Client>>, 175 + playlists: Vec<types::playlist::Playlist>, 176 + user_id: &str, 177 + did: &str, 178 + ) -> Result<(), Error> { 179 + let token = generate_token(did)?; 180 + for playlist in playlists { 181 + println!( 182 + "Saving playlist: {} - {} tracks", 183 + playlist.name.bright_green(), 184 + playlist.tracks.total 185 + ); 160 186 161 - sqlx::query(r#" 187 + sqlx::query( 188 + r#" 162 189 INSERT INTO playlists (name, description, picture, spotify_link, created_by) 163 190 VALUES ($1, $2, $3, $4, $5) 164 191 ON CONFLICT (spotify_link) DO UPDATE set ··· 167 194 picture = EXCLUDED.picture, 168 195 spotify_link = EXCLUDED.spotify_link, 169 196 created_by = EXCLUDED.created_by 170 - "#) 171 - .bind(playlist.name) 172 - .bind(playlist.description) 173 - .bind(playlist.images.first().map(|i| i.url.clone())) 174 - .bind(&playlist.external_urls.spotify) 175 - .bind(user_id) 176 - .execute(pool) 177 - .await?; 197 + "#, 198 + ) 199 + .bind(playlist.name) 200 + .bind(playlist.description) 201 + .bind(playlist.images.first().map(|i| i.url.clone())) 202 + .bind(&playlist.external_urls.spotify) 203 + .bind(user_id) 204 + .execute(pool) 205 + .await?; 178 206 179 - let new_playlist: Vec<xata::playlist::Playlist> = sqlx::query_as(r#"SELECT * FROM playlists WHERE spotify_link = $1"#) 180 - .bind(&playlist.external_urls.spotify) 181 - .fetch_all(pool) 182 - .await?; 207 + let new_playlist: Vec<xata::playlist::Playlist> = 208 + sqlx::query_as(r#"SELECT * FROM playlists WHERE spotify_link = $1"#) 209 + .bind(&playlist.external_urls.spotify) 210 + .fetch_all(pool) 211 + .await?; 183 212 184 - let new_playlist = new_playlist.first().unwrap(); 213 + let new_playlist = new_playlist.first().unwrap(); 185 214 186 - let nc = nc.lock().unwrap(); 187 - nc.publish("rocksky.playlist", 188 - serde_json::to_string(&json!({ 189 - "id": new_playlist.xata_id.clone(), 190 - "did": did, 191 - }) 192 - ).unwrap().into() 193 - ).await?; 194 - drop(nc); 215 + let nc = nc.lock().unwrap(); 216 + nc.publish( 217 + "rocksky.playlist", 218 + serde_json::to_string(&json!({ 219 + "id": new_playlist.xata_id.clone(), 220 + "did": did, 221 + })) 222 + .unwrap() 223 + .into(), 224 + ) 225 + .await?; 226 + drop(nc); 195 227 196 - let mut tracks_to_save: Vec<(String, String)> = vec![]; 197 - let mut i = 1; 198 - for track in playlist.tracks.items.unwrap_or_default() { 199 - println!("Saving track: {} - {}/{}", track.track.name.bright_green(), i, playlist.tracks.total); 200 - i += 1; 201 - match save_track(track.track, &token).await? { 202 - Some(track) => { 203 - println!("Saved track: {}", track.xata_id.bright_green()); 204 - tracks_to_save.push((new_playlist.xata_id.clone(), track.xata_id.clone())); 205 - }, 206 - None => { 207 - println!("Failed to save track"); 228 + let mut tracks_to_save: Vec<(String, String)> = vec![]; 229 + let mut i = 1; 230 + for track in playlist.tracks.items.unwrap_or_default() { 231 + println!( 232 + "Saving track: {} - {}/{}", 233 + track.track.name.bright_green(), 234 + i, 235 + playlist.tracks.total 236 + ); 237 + i += 1; 238 + match save_track(track.track, &token).await? { 239 + Some(track) => { 240 + println!("Saved track: {}", track.xata_id.bright_green()); 241 + tracks_to_save.push((new_playlist.xata_id.clone(), track.xata_id.clone())); 242 + } 243 + None => { 244 + println!("Failed to save track"); 245 + } 246 + }; 208 247 } 209 - }; 210 - } 211 248 212 - // delete all tracks from playlist 213 - sqlx::query(r#" 249 + // delete all tracks from playlist 250 + sqlx::query( 251 + r#" 214 252 DELETE FROM playlist_tracks WHERE playlist_id = $1 215 - "#) 216 - .bind(&new_playlist.xata_id) 217 - .execute(pool) 218 - .await?; 253 + "#, 254 + ) 255 + .bind(&new_playlist.xata_id) 256 + .execute(pool) 257 + .await?; 219 258 220 - // save tracks to playlist 221 - for (playlist_id, track_id) in tracks_to_save { 222 - sqlx::query(r#" 259 + // save tracks to playlist 260 + for (playlist_id, track_id) in tracks_to_save { 261 + sqlx::query( 262 + r#" 223 263 INSERT INTO playlist_tracks (playlist_id, track_id) 224 264 VALUES ($1, $2) 225 265 ON CONFLICT DO NOTHING 226 - "#) 227 - .bind(&playlist_id) 228 - .bind(&track_id) 229 - .execute(pool) 230 - .await?; 231 - } 266 + "#, 267 + ) 268 + .bind(&playlist_id) 269 + .bind(&track_id) 270 + .execute(pool) 271 + .await?; 272 + } 232 273 233 - sqlx::query(r#" 274 + sqlx::query( 275 + r#" 234 276 INSERT INTO user_playlists (user_id, playlist_id) 235 277 VALUES ($1, $2) 236 278 ON CONFLICT (user_id, playlist_id) DO NOTHING 237 - "#) 238 - .bind(user_id) 239 - .bind(&new_playlist.xata_id) 240 - .execute(pool) 241 - .await?; 279 + "#, 280 + ) 281 + .bind(user_id) 282 + .bind(&new_playlist.xata_id) 283 + .execute(pool) 284 + .await?; 242 285 243 - let user_playlist: Vec<xata::user_playlist::UserPlaylist> = sqlx::query_as("SELECT * FROM user_playlists WHERE user_id = $1 AND playlist_id = $2") 244 - .bind(user_id) 245 - .bind(&new_playlist.xata_id) 246 - .fetch_all(pool) 247 - .await?; 248 - let user_playlist = user_playlist.first().unwrap(); 286 + let user_playlist: Vec<xata::user_playlist::UserPlaylist> = 287 + sqlx::query_as("SELECT * FROM user_playlists WHERE user_id = $1 AND playlist_id = $2") 288 + .bind(user_id) 289 + .bind(&new_playlist.xata_id) 290 + .fetch_all(pool) 291 + .await?; 292 + let user_playlist = user_playlist.first().unwrap(); 249 293 250 - let conn = conn.lock().unwrap(); 251 - conn.execute("INSERT INTO playlists (id, name, description, picture, spotify_link, uri, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING", 294 + let conn = conn.lock().unwrap(); 295 + conn.execute("INSERT INTO playlists (id, name, description, picture, spotify_link, uri, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING", 252 296 params![ 253 297 &new_playlist.xata_id, 254 298 &new_playlist.name, ··· 260 304 ] 261 305 )?; 262 306 263 - conn.execute( 307 + conn.execute( 264 308 "INSERT INTO user_playlists (id, user_id, playlist_id, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING", 265 309 params![ 266 310 &user_playlist.xata_id, ··· 269 313 chrono::Utc::now() 270 314 ] 271 315 )?; 272 - 273 - } 274 - Ok(()) 316 + } 317 + Ok(()) 275 318 } 276 319 277 - 278 - pub async fn save_track(track: types::playlist::Track, token: &str) -> Result<Option<xata::track::Track>, Error> { 279 - let client = Client::new(); 320 + pub async fn save_track( 321 + track: types::playlist::Track, 322 + token: &str, 323 + ) -> Result<Option<xata::track::Track>, Error> { 324 + let client = Client::new(); 280 325 let response = client 281 326 .post(&format!("{}/tracks", ROCKSKY_API)) 282 327 .bearer_auth(token) ··· 304 349 .await?; 305 350 306 351 if !response.status().is_success() { 307 - println!("Failed to save track: {}", response.text().await?); 308 - return Ok(None); 352 + println!("Failed to save track: {}", response.text().await?); 353 + return Ok(None); 309 354 } 310 355 311 356 // `${track.title} - ${track.artist} - ${track.album}`.toLowerCase() 312 - let sha256 = format!("{:x}", sha2::Sha256::digest(format!("{} - {} - {}", track.name, track.artists.iter().map(|artist| artist.name.clone()).collect::<Vec<String>>().join(", "), track.album.name).to_lowercase().as_bytes())); 357 + let sha256 = format!( 358 + "{:x}", 359 + sha2::Sha256::digest( 360 + format!( 361 + "{} - {} - {}", 362 + track.name, 363 + track 364 + .artists 365 + .iter() 366 + .map(|artist| artist.name.clone()) 367 + .collect::<Vec<String>>() 368 + .join(", "), 369 + track.album.name 370 + ) 371 + .to_lowercase() 372 + .as_bytes() 373 + ) 374 + ); 313 375 // get by sha256 314 376 let response = client 315 - .get(&format!("{}/tracks/{}", ROCKSKY_API, sha256)) 316 - .bearer_auth(token) 317 - .send() 318 - .await?; 377 + .get(&format!("{}/tracks/{}", ROCKSKY_API, sha256)) 378 + .bearer_auth(token) 379 + .send() 380 + .await?; 319 381 320 382 // wait 6 seconds to avoid rate limiting 321 383 tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; ··· 323 385 let data = response.text().await?; 324 386 325 387 if !status.is_success() { 326 - println!("Failed to get track: {}", data); 388 + println!("Failed to get track: {}", data); 327 389 } 328 390 329 - let track: xata::track::Track = serde_json::from_str(&data)?; 391 + let track: xata::track::Track = serde_json::from_str(&data)?; 330 392 331 - Ok(Some(track)) 393 + Ok(Some(track)) 332 394 }
+18 -15
crates/playlists/src/main.rs
··· 1 1 use core::{create_tables, find_spotify_users, load_users, save_playlists}; 2 - use std::{env, sync::{Arc, Mutex}}; 2 + use std::{ 3 + env, 4 + sync::{Arc, Mutex}, 5 + }; 3 6 4 7 use anyhow::Error; 5 8 use async_nats::connect; ··· 10 13 use spotify::get_user_playlists; 11 14 use sqlx::postgres::PgPoolOptions; 12 15 13 - pub mod types; 16 + pub mod core; 14 17 pub mod crypto; 18 + pub mod spotify; 19 + pub mod types; 15 20 pub mod xata; 16 - pub mod core; 17 - pub mod spotify; 18 21 19 22 #[tokio::main] 20 23 async fn main() -> Result<(), Error> { ··· 24 27 let conn = Arc::new(Mutex::new(conn)); 25 28 create_tables(conn.clone())?; 26 29 27 - subscribe( 28 - conn.clone() 29 - ).await?; 30 + subscribe(conn.clone()).await?; 30 31 31 - let pool = PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; 32 + let pool = PgPoolOptions::new() 33 + .max_connections(5) 34 + .connect(&env::var("XATA_POSTGRES_URL")?) 35 + .await?; 32 36 let users = find_spotify_users(&pool, 0, 100).await?; 33 37 34 38 load_users(conn.clone(), &pool).await?; ··· 46 50 println!("Connected to NATS server at {}", addr.bright_green()); 47 51 48 52 for user in users { 49 - let token = user.1.clone(); 50 - let did = user.2.clone(); 51 - let user_id = user.3.clone(); 52 - let playlists = get_user_playlists(token).await?; 53 - save_playlists(&pool, conn.clone(), nc.clone(),playlists, &user_id, &did).await?; 53 + let token = user.1.clone(); 54 + let did = user.2.clone(); 55 + let user_id = user.3.clone(); 56 + let playlists = get_user_playlists(token).await?; 57 + save_playlists(&pool, conn.clone(), nc.clone(), playlists, &user_id, &did).await?; 54 58 } 55 59 56 60 println!("Done!"); 57 61 58 62 loop { 59 - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 63 + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 60 64 } 61 65 } 62 -
+43 -48
crates/playlists/src/spotify.rs
··· 6 6 use crate::types::{self, token::AccessToken}; 7 7 8 8 pub async fn refresh_token(token: &str) -> Result<AccessToken, Error> { 9 - if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { 10 - panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); 11 - } 9 + if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { 10 + panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); 11 + } 12 12 13 - let client_id = env::var("SPOTIFY_CLIENT_ID")?; 14 - let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; 13 + let client_id = env::var("SPOTIFY_CLIENT_ID")?; 14 + let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; 15 15 16 - let client = Client::new(); 16 + let client = Client::new(); 17 17 18 - let response = client.post("https://accounts.spotify.com/api/token") 19 - .basic_auth(&client_id, Some(client_secret)) 20 - .form(&[ 21 - ("grant_type", "refresh_token"), 22 - ("refresh_token", token), 23 - ("client_id", &client_id) 24 - ]) 25 - .send() 26 - .await?; 27 - let token = response.json::<AccessToken>().await?; 28 - Ok(token) 18 + let response = client 19 + .post("https://accounts.spotify.com/api/token") 20 + .basic_auth(&client_id, Some(client_secret)) 21 + .form(&[ 22 + ("grant_type", "refresh_token"), 23 + ("refresh_token", token), 24 + ("client_id", &client_id), 25 + ]) 26 + .send() 27 + .await?; 28 + let token = response.json::<AccessToken>().await?; 29 + Ok(token) 29 30 } 30 31 32 + pub async fn get_user_playlists(token: String) -> Result<Vec<types::playlist::Playlist>, Error> { 33 + let token = refresh_token(&token).await?; 34 + let client = Client::new(); 35 + let response = client 36 + .get("https://api.spotify.com/v1/me/playlists") 37 + .header("Authorization", format!("Bearer {}", token.access_token)) 38 + .send() 39 + .await?; 40 + let playlists = response.json::<types::playlist::SpotifyResponse>().await?; 41 + let mut all_playlists = vec![]; 31 42 43 + for playlist in playlists.items { 44 + all_playlists.push(get_playlist(&playlist.id, &token.access_token).await?); 45 + // wait for 1 second to avoid rate limiting 46 + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 47 + } 32 48 33 - pub async fn get_user_playlists( 34 - token: String, 35 - ) -> Result<Vec<types::playlist::Playlist>, Error>{ 36 - let token = refresh_token(&token).await?; 37 - let client = Client::new(); 38 - let response = client 39 - .get("https://api.spotify.com/v1/me/playlists") 40 - .header("Authorization", format!("Bearer {}", token.access_token)) 41 - .send() 42 - .await?; 43 - let playlists = response.json::<types::playlist::SpotifyResponse>().await?; 44 - let mut all_playlists = vec![]; 45 - 46 - for playlist in playlists.items { 47 - all_playlists.push( 48 - get_playlist(&playlist.id, &token.access_token).await? 49 - ); 50 - // wait for 1 second to avoid rate limiting 51 - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 52 - } 53 - 54 - Ok(all_playlists) 49 + Ok(all_playlists) 55 50 } 56 51 57 52 pub async fn get_playlist(id: &str, token: &str) -> Result<types::playlist::Playlist, Error> { 58 - let client = Client::new(); 59 - let response = client 60 - .get(format!("https://api.spotify.com/v1/playlists/{}", id)) 61 - .header("Authorization", format!("Bearer {}", token)) 62 - .send() 63 - .await?; 53 + let client = Client::new(); 54 + let response = client 55 + .get(format!("https://api.spotify.com/v1/playlists/{}", id)) 56 + .header("Authorization", format!("Bearer {}", token)) 57 + .send() 58 + .await?; 64 59 65 - let playlist = response.json::<types::playlist::Playlist>().await?; 66 - Ok(playlist) 67 - } 60 + let playlist = response.json::<types::playlist::Playlist>().await?; 61 + Ok(playlist) 62 + }
+53 -49
crates/playlists/src/subscriber/mod.rs
··· 1 - use std::{env, sync::{Arc, Mutex}, thread}; 2 1 use anyhow::Error; 3 2 use async_nats::{connect, Client}; 4 3 use duckdb::{params, Connection}; 5 4 use owo_colors::OwoColorize; 5 + use std::{ 6 + env, 7 + sync::{Arc, Mutex}, 8 + thread, 9 + }; 6 10 use tokio_stream::StreamExt; 7 11 use types::UserPayload; 8 12 ··· 20 24 Ok(()) 21 25 } 22 26 23 - 24 27 pub fn on_new_user(nc: Arc<Mutex<Client>>, conn: Arc<Mutex<Connection>>) { 25 - thread::spawn(move || { 26 - let rt = tokio::runtime::Runtime::new().unwrap(); 27 - let conn = conn.clone(); 28 - let nc = nc.clone(); 29 - rt.block_on(async { 30 - let nc = nc.lock().unwrap(); 31 - let mut sub = nc.subscribe("rocksky.user".to_string()).await?; 32 - drop(nc); 28 + thread::spawn(move || { 29 + let rt = tokio::runtime::Runtime::new().unwrap(); 30 + let conn = conn.clone(); 31 + let nc = nc.clone(); 32 + rt.block_on(async { 33 + let nc = nc.lock().unwrap(); 34 + let mut sub = nc.subscribe("rocksky.user".to_string()).await?; 35 + drop(nc); 33 36 34 - while let Some(msg) = sub.next().await { 35 - let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 36 - match serde_json::from_str::<UserPayload>(&data) { 37 - Ok(payload) => { 38 - match save_user(conn.clone(), payload.clone()).await { 39 - Ok(_) => println!("User saved successfully for {}{}", "@".cyan(), payload.handle.cyan()), 40 - Err(e) => eprintln!("Error saving user: {}", e), 37 + while let Some(msg) = sub.next().await { 38 + let data = String::from_utf8(msg.payload.to_vec()).unwrap(); 39 + match serde_json::from_str::<UserPayload>(&data) { 40 + Ok(payload) => match save_user(conn.clone(), payload.clone()).await { 41 + Ok(_) => println!( 42 + "User saved successfully for {}{}", 43 + "@".cyan(), 44 + payload.handle.cyan() 45 + ), 46 + Err(e) => eprintln!("Error saving user: {}", e), 47 + }, 48 + Err(e) => { 49 + eprintln!("Error parsing payload: {}", e); 50 + println!("{}", data); 51 + } 52 + } 41 53 } 42 - }, 43 - Err(e) => { 44 - eprintln!("Error parsing payload: {}", e); 45 - println!("{}", data); 46 - } 47 - } 48 - } 49 54 50 - Ok::<(), Error>(()) 51 - })?; 55 + Ok::<(), Error>(()) 56 + })?; 52 57 53 - Ok::<(), Error>(()) 54 - }); 58 + Ok::<(), Error>(()) 59 + }); 55 60 } 56 61 57 - 58 62 pub async fn save_user(conn: Arc<Mutex<Connection>>, payload: UserPayload) -> Result<(), Error> { 59 - let conn = conn.lock().unwrap(); 63 + let conn = conn.lock().unwrap(); 60 64 61 - match conn.execute( 62 - "INSERT INTO users ( 65 + match conn.execute( 66 + "INSERT INTO users ( 63 67 id, 64 68 avatar, 65 69 did, ··· 77 81 did = EXCLUDED.did, 78 82 display_name = EXCLUDED.display_name, 79 83 handle = EXCLUDED.handle", 80 - params![ 81 - payload.xata_id, 82 - payload.avatar, 83 - payload.did, 84 - payload.display_name, 85 - payload.handle, 86 - ], 87 - ) { 88 - Ok(_) => (), 89 - Err(e) => { 90 - if !e.to_string().contains("violates primary key constraint") { 91 - println!("[users] error: {}", e); 92 - return Err(e.into()); 93 - } 94 - } 95 - } 96 - Ok(()) 84 + params![ 85 + payload.xata_id, 86 + payload.avatar, 87 + payload.did, 88 + payload.display_name, 89 + payload.handle, 90 + ], 91 + ) { 92 + Ok(_) => (), 93 + Err(e) => { 94 + if !e.to_string().contains("violates primary key constraint") { 95 + println!("[users] error: {}", e); 96 + return Err(e.into()); 97 + } 98 + } 99 + } 100 + Ok(()) 97 101 }
+32 -31
crates/scrobbler/src/auth.rs
··· 1 - use std::collections::BTreeMap; 2 1 use anyhow::Error; 3 - use sqlx::{Pool, Postgres}; 4 - use std::env; 5 2 use jsonwebtoken::DecodingKey; 6 3 use jsonwebtoken::EncodingKey; 7 4 use jsonwebtoken::Header; 8 5 use jsonwebtoken::Validation; 9 6 use serde::{Deserialize, Serialize}; 7 + use sqlx::{Pool, Postgres}; 8 + use std::collections::BTreeMap; 9 + use std::env; 10 10 11 11 use crate::cache::Cache; 12 12 use crate::repo; ··· 28 28 ) -> Result<(), Error> { 29 29 match repo::user::get_user_by_apikey(pool, api_key).await? { 30 30 Some(user) => { 31 - let shared_secret = user.shared_secret 31 + let shared_secret = user 32 + .shared_secret 32 33 .ok_or_else(|| Error::msg("User does not have a shared secret"))?; 33 34 let hashed_password = md5::compute(format!("{}", shared_secret)); 34 35 let hashed_password = format!("{:x}", hashed_password); ··· 40 41 return Err(Error::msg("Invalid password")); 41 42 } 42 43 Ok(()) 43 - }, 44 - None => { 45 - Err(Error::msg("Invalid API key")) 46 44 } 45 + None => Err(Error::msg("Invalid API key")), 47 46 } 48 47 } 49 48 50 49 pub async fn authenticate( 51 - pool: &Pool<Postgres>, 52 - api_key: &str, 53 - api_sig: &str, 54 - session_key: &str, 55 - form: &BTreeMap<String, String>, 50 + pool: &Pool<Postgres>, 51 + api_key: &str, 52 + api_sig: &str, 53 + session_key: &str, 54 + form: &BTreeMap<String, String>, 56 55 ) -> Result<(), Error> { 57 56 let claims = decode_token(session_key)?; 58 57 ··· 73 72 Ok(()) 74 73 } 75 74 76 - pub async fn extract_did(pool: &Pool<Postgres>, form: &BTreeMap<String, String>) -> Result<String, Error> { 77 - let apikey = form.get("api_key").ok_or_else(|| Error::msg("Missing api_key"))?; 75 + pub async fn extract_did( 76 + pool: &Pool<Postgres>, 77 + form: &BTreeMap<String, String>, 78 + ) -> Result<String, Error> { 79 + let apikey = form 80 + .get("api_key") 81 + .ok_or_else(|| Error::msg("Missing api_key"))?; 78 82 let user = repo::user::get_user_by_apikey(pool, apikey).await?; 79 - let did = user.ok_or_else(|| Error::msg("Corresponding user not found"))?.did; 83 + let did = user 84 + .ok_or_else(|| Error::msg("Corresponding user not found"))? 85 + .did; 80 86 Ok(did) 81 87 } 82 88 ··· 118 124 cache: &Cache, 119 125 api_key: &str, 120 126 ) -> Result<String, Error> { 121 - match repo::user::get_user_by_apikey(pool, &api_key).await? { 127 + match repo::user::get_user_by_apikey(pool, &api_key).await? { 122 128 Some(user) => { 123 - let mut bytes = [0u8; 16]; 124 - rand::fill(&mut bytes[..]); 129 + let mut bytes = [0u8; 16]; 130 + rand::fill(&mut bytes[..]); 125 131 126 - let session_id = hex::encode(bytes); 132 + let session_id = hex::encode(bytes); 127 133 128 - let user = serde_json::to_string(&user) 129 - .map_err(|_| Error::msg("Failed to serialize user"))?; 130 - cache.set(&format!("lastfm:{}", session_id), &user)?; 131 - Ok(session_id) 132 - }, 133 - None => { 134 - Err(Error::msg("Invalid API key")) 134 + let user = 135 + serde_json::to_string(&user).map_err(|_| Error::msg("Failed to serialize user"))?; 136 + cache.set(&format!("lastfm:{}", session_id), &user)?; 137 + Ok(session_id) 135 138 } 139 + None => Err(Error::msg("Invalid API key")), 136 140 } 137 141 } 138 142 139 - pub fn verify_session_id( 140 - cache: &Cache, 141 - session_id: &str, 142 - ) -> Result<String, Error> { 143 + pub fn verify_session_id(cache: &Cache, session_id: &str) -> Result<String, Error> { 143 144 let user = cache.get(&format!("lastfm:{}", session_id))?; 144 145 if user.is_none() { 145 146 return Err(Error::msg("Session ID not found")); ··· 164 165 165 166 assert_eq!(claims.did, "did:plc:7vdlgi2bflelz7mmuxoqjfcr"); 166 167 } 167 - } 168 + }
+40 -57
crates/scrobbler/src/handlers/mod.rs
··· 2 2 use anyhow::Error; 3 3 use scrobble::handle_scrobble; 4 4 use sqlx::{Pool, Postgres}; 5 + use std::collections::BTreeMap; 6 + use std::sync::Arc; 7 + use tokio_stream::StreamExt; 5 8 use v1::authenticate::authenticate; 6 9 use v1::nowplaying::nowplaying; 7 10 use v1::submission::submission; 8 - use std::collections::BTreeMap; 9 - use std::sync::Arc; 10 - use tokio_stream::StreamExt; 11 11 12 12 use crate::cache::Cache; 13 13 use crate::listenbrainz::submit::submit_listens; ··· 19 19 20 20 #[macro_export] 21 21 macro_rules! read_payload { 22 - ($payload:expr) => {{ 23 - let mut body = Vec::new(); 24 - while let Some(chunk) = $payload.next().await { 25 - match chunk { 26 - Ok(bytes) => body.extend_from_slice(&bytes), 27 - Err(err) => return Err(err.into()), 28 - } 29 - } 30 - body 31 - }}; 22 + ($payload:expr) => {{ 23 + let mut body = Vec::new(); 24 + while let Some(chunk) = $payload.next().await { 25 + match chunk { 26 + Ok(bytes) => body.extend_from_slice(&bytes), 27 + Err(err) => return Err(err.into()), 28 + } 29 + } 30 + body 31 + }}; 32 32 } 33 - 34 33 35 34 #[get("/")] 36 35 pub async fn index( ··· 42 41 return Ok(HttpResponse::Ok().body(BANNER)); 43 42 } 44 43 45 - authenticate( 46 - params.into_inner(), 47 - cache.get_ref(), 48 - data.get_ref(), 49 - ) 50 - .await 51 - .map_err(actix_web::error::ErrorInternalServerError) 44 + authenticate(params.into_inner(), cache.get_ref(), data.get_ref()) 45 + .await 46 + .map_err(actix_web::error::ErrorInternalServerError) 52 47 } 53 48 54 49 #[post("/nowplaying")] ··· 57 52 cache: web::Data<Cache>, 58 53 form: web::Form<BTreeMap<String, String>>, 59 54 ) -> impl Responder { 60 - nowplaying( 61 - form.into_inner(), 62 - cache.get_ref(), 63 - data.get_ref(), 64 - ) 65 - .map_err(actix_web::error::ErrorInternalServerError) 55 + nowplaying(form.into_inner(), cache.get_ref(), data.get_ref()) 56 + .map_err(actix_web::error::ErrorInternalServerError) 66 57 } 67 58 68 59 #[post("/submission")] ··· 71 62 cache: web::Data<Cache>, 72 63 form: web::Form<BTreeMap<String, String>>, 73 64 ) -> impl Responder { 74 - submission( 75 - form.into_inner(), 76 - cache.get_ref(), 77 - data.get_ref(), 78 - ) 79 - .await 80 - .map_err(actix_web::error::ErrorInternalServerError) 65 + submission(form.into_inner(), cache.get_ref(), data.get_ref()) 66 + .await 67 + .map_err(actix_web::error::ErrorInternalServerError) 81 68 } 82 69 83 70 #[get("/2.0")] ··· 95 82 let cache = cache.get_ref(); 96 83 97 84 let method = form.get("method").unwrap_or(&"".to_string()).to_string(); 98 - call_method(&method, conn, cache, form.into_inner()).await 99 - .map_err(actix_web::error::ErrorInternalServerError) 85 + call_method(&method, conn, cache, form.into_inner()) 86 + .await 87 + .map_err(actix_web::error::ErrorInternalServerError) 100 88 } 101 89 102 90 #[post("/1/submit-listens")] 103 91 pub async fn handle_submit_listens( 104 - req: HttpRequest, 105 - data: web::Data<Arc<Pool<Postgres>>>, 106 - cache: web::Data<Cache>, 107 - mut payload: web::Payload, 92 + req: HttpRequest, 93 + data: web::Data<Arc<Pool<Postgres>>>, 94 + cache: web::Data<Cache>, 95 + mut payload: web::Payload, 108 96 ) -> impl Responder { 109 - let token = match req.headers().get("Authorization") { 97 + let token = match req.headers().get("Authorization") { 110 98 Some(header) => header.to_str().map_err(actix_web::error::ErrorBadRequest)?, 111 99 None => return Ok(HttpResponse::Unauthorized().finish()), 112 100 }; ··· 135 123 } 136 124 137 125 #[get("/1/validate-token")] 138 - pub async fn handle_validate_token( 139 - _req: HttpRequest, 140 - ) -> impl Responder { 141 - HttpResponse::Ok().json( 142 - serde_json::json!({ 143 - "code": 200, 144 - "message": "Token valid.", 145 - "valid": true, 146 - }) 147 - ) 126 + pub async fn handle_validate_token(_req: HttpRequest) -> impl Responder { 127 + HttpResponse::Ok().json(serde_json::json!({ 128 + "code": 200, 129 + "message": "Token valid.", 130 + "valid": true, 131 + })) 148 132 } 149 133 150 134 pub async fn call_method( 151 135 method: &str, 152 136 pool: &Arc<Pool<Postgres>>, 153 137 cache: &Cache, 154 - form: BTreeMap<String, String>) -> Result<HttpResponse, Error> { 155 - match method { 156 - "track.scrobble" => handle_scrobble(form, pool, cache).await, 157 - _ => { 158 - Err(Error::msg(format!("Unsupported method: {}", method))) 138 + form: BTreeMap<String, String>, 139 + ) -> Result<HttpResponse, Error> { 140 + match method { 141 + "track.scrobble" => handle_scrobble(form, pool, cache).await, 142 + _ => Err(Error::msg(format!("Unsupported method: {}", method))), 159 143 } 160 - } 161 144 }
+14 -21
crates/scrobbler/src/handlers/scrobble.rs
··· 1 - use std::collections::BTreeMap; 2 1 use actix_web::HttpResponse; 3 2 use anyhow::Error; 4 3 use serde_json::json; 5 4 use sqlx::Pool; 5 + use std::collections::BTreeMap; 6 6 7 - use crate::{auth::authenticate, cache::Cache, params::validate_scrobble_params, response::build_response, scrobbler::scrobble}; 7 + use crate::{ 8 + auth::authenticate, cache::Cache, params::validate_scrobble_params, response::build_response, 9 + scrobbler::scrobble, 10 + }; 8 11 9 12 pub async fn handle_scrobble( 10 - form: BTreeMap<String, String>, 11 - conn: &Pool<sqlx::Postgres>, 12 - cache: &Cache, 13 + form: BTreeMap<String, String>, 14 + conn: &Pool<sqlx::Postgres>, 15 + cache: &Cache, 13 16 ) -> Result<HttpResponse, Error> { 14 - let params = match validate_scrobble_params( 15 - &form, 16 - &["api_key", "api_sig", "sk", "method"], 17 - ) { 17 + let params = match validate_scrobble_params(&form, &["api_key", "api_sig", "sk", "method"]) { 18 18 Ok(params) => params, 19 19 Err(e) => { 20 20 return Ok(HttpResponse::BadRequest().json(json!({ ··· 24 24 } 25 25 }; 26 26 27 - if let Err(e) = authenticate( 28 - conn, 29 - &params[0], 30 - &params[1], 31 - &params[2], 32 - &form 33 - ).await { 27 + if let Err(e) = authenticate(conn, &params[0], &params[1], &params[2], &form).await { 34 28 return Ok(HttpResponse::Forbidden().json(json!({ 35 29 "error": 2, 36 30 "message": format!("Authentication failed: {}", e) ··· 46 40 "message": e.to_string() 47 41 }))); 48 42 } 49 - Ok( 50 - HttpResponse::BadRequest().json(json!({ 51 - "error": 4, 52 - "message": format!("Failed to parse scrobbles: {}", e) 53 - }))) 43 + Ok(HttpResponse::BadRequest().json(json!({ 44 + "error": 4, 45 + "message": format!("Failed to parse scrobbles: {}", e) 46 + }))) 54 47 } 55 48 } 56 49 }
+17 -15
crates/scrobbler/src/handlers/v1/authenticate.rs
··· 4 4 use anyhow::Error; 5 5 use serde_json::json; 6 6 7 - use crate::{auth::{authenticate_v1, generate_session_id}, cache::Cache, params::validate_required_params}; 7 + use crate::{ 8 + auth::{authenticate_v1, generate_session_id}, 9 + cache::Cache, 10 + params::validate_required_params, 11 + }; 8 12 9 13 pub async fn authenticate( 10 14 params: BTreeMap<String, String>, 11 15 cache: &Cache, 12 16 pool: &Arc<sqlx::Pool<sqlx::Postgres>>, 13 17 ) -> Result<HttpResponse, Error> { 14 - match validate_required_params(&params, &["hs", "u", "t", "a"]) { 18 + match validate_required_params(&params, &["hs", "u", "t", "a"]) { 15 19 Ok(_) => { 16 20 let u = params.get("u").unwrap().to_string(); 17 21 let t = params.get("t").unwrap().to_string(); 18 22 let a = params.get("a").unwrap().to_string(); 19 23 20 - let scrobbler_origin_url = env::var("SCROBBLER_ORIGIN_URL").unwrap_or_else(|_| "https://audioscrobbler.rocksky.app".to_string()); 24 + let scrobbler_origin_url = env::var("SCROBBLER_ORIGIN_URL") 25 + .unwrap_or_else(|_| "https://audioscrobbler.rocksky.app".to_string()); 21 26 22 27 if authenticate_v1(&pool, &u, &t, &a).await.is_err() { 23 28 return Ok(HttpResponse::Unauthorized().json(json!({ ··· 26 31 }))); 27 32 } 28 33 29 - let session_id = generate_session_id( 30 - pool, 31 - cache, 32 - &u, 33 - ); 34 + let session_id = generate_session_id(pool, cache, &u); 34 35 35 36 let session_id = session_id.await; 36 37 if session_id.is_err() { ··· 44 45 45 46 let now_playing_url = format!("{}/nowplaying", scrobbler_origin_url); 46 47 let submission_url = format!("{}/submission", scrobbler_origin_url); 47 - Ok(HttpResponse::Ok().body(format!("OK\n{}\n{}\n{}", session_id, now_playing_url, submission_url))) 48 + Ok(HttpResponse::Ok().body(format!( 49 + "OK\n{}\n{}\n{}", 50 + session_id, now_playing_url, submission_url 51 + ))) 48 52 } 49 - Err(e) => { 50 - Ok(HttpResponse::BadRequest().json(json!({ 51 - "error": 5, 52 - "message": format!("{}", e) 53 - }))) 54 - } 53 + Err(e) => Ok(HttpResponse::BadRequest().json(json!({ 54 + "error": 5, 55 + "message": format!("{}", e) 56 + }))), 55 57 } 56 58 }
+10 -11
crates/scrobbler/src/handlers/v1/submission.rs
··· 1 - use std::{collections::BTreeMap, sync::Arc}; 2 - use anyhow::Error; 3 1 use actix_web::HttpResponse; 2 + use anyhow::Error; 4 3 use owo_colors::OwoColorize; 5 4 use serde_json::json; 5 + use std::{collections::BTreeMap, sync::Arc}; 6 6 7 - use crate::{auth::verify_session_id, cache::Cache, params::validate_required_params, scrobbler::scrobble_v1}; 7 + use crate::{ 8 + auth::verify_session_id, cache::Cache, params::validate_required_params, scrobbler::scrobble_v1, 9 + }; 8 10 9 11 pub async fn submission( 10 12 form: BTreeMap<String, String>, ··· 29 31 let user_id = user_id.unwrap(); 30 32 println!("Submission: {} - {} {} {} {}", a, t, i, user_id, s.cyan()); 31 33 32 - 33 34 match scrobble_v1(pool, cache, &form).await { 34 35 Ok(_) => Ok(HttpResponse::Ok().body("OK\n")), 35 36 Err(e) => Ok(HttpResponse::BadRequest().json(json!({ ··· 38 39 }))), 39 40 } 40 41 } 41 - Err(e) => { 42 - Ok(HttpResponse::BadRequest().json(json!({ 43 - "error": 5, 44 - "message": format!("{}", e) 45 - }))) 46 - } 42 + Err(e) => Ok(HttpResponse::BadRequest().json(json!({ 43 + "error": 5, 44 + "message": format!("{}", e) 45 + }))), 47 46 } 48 - } 47 + }
+31 -38
crates/scrobbler/src/listenbrainz/submit.rs
··· 1 - use std::sync::Arc; 2 - use anyhow::Error; 3 1 use actix_web::HttpResponse; 2 + use anyhow::Error; 4 3 use owo_colors::OwoColorize; 5 4 use serde_json::json; 5 + use std::sync::Arc; 6 6 7 7 use crate::{cache::Cache, scrobbler::scrobble_listenbrainz}; 8 8 9 9 use super::types::SubmitListensRequest; 10 10 11 11 pub async fn submit_listens( 12 - payload: SubmitListensRequest, 13 - cache: &Cache, 14 - pool: &Arc<sqlx::Pool<sqlx::Postgres>>, 15 - token: &str 12 + payload: SubmitListensRequest, 13 + cache: &Cache, 14 + pool: &Arc<sqlx::Pool<sqlx::Postgres>>, 15 + token: &str, 16 16 ) -> Result<HttpResponse, Error> { 17 - if payload.listen_type != "playing_now" { 18 - println!("skipping listen type: {}", payload.listen_type.cyan()); 19 - return Ok(HttpResponse::Ok().json( 20 - json!({ 21 - "status": "ok", 22 - "payload": { 23 - "submitted_listens": 0, 24 - "ignored_listens": 1 25 - }, 26 - }) 27 - )); 28 - } 29 - match scrobble_listenbrainz(pool, cache, payload, token) 30 - .await { 31 - Ok(_) => Ok(HttpResponse::Ok().json( 32 - json!({ 33 - "status": "ok", 34 - "payload": { 35 - "submitted_listens": 1, 36 - "ignored_listens": 0 37 - }, 38 - }) 39 - )), 40 - Err(e) => { 41 - println!("Error submitting listens: {}", e); 42 - Ok(HttpResponse::BadRequest().json( 43 - serde_json::json!({ 44 - "error": 4, 45 - "message": format!("Failed to parse listens: {}", e) 46 - }) 47 - )) 17 + if payload.listen_type != "playing_now" { 18 + println!("skipping listen type: {}", payload.listen_type.cyan()); 19 + return Ok(HttpResponse::Ok().json(json!({ 20 + "status": "ok", 21 + "payload": { 22 + "submitted_listens": 0, 23 + "ignored_listens": 1 24 + }, 25 + }))); 26 + } 27 + match scrobble_listenbrainz(pool, cache, payload, token).await { 28 + Ok(_) => Ok(HttpResponse::Ok().json(json!({ 29 + "status": "ok", 30 + "payload": { 31 + "submitted_listens": 1, 32 + "ignored_listens": 0 33 + }, 34 + }))), 35 + Err(e) => { 36 + println!("Error submitting listens: {}", e); 37 + Ok(HttpResponse::BadRequest().json(serde_json::json!({ 38 + "error": 4, 39 + "message": format!("Failed to parse listens: {}", e) 40 + }))) 41 + } 48 42 } 49 - } 50 43 }
+16 -22
crates/scrobbler/src/listenbrainz/validate_token.rs
··· 3 3 4 4 use crate::auth::decode_token; 5 5 6 - pub async fn validate_token( 7 - token: &str 8 - ) -> Result<HttpResponse, Error> { 9 - match decode_token(token) { 10 - Ok(_) => Ok(HttpResponse::Ok().json( 11 - serde_json::json!({ 12 - "status": "ok", 13 - "payload": { 14 - "valid": true, 15 - }, 16 - }) 17 - )), 18 - Err(e) => { 19 - println!("Error validating token: {}", e); 20 - Ok(HttpResponse::BadRequest().json( 21 - serde_json::json!({ 22 - "error": 4, 23 - "message": format!("Failed to validate token: {}", e) 24 - }) 25 - )) 6 + pub async fn validate_token(token: &str) -> Result<HttpResponse, Error> { 7 + match decode_token(token) { 8 + Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({ 9 + "status": "ok", 10 + "payload": { 11 + "valid": true, 12 + }, 13 + }))), 14 + Err(e) => { 15 + println!("Error validating token: {}", e); 16 + Ok(HttpResponse::BadRequest().json(serde_json::json!({ 17 + "error": 4, 18 + "message": format!("Failed to validate token: {}", e) 19 + }))) 20 + } 26 21 } 27 - } 28 - } 22 + }
+24 -15
crates/scrobbler/src/main.rs
··· 1 + pub mod auth; 2 + pub mod cache; 3 + pub mod crypto; 1 4 pub mod handlers; 2 - pub mod signature; 5 + pub mod listenbrainz; 3 6 pub mod musicbrainz; 4 - pub mod spotify; 5 - pub mod xata; 6 - pub mod cache; 7 - pub mod auth; 8 7 pub mod params; 9 - pub mod scrobbler; 8 + pub mod repo; 10 9 pub mod response; 11 - pub mod crypto; 12 10 pub mod rocksky; 13 - pub mod repo; 11 + pub mod scrobbler; 12 + pub mod signature; 13 + pub mod spotify; 14 14 pub mod types; 15 - pub mod listenbrainz; 15 + pub mod xata; 16 16 17 + use actix_limitation::{Limiter, RateLimiter}; 17 18 use actix_session::SessionExt as _; 18 - use std::{env, sync::Arc, time::Duration}; 19 - use actix_limitation::{Limiter, RateLimiter}; 20 - use actix_web::{dev::ServiceRequest, web::{self, Data}, App, HttpServer}; 19 + use actix_web::{ 20 + dev::ServiceRequest, 21 + web::{self, Data}, 22 + App, HttpServer, 23 + }; 21 24 use anyhow::Error; 22 25 use cache::Cache; 23 26 use dotenv::dotenv; 24 27 use owo_colors::OwoColorize; 25 28 use sqlx::postgres::PgPoolOptions; 26 - 29 + use std::{env, sync::Arc, time::Duration}; 27 30 28 31 pub const BANNER: &str = r#" 29 32 ___ ___ _____ __ __ __ ··· 43 46 44 47 let cache = Cache::new()?; 45 48 46 - let pool = PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; 49 + let pool = PgPoolOptions::new() 50 + .max_connections(5) 51 + .connect(&env::var("XATA_POSTGRES_URL")?) 52 + .await?; 47 53 let conn = Arc::new(pool); 48 54 49 55 let host = env::var("SCROBBLE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); ··· 52 58 .parse::<u16>() 53 59 .unwrap_or(7882); 54 60 55 - println!("Starting Scrobble server @ {}", format!("{}:{}", host, port).green()); 61 + println!( 62 + "Starting Scrobble server @ {}", 63 + format!("{}:{}", host, port).green() 64 + ); 56 65 57 66 let limiter = web::Data::new( 58 67 Limiter::builder("redis://127.0.0.1")
+4 -19
crates/scrobbler/src/musicbrainz/client.rs
··· 11 11 MusicbrainzClient {} 12 12 } 13 13 14 - pub async fn search( 15 - &self, 16 - query: &str, 17 - ) -> Result<Recordings, Error> { 14 + pub async fn search(&self, query: &str) -> Result<Recordings, Error> { 18 15 let url = format!("{}/recording", BASE_URL); 19 16 let client = reqwest::Client::new(); 20 17 let response = client 21 18 .get(&url) 22 19 .header("Accept", "application/json") 23 20 .header("User-Agent", USER_AGENT) 24 - .query( 25 - &[ 26 - ("query", query), 27 - ("inc", "artist-credits+releases"), 28 - ], 29 - ) 21 + .query(&[("query", query), ("inc", "artist-credits+releases")]) 30 22 .send() 31 23 .await?; 32 24 33 25 Ok(response.json().await?) 34 26 } 35 27 36 - pub async fn get_recording( 37 - &self, 38 - mbid: &str, 39 - ) -> Result<Recording, Error> { 28 + pub async fn get_recording(&self, mbid: &str) -> Result<Recording, Error> { 40 29 let url = format!("{}/recording/{}", BASE_URL, mbid); 41 30 let client = reqwest::Client::new(); 42 31 let response = client 43 32 .get(&url) 44 33 .header("Accept", "application/json") 45 34 .header("User-Agent", USER_AGENT) 46 - .query( 47 - &[ 48 - ("inc", "artist-credits+releases"), 49 - ], 50 - ) 35 + .query(&[("inc", "artist-credits+releases")]) 51 36 .send() 52 37 .await?; 53 38
+6 -4
crates/scrobbler/src/repo/album.rs
··· 4 4 use crate::xata::album::Album; 5 5 6 6 pub async fn get_album_by_track_id(pool: &Pool<Postgres>, track_id: &str) -> Result<Album, Error> { 7 - let results: Vec<Album> = sqlx::query_as(r#" 7 + let results: Vec<Album> = sqlx::query_as( 8 + r#" 8 9 SELECT * FROM albums 9 10 LEFT JOIN album_tracks ON albums.xata_id = album_tracks.album_id 10 11 WHERE album_tracks.track_id = $1 11 - "#) 12 + "#, 13 + ) 12 14 .bind(track_id) 13 15 .fetch_all(pool) 14 16 .await?; 15 17 16 - Ok(results[0].clone()) 17 - } 18 + Ok(results[0].clone()) 19 + }
+17 -12
crates/scrobbler/src/repo/api_key.rs
··· 3 3 4 4 use crate::xata::api_key::ApiKey; 5 5 6 - 7 - pub async fn get_apikey(pool: &Pool<Postgres>, apikey: &str, did: &str) -> Result<Option<ApiKey>, Error> { 8 - let results: Vec<ApiKey> = sqlx::query_as(r#" 6 + pub async fn get_apikey( 7 + pool: &Pool<Postgres>, 8 + apikey: &str, 9 + did: &str, 10 + ) -> Result<Option<ApiKey>, Error> { 11 + let results: Vec<ApiKey> = sqlx::query_as( 12 + r#" 9 13 SELECT * FROM api_keys 10 14 LEFT JOIN users ON api_keys.user_id = users.xata_id 11 15 WHERE api_keys.api_key = $1 AND users.did = $2 12 - "#) 13 - .bind(apikey) 14 - .bind(did) 15 - .fetch_all(pool) 16 - .await?; 16 + "#, 17 + ) 18 + .bind(apikey) 19 + .bind(did) 20 + .fetch_all(pool) 21 + .await?; 17 22 18 - if results.len() == 0 { 19 - return Ok(None); 20 - } 23 + if results.len() == 0 { 24 + return Ok(None); 25 + } 21 26 22 - Ok(Some(results[0].clone())) 27 + Ok(Some(results[0].clone())) 23 28 }
+10 -5
crates/scrobbler/src/repo/artist.rs
··· 3 3 4 4 use crate::xata::artist::Artist; 5 5 6 - pub async fn get_artist_by_track_id(pool: &Pool<Postgres>, track_id: &str) -> Result<Artist, Error> { 7 - let results: Vec<Artist> = sqlx::query_as(r#" 6 + pub async fn get_artist_by_track_id( 7 + pool: &Pool<Postgres>, 8 + track_id: &str, 9 + ) -> Result<Artist, Error> { 10 + let results: Vec<Artist> = sqlx::query_as( 11 + r#" 8 12 SELECT * FROM artists 9 13 LEFT JOIN artist_tracks ON artists.xata_id = artist_tracks.artist_id 10 14 WHERE artist_tracks.track_id = $1 11 - "#) 15 + "#, 16 + ) 12 17 .bind(track_id) 13 18 .fetch_all(pool) 14 19 .await?; 15 20 16 - Ok(results[0].clone()) 17 - } 21 + Ok(results[0].clone()) 22 + }
+27 -18
crates/scrobbler/src/repo/spotify_token.rs
··· 3 3 4 4 use crate::xata::spotify_token::SpotifyToken; 5 5 6 - 7 - pub async fn get_spotify_token(pool: &Pool<Postgres>, did: &str) -> Result<Option<SpotifyToken>, Error> { 8 - let results: Vec<SpotifyToken> = sqlx::query_as(r#" 6 + pub async fn get_spotify_token( 7 + pool: &Pool<Postgres>, 8 + did: &str, 9 + ) -> Result<Option<SpotifyToken>, Error> { 10 + let results: Vec<SpotifyToken> = sqlx::query_as( 11 + r#" 9 12 SELECT * FROM spotify_tokens 10 13 LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id 11 14 LEFT JOIN users ON spotify_accounts.user_id = users.xata_id 12 15 WHERE users.did = $1 13 - "#) 14 - .bind(did) 15 - .fetch_all(pool) 16 - .await?; 16 + "#, 17 + ) 18 + .bind(did) 19 + .fetch_all(pool) 20 + .await?; 17 21 18 - if results.len() == 0 { 19 - return Ok(None); 20 - } 22 + if results.len() == 0 { 23 + return Ok(None); 24 + } 21 25 22 - Ok(Some(results[0].clone())) 26 + Ok(Some(results[0].clone())) 23 27 } 24 28 25 - pub async fn get_spotify_tokens(pool: &Pool<Postgres>, limit: u32) -> Result<Vec<SpotifyToken>, Error> { 26 - let results: Vec<SpotifyToken> = sqlx::query_as(r#" 29 + pub async fn get_spotify_tokens( 30 + pool: &Pool<Postgres>, 31 + limit: u32, 32 + ) -> Result<Vec<SpotifyToken>, Error> { 33 + let results: Vec<SpotifyToken> = sqlx::query_as( 34 + r#" 27 35 SELECT * FROM spotify_tokens 28 36 LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id 29 37 LEFT JOIN users ON spotify_accounts.user_id = users.xata_id 30 38 LIMIT $1 31 - "#) 32 - .bind(limit as i32) 33 - .fetch_all(pool) 34 - .await?; 39 + "#, 40 + ) 41 + .bind(limit as i32) 42 + .fetch_all(pool) 43 + .await?; 35 44 36 - Ok(results) 45 + Ok(results) 37 46 }
+22 -14
crates/scrobbler/src/repo/track.rs
··· 3 3 4 4 use crate::xata::track::Track; 5 5 6 - pub async fn get_track(pool: &Pool<Postgres>, title: &str, artist: &str) -> Result<Option<Track>, Error> { 7 - let results: Vec<Track> = sqlx::query_as(r#" 6 + pub async fn get_track( 7 + pool: &Pool<Postgres>, 8 + title: &str, 9 + artist: &str, 10 + ) -> Result<Option<Track>, Error> { 11 + let results: Vec<Track> = sqlx::query_as( 12 + r#" 8 13 SELECT * FROM tracks 9 14 WHERE LOWER(title) = LOWER($1) 10 15 AND (LOWER(artist) = LOWER($2) OR LOWER(album_artist) = LOWER($2)) 11 - "#) 16 + "#, 17 + ) 12 18 .bind(title) 13 19 .bind(artist) 14 20 .fetch_all(pool) 15 21 .await?; 16 22 17 - if results.len() == 0 { 18 - return Ok(None); 19 - } 23 + if results.len() == 0 { 24 + return Ok(None); 25 + } 20 26 21 - Ok(Some(results[0].clone())) 27 + Ok(Some(results[0].clone())) 22 28 } 23 29 24 30 pub async fn get_track_by_mbid(pool: &Pool<Postgres>, mbid: &str) -> Result<Option<Track>, Error> { 25 - let results: Vec<Track> = sqlx::query_as(r#" 31 + let results: Vec<Track> = sqlx::query_as( 32 + r#" 26 33 SELECT * FROM tracks WHERE mb_id = $1 27 - "#) 34 + "#, 35 + ) 28 36 .bind(mbid) 29 37 .fetch_all(pool) 30 38 .await?; 31 39 32 - if results.len() == 0 { 33 - return Ok(None); 34 - } 40 + if results.len() == 0 { 41 + return Ok(None); 42 + } 35 43 36 - Ok(Some(results[0].clone())) 37 - } 44 + Ok(Some(results[0].clone())) 45 + }
+31 -23
crates/scrobbler/src/repo/user.rs
··· 3 3 4 4 use crate::xata::user::{User, UserWithoutSecret}; 5 5 6 - 7 - pub async fn get_user_by_apikey(pool: &Pool<Postgres>, apikey: &str) -> Result<Option<User>, Error> { 8 - let results: Vec<User> = sqlx::query_as(r#" 6 + pub async fn get_user_by_apikey( 7 + pool: &Pool<Postgres>, 8 + apikey: &str, 9 + ) -> Result<Option<User>, Error> { 10 + let results: Vec<User> = sqlx::query_as( 11 + r#" 9 12 SELECT * FROM users 10 13 LEFT JOIN api_keys ON users.xata_id = api_keys.user_id 11 14 WHERE api_keys.api_key = $1 12 - "#) 13 - .bind(apikey) 14 - .fetch_all(pool) 15 - .await?; 15 + "#, 16 + ) 17 + .bind(apikey) 18 + .fetch_all(pool) 19 + .await?; 16 20 17 - if results.is_empty(){ 18 - return Ok(None); 19 - } 21 + if results.is_empty() { 22 + return Ok(None); 23 + } 20 24 21 - Ok(Some(results[0].clone())) 25 + Ok(Some(results[0].clone())) 22 26 } 23 27 24 - 25 - pub async fn get_user_by_did(pool: &Pool<Postgres>, did: &str) -> Result<Option<UserWithoutSecret>, Error> { 26 - let results: Vec<UserWithoutSecret> = sqlx::query_as(r#" 28 + pub async fn get_user_by_did( 29 + pool: &Pool<Postgres>, 30 + did: &str, 31 + ) -> Result<Option<UserWithoutSecret>, Error> { 32 + let results: Vec<UserWithoutSecret> = sqlx::query_as( 33 + r#" 27 34 SELECT * FROM users 28 35 WHERE did = $1 29 - "#) 30 - .bind(did) 31 - .fetch_all(pool) 32 - .await?; 36 + "#, 37 + ) 38 + .bind(did) 39 + .fetch_all(pool) 40 + .await?; 33 41 34 - if results.is_empty() { 35 - return Ok(None); 36 - } 42 + if results.is_empty() { 43 + return Ok(None); 44 + } 37 45 38 - Ok(Some(results[0].clone())) 39 - } 46 + Ok(Some(results[0].clone())) 47 + }
+34 -27
crates/scrobbler/src/rocksky.rs
··· 6 6 const ROCKSKY_API: &str = "https://api.rocksky.app"; 7 7 8 8 pub async fn scrobble(cache: &Cache, did: &str, track: Track, timestamp: u64) -> Result<(), Error> { 9 - let key = format!("{} - {}", track.artist.to_lowercase(), track.title.to_lowercase()); 9 + let key = format!( 10 + "{} - {}", 11 + track.artist.to_lowercase(), 12 + track.title.to_lowercase() 13 + ); 10 14 11 - // Check if the track is already in the cache, if not add it 12 - if !cache.exists(&key)? { 13 - let value = serde_json::to_string(&track)?; 14 - let ttl = 15 * 60; // 15 minutes 15 - cache.setex(&key, &value, ttl)?; 16 - } 15 + // Check if the track is already in the cache, if not add it 16 + if !cache.exists(&key)? { 17 + let value = serde_json::to_string(&track)?; 18 + let ttl = 15 * 60; // 15 minutes 19 + cache.setex(&key, &value, ttl)?; 20 + } 17 21 18 - let mut track = track; 19 - track.timestamp = Some(timestamp); 22 + let mut track = track; 23 + track.timestamp = Some(timestamp); 20 24 21 - let token = generate_token(did)?; 22 - let client = Client::new(); 25 + let token = generate_token(did)?; 26 + let client = Client::new(); 23 27 24 - println!("Scrobbling track: \n {:#?}", track); 28 + println!("Scrobbling track: \n {:#?}", track); 25 29 26 - let response= client 27 - .post(&format!("{}/now-playing", ROCKSKY_API)) 28 - .bearer_auth(token) 29 - .json(&track) 30 - .send() 31 - .await?; 30 + let response = client 31 + .post(&format!("{}/now-playing", ROCKSKY_API)) 32 + .bearer_auth(token) 33 + .json(&track) 34 + .send() 35 + .await?; 32 36 33 - let status = response.status(); 34 - println!("Response status: {}", status); 35 - if !status.is_success() { 36 - let response_text = response.text().await?; 37 - println!("did: {}", did); 38 - println!("Failed to scrobble track: {}", response_text); 39 - return Err(Error::msg(format!("Failed to scrobble track: {}", response_text))); 40 - } 37 + let status = response.status(); 38 + println!("Response status: {}", status); 39 + if !status.is_success() { 40 + let response_text = response.text().await?; 41 + println!("did: {}", did); 42 + println!("Failed to scrobble track: {}", response_text); 43 + return Err(Error::msg(format!( 44 + "Failed to scrobble track: {}", 45 + response_text 46 + ))); 47 + } 41 48 42 - Ok(()) 49 + Ok(()) 43 50 }
+137 -56
crates/scrobbler/src/scrobbler.rs
··· 6 6 use sqlx::{Pool, Postgres}; 7 7 8 8 use crate::{ 9 - auth::{decode_token, extract_did}, cache::Cache, crypto::decrypt_aes_256_ctr, listenbrainz::types::SubmitListensRequest, musicbrainz::client::MusicbrainzClient, repo, rocksky, spotify::{ 10 - client::SpotifyClient, 11 - refresh_token 12 - }, types::{Scrobble, Track}, xata::user::User 9 + auth::{decode_token, extract_did}, 10 + cache::Cache, 11 + crypto::decrypt_aes_256_ctr, 12 + listenbrainz::types::SubmitListensRequest, 13 + musicbrainz::client::MusicbrainzClient, 14 + repo, rocksky, 15 + spotify::{client::SpotifyClient, refresh_token}, 16 + types::{Scrobble, Track}, 17 + xata::user::User, 13 18 }; 14 19 15 20 fn parse_batch(form: &BTreeMap<String, String>) -> Result<Vec<Scrobble>, Error> { ··· 25 30 break; 26 31 } 27 32 28 - let album = form.get(&format!("album[{}]", index)) 33 + let album = form 34 + .get(&format!("album[{}]", index)) 29 35 .cloned() 30 36 .map(|x| x.trim().to_string()); 31 - let context = form.get(&format!("context[{}]", index)) 37 + let context = form 38 + .get(&format!("context[{}]", index)) 32 39 .cloned() 33 40 .map(|x| x.trim().to_string()); 34 - let stream_id = form.get(&format!("streamId[{}]", index)) 41 + let stream_id = form 42 + .get(&format!("streamId[{}]", index)) 35 43 .and_then(|s| s.trim().parse().ok()); 36 44 let chosen_by_user = form 37 45 .get(&format!("chosenByUser[{}]", index)) ··· 40 48 .get(&format!("trackNumber[{}]", index)) 41 49 .and_then(|s| s.trim().parse().ok()); 42 50 let mbid = form.get(&format!("mbid[{}]", index)).cloned(); 43 - let album_artist = form.get(&format!("albumArtist[{}]", index)).map(|x| x.trim().to_string()); 51 + let album_artist = form 52 + .get(&format!("albumArtist[{}]", index)) 53 + .map(|x| x.trim().to_string()); 44 54 let duration = form 45 55 .get(&format!("duration[{}]", index)) 46 56 .and_then(|s| s.trim().parse().ok()); 47 57 48 - let timestamp = timestamp.unwrap().trim().parse().unwrap_or( 49 - chrono::Utc::now().timestamp() as u64, 50 - ); 58 + let timestamp = timestamp 59 + .unwrap() 60 + .trim() 61 + .parse() 62 + .unwrap_or(chrono::Utc::now().timestamp() as u64); 51 63 52 64 // validate timestamp, must be in the past (between 14 days before to present) 53 65 let now = chrono::Utc::now().timestamp() as u64; ··· 80 92 Ok(result) 81 93 } 82 94 83 - pub async fn scrobble(pool: &Pool<Postgres>, cache: &Cache, form: &BTreeMap<String, String>) -> Result<Vec<Scrobble>, Error> { 95 + pub async fn scrobble( 96 + pool: &Pool<Postgres>, 97 + cache: &Cache, 98 + form: &BTreeMap<String, String>, 99 + ) -> Result<Vec<Scrobble>, Error> { 84 100 let mut scrobbles = parse_batch(form)?; 85 101 86 102 if scrobbles.is_empty() { ··· 99 115 100 116 for scrobble in &mut scrobbles { 101 117 /* 102 - 0. check if scrobble is cached 103 - 1. if mbid is present, check if it exists in the database 104 - 2. if it exists, scrobble 105 - 3. if it doesn't exist, check if it exists in Musicbrainz (using mbid) 106 - 4. if it exists, get album art from spotify and scrobble 107 - 5. if it doesn't exist, check if it exists in Spotify 108 - 6. if it exists, scrobble 109 - 7. if it doesn't exist, check if it exists in Musicbrainz (using track and artist) 110 - 8. if it exists, scrobble 111 - 9. if it doesn't exist, skip unknown track 112 - */ 113 - let key = format!("{} - {}", scrobble.artist.to_lowercase(), scrobble.track.to_lowercase()); 118 + 0. check if scrobble is cached 119 + 1. if mbid is present, check if it exists in the database 120 + 2. if it exists, scrobble 121 + 3. if it doesn't exist, check if it exists in Musicbrainz (using mbid) 122 + 4. if it exists, get album art from spotify and scrobble 123 + 5. if it doesn't exist, check if it exists in Spotify 124 + 6. if it exists, scrobble 125 + 7. if it doesn't exist, check if it exists in Musicbrainz (using track and artist) 126 + 8. if it exists, scrobble 127 + 9. if it doesn't exist, skip unknown track 128 + */ 129 + let key = format!( 130 + "{} - {}", 131 + scrobble.artist.to_lowercase(), 132 + scrobble.track.to_lowercase() 133 + ); 114 134 let cached = cache.get(&key)?; 115 135 if cached.is_some() { 116 136 println!("{}", format!("Cached: {}", key).yellow()); ··· 149 169 None => None, 150 170 }, 151 171 }; 152 - track.release_date = album.release_date.map(|x| x.split("T").next().unwrap().to_string()); 172 + track.release_date = album 173 + .release_date 174 + .map(|x| x.split("T").next().unwrap().to_string()); 153 175 track.artist_picture = artist.picture.clone(); 154 176 155 177 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; ··· 168 190 169 191 let spotify_token = decrypt_aes_256_ctr( 170 192 &spotify_token.refresh_token, 171 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 193 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 172 194 )?; 173 195 174 196 let spotify_token = refresh_token(&spotify_token).await?; 175 197 let spotify_client = SpotifyClient::new(&spotify_token.access_token); 176 198 177 - let result = spotify_client.search(&format!(r#"track:"{}" artist:"{}""#, scrobble.track, scrobble.artist)).await?; 199 + let result = spotify_client 200 + .search(&format!( 201 + r#"track:"{}" artist:"{}""#, 202 + scrobble.track, scrobble.artist 203 + )) 204 + .await?; 178 205 179 206 if let Some(track) = result.tracks.items.first() { 180 207 println!("{}", "Spotify (track)".yellow()); 181 208 scrobble.album = Some(track.album.name.clone()); 182 209 let mut track = track.clone(); 183 210 184 - if let Some(album) = spotify_client.get_album(&track.album.id).await? { 211 + if let Some(album) = spotify_client.get_album(&track.album.id).await? { 185 212 track.album = album; 186 213 } 187 214 188 - if let Some(artist) = spotify_client.get_artist(&track.album.artists[0].id).await? { 215 + if let Some(artist) = spotify_client 216 + .get_artist(&track.album.artists[0].id) 217 + .await? 218 + { 189 219 track.album.artists[0] = artist; 190 220 } 191 221 ··· 204 234 let result = mb_client.get_recording(&recording.id).await?; 205 235 println!("{}", "Musicbrainz (recording)".yellow()); 206 236 scrobble.album = Some(Track::from(result.clone()).album); 207 - rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 237 + rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 208 238 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 209 239 continue; 210 240 } 211 241 212 - println!("{} {} - {}, skipping", "Track not found: ".yellow(), scrobble.artist, scrobble.track); 242 + println!( 243 + "{} {} - {}, skipping", 244 + "Track not found: ".yellow(), 245 + scrobble.artist, 246 + scrobble.track 247 + ); 213 248 scrobble.ignored = Some(true); 214 249 } 215 250 216 - 217 251 Ok(scrobbles.clone()) 218 252 } 219 253 220 - 221 - pub async fn scrobble_v1(pool: &Pool<Postgres>, cache: &Cache, form: &BTreeMap<String, String>) -> Result<(), Error> { 254 + pub async fn scrobble_v1( 255 + pool: &Pool<Postgres>, 256 + cache: &Cache, 257 + form: &BTreeMap<String, String>, 258 + ) -> Result<(), Error> { 222 259 let session_id = form.get("s").unwrap().to_string(); 223 260 let artist = form.get("a[0]").unwrap().to_string(); 224 261 let track = form.get("t[0]").unwrap().to_string(); ··· 269 306 8. if it exists, scrobble 270 307 9. if it doesn't exist, skip unknown track 271 308 */ 272 - let key = format!("{} - {}", scrobble.artist.to_lowercase(), scrobble.track.to_lowercase()); 309 + let key = format!( 310 + "{} - {}", 311 + scrobble.artist.to_lowercase(), 312 + scrobble.track.to_lowercase() 313 + ); 273 314 let cached = cache.get(&key)?; 274 315 if cached.is_some() { 275 316 println!("{}", format!("Cached: {}", key).yellow()); ··· 308 349 None => None, 309 350 }, 310 351 }; 311 - track.release_date = album.release_date.map(|x| x.split("T").next().unwrap().to_string()); 352 + track.release_date = album 353 + .release_date 354 + .map(|x| x.split("T").next().unwrap().to_string()); 312 355 track.artist_picture = artist.picture.clone(); 313 356 314 357 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; ··· 327 370 328 371 let spotify_token = decrypt_aes_256_ctr( 329 372 &spotify_token.refresh_token, 330 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 373 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 331 374 )?; 332 375 333 376 let spotify_token = refresh_token(&spotify_token).await?; 334 377 let spotify_client = SpotifyClient::new(&spotify_token.access_token); 335 378 336 - let result = spotify_client.search(&format!(r#"track:"{}" artist:"{}""#, scrobble.track, scrobble.artist)).await?; 379 + let result = spotify_client 380 + .search(&format!( 381 + r#"track:"{}" artist:"{}""#, 382 + scrobble.track, scrobble.artist 383 + )) 384 + .await?; 337 385 338 386 if let Some(track) = result.tracks.items.first() { 339 387 println!("{}", "Spotify (track)".yellow()); 340 388 scrobble.album = Some(track.album.name.clone()); 341 389 let mut track = track.clone(); 342 390 343 - if let Some(album) = spotify_client.get_album(&track.album.id).await? { 391 + if let Some(album) = spotify_client.get_album(&track.album.id).await? { 344 392 track.album = album; 345 393 } 346 394 347 - if let Some(artist) = spotify_client.get_artist(&track.album.artists[0].id).await? { 395 + if let Some(artist) = spotify_client 396 + .get_artist(&track.album.artists[0].id) 397 + .await? 398 + { 348 399 track.album.artists[0] = artist; 349 400 } 350 401 ··· 363 414 let result = mb_client.get_recording(&recording.id).await?; 364 415 println!("{}", "Musicbrainz (recording)".yellow()); 365 416 scrobble.album = Some(Track::from(result.clone()).album); 366 - rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 417 + rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 367 418 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 368 419 return Ok(()); 369 420 } 370 421 371 - println!("{} {} - {}, skipping", "Track not found: ".yellow(), artist, track); 422 + println!( 423 + "{} {} - {}, skipping", 424 + "Track not found: ".yellow(), 425 + artist, 426 + track 427 + ); 372 428 373 429 Ok(()) 374 430 } 375 431 376 - pub async fn scrobble_listenbrainz(pool: &Pool<Postgres>, cache: &Cache, req: SubmitListensRequest, token: &str) -> Result<(), Error> { 432 + pub async fn scrobble_listenbrainz( 433 + pool: &Pool<Postgres>, 434 + cache: &Cache, 435 + req: SubmitListensRequest, 436 + token: &str, 437 + ) -> Result<(), Error> { 377 438 println!("Listenbrainz\n{:#?}", req); 378 439 379 440 if req.payload.is_empty() { ··· 396 457 if let Some(did) = user { 397 458 did 398 459 } else { 399 - return Err(Error::msg(format!("Failed to decode token: {} {}", e, token))); 460 + return Err(Error::msg(format!( 461 + "Failed to decode token: {} {}", 462 + e, token 463 + ))); 400 464 } 401 465 } 402 466 }; 403 467 404 - let user = repo::user::get_user_by_did(pool, &did) 405 - .await?; 468 + let user = repo::user::get_user_by_did(pool, &did).await?; 406 469 407 470 if user.is_none() { 408 471 return Err(Error::msg("User not found")); 409 472 } 410 - 411 473 412 474 cache.setex( 413 475 &format!("listenbrainz:emby:{}:{}:{}", artist, track, did), ··· 450 512 8. if it exists, scrobble 451 513 9. if it doesn't exist, skip unknown track 452 514 */ 453 - let key = format!("{} - {}", scrobble.artist.to_lowercase(), scrobble.track.to_lowercase()); 515 + let key = format!( 516 + "{} - {}", 517 + scrobble.artist.to_lowercase(), 518 + scrobble.track.to_lowercase() 519 + ); 454 520 let cached = cache.get(&key)?; 455 521 if cached.is_some() { 456 522 println!("{}", format!("Cached: {}", key).yellow()); ··· 489 555 None => None, 490 556 }, 491 557 }; 492 - track.release_date = album.release_date.map(|x| x.split("T").next().unwrap().to_string()); 558 + track.release_date = album 559 + .release_date 560 + .map(|x| x.split("T").next().unwrap().to_string()); 493 561 track.artist_picture = artist.picture.clone(); 494 562 495 563 rocksky::scrobble(cache, &did, track, scrobble.timestamp).await?; ··· 508 576 509 577 let spotify_token = decrypt_aes_256_ctr( 510 578 &spotify_token.refresh_token, 511 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 579 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 512 580 )?; 513 581 514 582 let spotify_token = refresh_token(&spotify_token).await?; 515 583 let spotify_client = SpotifyClient::new(&spotify_token.access_token); 516 584 517 - let result = spotify_client.search(&format!(r#"track:"{}" artist:"{}""#, scrobble.track, scrobble.artist)).await?; 585 + let result = spotify_client 586 + .search(&format!( 587 + r#"track:"{}" artist:"{}""#, 588 + scrobble.track, scrobble.artist 589 + )) 590 + .await?; 518 591 519 592 if let Some(track) = result.tracks.items.first() { 520 593 println!("{}", "Spotify (track)".yellow()); 521 594 scrobble.album = Some(track.album.name.clone()); 522 595 let mut track = track.clone(); 523 596 524 - if let Some(album) = spotify_client.get_album(&track.album.id).await? { 597 + if let Some(album) = spotify_client.get_album(&track.album.id).await? { 525 598 track.album = album; 526 599 } 527 600 528 - if let Some(artist) = spotify_client.get_artist(&track.album.artists[0].id).await? { 601 + if let Some(artist) = spotify_client 602 + .get_artist(&track.album.artists[0].id) 603 + .await? 604 + { 529 605 track.album.artists[0] = artist; 530 606 } 531 607 ··· 544 620 let result = mb_client.get_recording(&recording.id).await?; 545 621 println!("{}", "Musicbrainz (recording)".yellow()); 546 622 scrobble.album = Some(Track::from(result.clone()).album); 547 - rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 623 + rocksky::scrobble(cache, &did, result.into(), scrobble.timestamp).await?; 548 624 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 549 625 return Ok(()); 550 626 } 551 627 552 - println!("{} {} - {}, skipping", "Track not found: ".yellow(), artist, track); 628 + println!( 629 + "{} {} - {}, skipping", 630 + "Track not found: ".yellow(), 631 + artist, 632 + track 633 + ); 553 634 554 635 Ok(()) 555 - } 636 + }
+38 -37
crates/scrobbler/src/spotify/client.rs
··· 4 4 pub const BASE_URL: &str = "https://api.spotify.com/v1"; 5 5 6 6 pub struct SpotifyClient { 7 - token: String, 7 + token: String, 8 8 } 9 9 10 10 impl SpotifyClient { ··· 17 17 pub async fn search(&self, query: &str) -> Result<SearchResponse, Error> { 18 18 let url = format!("{}/search", BASE_URL); 19 19 let client = reqwest::Client::new(); 20 - let response = client.get(&url) 21 - .bearer_auth(&self.token) 22 - .query(&[ 23 - ("type", "track"), 24 - ("q", query), 25 - ]) 26 - .send().await?; 20 + let response = client 21 + .get(&url) 22 + .bearer_auth(&self.token) 23 + .query(&[("type", "track"), ("q", query)]) 24 + .send() 25 + .await?; 27 26 let result = response.json().await?; 28 27 Ok(result) 29 28 } 30 29 31 30 pub async fn get_album(&self, id: &str) -> Result<Option<Album>, Error> { 32 - let url = format!("{}/albums/{}", BASE_URL, id); 33 - let client = reqwest::Client::new(); 34 - let response = client.get(&url) 35 - .bearer_auth(&self.token) 36 - .send().await?; 31 + let url = format!("{}/albums/{}", BASE_URL, id); 32 + let client = reqwest::Client::new(); 33 + let response = client.get(&url).bearer_auth(&self.token).send().await?; 37 34 38 - let headers = response.headers().clone(); 39 - let data = response.text().await?; 35 + let headers = response.headers().clone(); 36 + let data = response.text().await?; 40 37 41 - if data == "Too many requests" { 42 - println!("> retry-after {}", headers.get("retry-after").unwrap().to_str().unwrap()); 43 - println!("> {} [get_album]", data); 44 - return Ok(None); 45 - } 38 + if data == "Too many requests" { 39 + println!( 40 + "> retry-after {}", 41 + headers.get("retry-after").unwrap().to_str().unwrap() 42 + ); 43 + println!("> {} [get_album]", data); 44 + return Ok(None); 45 + } 46 46 47 - Ok(Some(serde_json::from_str(&data)?)) 48 - } 47 + Ok(Some(serde_json::from_str(&data)?)) 48 + } 49 49 50 - pub async fn get_artist(&self, id: &str) -> Result<Option<Artist>, Error> { 51 - let url = format!("{}/artists/{}", BASE_URL, id); 52 - let client = reqwest::Client::new(); 53 - let response = client.get(&url) 54 - .bearer_auth(&self.token) 55 - .send().await?; 50 + pub async fn get_artist(&self, id: &str) -> Result<Option<Artist>, Error> { 51 + let url = format!("{}/artists/{}", BASE_URL, id); 52 + let client = reqwest::Client::new(); 53 + let response = client.get(&url).bearer_auth(&self.token).send().await?; 56 54 57 - let headers = response.headers().clone(); 58 - let data = response.text().await?; 55 + let headers = response.headers().clone(); 56 + let data = response.text().await?; 59 57 60 - if data == "Too many requests" { 61 - println!("> retry-after {}", headers.get("retry-after").unwrap().to_str().unwrap()); 62 - println!("> {} [get_artist]", data); 63 - return Ok(None); 64 - } 58 + if data == "Too many requests" { 59 + println!( 60 + "> retry-after {}", 61 + headers.get("retry-after").unwrap().to_str().unwrap() 62 + ); 63 + println!("> {} [get_artist]", data); 64 + return Ok(None); 65 + } 65 66 66 - Ok(Some(serde_json::from_str(&data)?)) 67 - } 67 + Ok(Some(serde_json::from_str(&data)?)) 68 + } 68 69 }
+20 -20
crates/scrobbler/src/spotify/mod.rs
··· 1 1 use std::env; 2 2 3 + use anyhow::Error; 3 4 use reqwest::Client; 4 5 use types::AccessToken; 5 - use anyhow::Error; 6 6 7 7 pub mod client; 8 8 pub mod types; 9 - 10 9 11 10 pub async fn refresh_token(token: &str) -> Result<AccessToken, Error> { 12 - if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { 13 - panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); 14 - } 11 + if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { 12 + panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); 13 + } 15 14 16 - let client_id = env::var("SPOTIFY_CLIENT_ID")?; 17 - let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; 15 + let client_id = env::var("SPOTIFY_CLIENT_ID")?; 16 + let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; 18 17 19 - let client = Client::new(); 18 + let client = Client::new(); 20 19 21 - let response = client.post("https://accounts.spotify.com/api/token") 22 - .basic_auth(&client_id, Some(client_secret)) 23 - .form(&[ 24 - ("grant_type", "refresh_token"), 25 - ("refresh_token", token), 26 - ("client_id", &client_id) 27 - ]) 28 - .send() 29 - .await?; 30 - let token = response.json::<AccessToken>().await?; 31 - Ok(token) 32 - } 20 + let response = client 21 + .post("https://accounts.spotify.com/api/token") 22 + .basic_auth(&client_id, Some(client_secret)) 23 + .form(&[ 24 + ("grant_type", "refresh_token"), 25 + ("refresh_token", token), 26 + ("client_id", &client_id), 27 + ]) 28 + .send() 29 + .await?; 30 + let token = response.json::<AccessToken>().await?; 31 + Ok(token) 32 + }
+488 -337
crates/spotify/src/main.rs
··· 1 - use std::{collections::HashMap, env, sync::{atomic::AtomicBool, Arc, Mutex}, thread}; 1 + use std::{ 2 + collections::HashMap, 3 + env, 4 + sync::{atomic::AtomicBool, Arc, Mutex}, 5 + thread, 6 + }; 2 7 8 + use anyhow::Error; 9 + use async_nats::connect; 3 10 use cache::Cache; 4 11 use crypto::decrypt_aes_256_ctr; 5 12 use dotenv::dotenv; 13 + use owo_colors::OwoColorize; 6 14 use reqwest::Client; 7 - use anyhow::Error; 8 15 use rocksky::{scrobble, update_library}; 9 16 use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; 10 17 use tokio_stream::StreamExt; 11 - use types::{album_tracks::AlbumTracks, currently_playing::{Album, Artist, CurrentlyPlaying}, spotify_token::SpotifyTokenWithEmail, token::AccessToken}; 12 - use owo_colors::OwoColorize; 13 - use async_nats::connect; 18 + use types::{ 19 + album_tracks::AlbumTracks, 20 + currently_playing::{Album, Artist, CurrentlyPlaying}, 21 + spotify_token::SpotifyTokenWithEmail, 22 + token::AccessToken, 23 + }; 14 24 15 - pub mod types; 16 25 pub mod cache; 17 26 pub mod crypto; 18 27 pub mod rocksky; 19 28 pub mod token; 29 + pub mod types; 20 30 21 31 const BASE_URL: &str = "https://spotify-api.rocksky.app/v1"; 22 32 ··· 40 50 println!("Found {} users", users.len().bright_green()); 41 51 42 52 // Shared HashMap to manage threads and their stop flags 43 - let thread_map: Arc<Mutex<HashMap<String, Arc<AtomicBool>>>> = Arc::new(Mutex::new(HashMap::new())); 53 + let thread_map: Arc<Mutex<HashMap<String, Arc<AtomicBool>>>> = 54 + Arc::new(Mutex::new(HashMap::new())); 44 55 45 56 // Start threads for all users 46 57 for user in users { ··· 51 62 let cache = cache.clone(); 52 63 let thread_map = Arc::clone(&thread_map); 53 64 54 - thread_map.lock().unwrap().insert(email.clone(), Arc::clone(&stop_flag)); 65 + thread_map 66 + .lock() 67 + .unwrap() 68 + .insert(email.clone(), Arc::clone(&stop_flag)); 55 69 56 70 thread::spawn(move || { 57 71 let rt = tokio::runtime::Runtime::new().unwrap(); 58 72 match rt.block_on(async { 59 - watch_currently_playing(email.clone(), token, did, stop_flag, cache.clone()).await?; 73 + watch_currently_playing(email.clone(), token, did, stop_flag, cache.clone()) 74 + .await?; 60 75 Ok::<(), Error>(()) 61 76 }) { 62 - Ok(_) => { 63 - } 77 + Ok(_) => {} 64 78 Err(e) => { 65 - println!("{} Error starting thread for user: {} - {}", format!("[{}]", email).bright_green(), email.bright_green(), e.to_string().bright_red()); 79 + println!( 80 + "{} Error starting thread for user: {} - {}", 81 + format!("[{}]", email).bright_green(), 82 + email.bright_green(), 83 + e.to_string().bright_red() 84 + ); 66 85 } 67 86 } 68 87 }); ··· 71 90 // Handle subscription messages 72 91 while let Some(message) = sub.next().await { 73 92 let user_id = String::from_utf8(message.payload.to_vec()).unwrap(); 74 - println!("Received message to restart thread for user: {}", user_id.bright_green()); 93 + println!( 94 + "Received message to restart thread for user: {}", 95 + user_id.bright_green() 96 + ); 75 97 76 98 let mut thread_map = thread_map.lock().unwrap(); 77 99 ··· 87 109 let user = find_spotify_user(&pool, &user_id).await?; 88 110 89 111 if user.is_none() { 90 - println!("Spotify user not found: {}, skipping", user_id.bright_green()); 91 - continue; 112 + println!( 113 + "Spotify user not found: {}, skipping", 114 + user_id.bright_green() 115 + ); 116 + continue; 92 117 } 93 118 94 119 let user = user.unwrap(); ··· 101 126 thread::spawn(move || { 102 127 let rt = tokio::runtime::Runtime::new().unwrap(); 103 128 match rt.block_on(async { 104 - watch_currently_playing(email.clone(), token, did, new_stop_flag, cache.clone()).await?; 129 + watch_currently_playing( 130 + email.clone(), 131 + token, 132 + did, 133 + new_stop_flag, 134 + cache.clone(), 135 + ) 136 + .await?; 105 137 Ok::<(), Error>(()) 106 138 }) { 107 - Ok(_) => {}, 108 - Err(e) => { 109 - println!("{} Error restarting thread for user: {} - {}", format!("[{}]", email).bright_green(), email.bright_green(), e.to_string().bright_red()); 110 - } 139 + Ok(_) => {} 140 + Err(e) => { 141 + println!( 142 + "{} Error restarting thread for user: {} - {}", 143 + format!("[{}]", email).bright_green(), 144 + email.bright_green(), 145 + e.to_string().bright_red() 146 + ); 147 + } 111 148 } 112 149 }); 113 150 114 151 println!("Restarted thread for user: {}", user_id.bright_green()); 115 152 } else { 116 - println!("No thread found for user: {}, starting new thread", user_id.bright_green()); 153 + println!( 154 + "No thread found for user: {}, starting new thread", 155 + user_id.bright_green() 156 + ); 117 157 let user = find_spotify_user(&pool, &user_id).await?; 118 158 if let Some(user) = user { 119 - let email = user.0.clone(); 120 - let token = user.1.clone(); 121 - let did = user.2.clone(); 122 - let stop_flag = Arc::new(AtomicBool::new(false)); 123 - let cache = cache.clone(); 159 + let email = user.0.clone(); 160 + let token = user.1.clone(); 161 + let did = user.2.clone(); 162 + let stop_flag = Arc::new(AtomicBool::new(false)); 163 + let cache = cache.clone(); 124 164 125 - thread_map.insert(email.clone(), Arc::clone(&stop_flag)); 165 + thread_map.insert(email.clone(), Arc::clone(&stop_flag)); 126 166 127 - thread::spawn(move || { 128 - let rt = tokio::runtime::Runtime::new().unwrap(); 129 - match rt.block_on(async { 130 - watch_currently_playing(email.clone(), token, did, stop_flag, cache.clone()).await?; 131 - Ok::<(), Error>(()) 132 - }) { 133 - Ok(_) => {}, 134 - Err(e) => { 135 - println!("{} Error starting thread for user: {} - {}", format!("[{}]", email).bright_green(), email.bright_green(), e.to_string().bright_red()); 167 + thread::spawn(move || { 168 + let rt = tokio::runtime::Runtime::new().unwrap(); 169 + match rt.block_on(async { 170 + watch_currently_playing( 171 + email.clone(), 172 + token, 173 + did, 174 + stop_flag, 175 + cache.clone(), 176 + ) 177 + .await?; 178 + Ok::<(), Error>(()) 179 + }) { 180 + Ok(_) => {} 181 + Err(e) => { 182 + println!( 183 + "{} Error starting thread for user: {} - {}", 184 + format!("[{}]", email).bright_green(), 185 + email.bright_green(), 186 + e.to_string().bright_red() 187 + ); 188 + } 136 189 } 137 - } 138 - }); 190 + }); 139 191 } 140 192 } 141 193 } ··· 144 196 } 145 197 146 198 pub async fn refresh_token(token: &str) -> Result<AccessToken, Error> { 147 - if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { 148 - panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); 149 - } 199 + if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { 200 + panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); 201 + } 150 202 151 - let client_id = env::var("SPOTIFY_CLIENT_ID")?; 152 - let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; 203 + let client_id = env::var("SPOTIFY_CLIENT_ID")?; 204 + let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; 153 205 154 - let client = Client::new(); 206 + let client = Client::new(); 155 207 156 - let response = client.post("https://accounts.spotify.com/api/token") 157 - .basic_auth(&client_id, Some(client_secret)) 158 - .form(&[ 159 - ("grant_type", "refresh_token"), 160 - ("refresh_token", token), 161 - ("client_id", &client_id) 162 - ]) 163 - .send() 164 - .await?; 165 - let token = response.json::<AccessToken>().await?; 166 - Ok(token) 208 + let response = client 209 + .post("https://accounts.spotify.com/api/token") 210 + .basic_auth(&client_id, Some(client_secret)) 211 + .form(&[ 212 + ("grant_type", "refresh_token"), 213 + ("refresh_token", token), 214 + ("client_id", &client_id), 215 + ]) 216 + .send() 217 + .await?; 218 + let token = response.json::<AccessToken>().await?; 219 + Ok(token) 167 220 } 168 221 169 - pub async fn get_currently_playing(cache: Cache, user_id: &str, token: &str) -> Result<Option<(CurrentlyPlaying, bool)>, Error> { 170 - if let Ok(Some(data)) = cache.get(user_id) { 171 - println!("{} {}", format!("[{}]", user_id).bright_green(), "Using cache".cyan()); 172 - if data == "No content" { 173 - return Ok(None); 174 - } 175 - let decoded_data = serde_json::from_str::<CurrentlyPlaying>(&data); 176 - 177 - if decoded_data.is_err() { 178 - println!("{} {} {}", format!("[{}]", user_id).bright_green(), "Cache is invalid".red(), data); 179 - cache.setex(user_id, "No content", 10)?; 180 - cache.del(&format!("{}:current", user_id))?; 181 - return Ok(None); 182 - } 183 - 184 - let data: CurrentlyPlaying = decoded_data.unwrap(); 185 - // detect if the song has changed 186 - let previous = cache.get(&format!("{}:previous", user_id))?; 187 - let changed = match previous { 188 - Some(previous) => { 189 - let previous: CurrentlyPlaying = serde_json::from_str(&previous)?; 190 - if previous.item.is_none() && data.item.is_some() { 191 - return Ok(Some((data, true))); 222 + pub async fn get_currently_playing( 223 + cache: Cache, 224 + user_id: &str, 225 + token: &str, 226 + ) -> Result<Option<(CurrentlyPlaying, bool)>, Error> { 227 + if let Ok(Some(data)) = cache.get(user_id) { 228 + println!( 229 + "{} {}", 230 + format!("[{}]", user_id).bright_green(), 231 + "Using cache".cyan() 232 + ); 233 + if data == "No content" { 234 + return Ok(None); 192 235 } 236 + let decoded_data = serde_json::from_str::<CurrentlyPlaying>(&data); 193 237 194 - if previous.item.is_some() && data.item.is_none() { 195 - return Ok(Some((data, false))); 238 + if decoded_data.is_err() { 239 + println!( 240 + "{} {} {}", 241 + format!("[{}]", user_id).bright_green(), 242 + "Cache is invalid".red(), 243 + data 244 + ); 245 + cache.setex(user_id, "No content", 10)?; 246 + cache.del(&format!("{}:current", user_id))?; 247 + return Ok(None); 196 248 } 197 249 198 - if previous.item.is_none() && data.item.is_none() { 199 - return Ok(Some((data, false))); 200 - } 250 + let data: CurrentlyPlaying = decoded_data.unwrap(); 251 + // detect if the song has changed 252 + let previous = cache.get(&format!("{}:previous", user_id))?; 253 + let changed = match previous { 254 + Some(previous) => { 255 + let previous: CurrentlyPlaying = serde_json::from_str(&previous)?; 256 + if previous.item.is_none() && data.item.is_some() { 257 + return Ok(Some((data, true))); 258 + } 201 259 202 - let previous_item = previous.item.unwrap(); 203 - let data_item = data.clone().item.unwrap(); 204 - previous_item.id != data_item.id && previous.progress_ms.unwrap_or(0) != data.progress_ms.unwrap_or(0) 205 - }, 206 - _ => true 207 - }; 208 - return Ok(Some((data, changed))); 209 - } 260 + if previous.item.is_some() && data.item.is_none() { 261 + return Ok(Some((data, false))); 262 + } 210 263 264 + if previous.item.is_none() && data.item.is_none() { 265 + return Ok(Some((data, false))); 266 + } 211 267 212 - let token = refresh_token(token).await?; 213 - let client = Client::new(); 214 - let response = client.get(format!("{}/me/player/currently-playing", BASE_URL)) 215 - .bearer_auth(token.access_token) 216 - .send() 217 - .await?; 268 + let previous_item = previous.item.unwrap(); 269 + let data_item = data.clone().item.unwrap(); 270 + previous_item.id != data_item.id 271 + && previous.progress_ms.unwrap_or(0) != data.progress_ms.unwrap_or(0) 272 + } 273 + _ => true, 274 + }; 275 + return Ok(Some((data, changed))); 276 + } 218 277 278 + let token = refresh_token(token).await?; 279 + let client = Client::new(); 280 + let response = client 281 + .get(format!("{}/me/player/currently-playing", BASE_URL)) 282 + .bearer_auth(token.access_token) 283 + .send() 284 + .await?; 219 285 220 - let headers = response.headers().clone(); 221 - let status = response.status().as_u16(); 222 - let data = response.text().await?; 286 + let headers = response.headers().clone(); 287 + let status = response.status().as_u16(); 288 + let data = response.text().await?; 223 289 224 - if status == 429 { 225 - println!("{} Too many requests, retry-after {}", format!("[{}]", user_id).bright_green(), headers.get("retry-after").unwrap().to_str().unwrap().bright_green()); 226 - return Ok(None); 227 - } 290 + if status == 429 { 291 + println!( 292 + "{} Too many requests, retry-after {}", 293 + format!("[{}]", user_id).bright_green(), 294 + headers 295 + .get("retry-after") 296 + .unwrap() 297 + .to_str() 298 + .unwrap() 299 + .bright_green() 300 + ); 301 + return Ok(None); 302 + } 228 303 229 - let previous = cache.get(&format!("{}:previous", user_id))?; 304 + let previous = cache.get(&format!("{}:previous", user_id))?; 230 305 231 - // check if status code is 204 232 - if status == 204 { 233 - println!("No content"); 234 - cache.setex(user_id, "No content", match previous.is_none() { 235 - true => 30, 236 - false => 10, 306 + // check if status code is 204 307 + if status == 204 { 308 + println!("No content"); 309 + cache.setex( 310 + user_id, 311 + "No content", 312 + match previous.is_none() { 313 + true => 30, 314 + false => 10, 315 + }, 316 + )?; 317 + cache.del(&format!("{}:current", user_id))?; 318 + return Ok(None); 237 319 } 238 - )?; 239 - cache.del(&format!("{}:current", user_id))?; 240 - return Ok(None); 241 - } 242 320 243 - let data = serde_json::from_str::<CurrentlyPlaying>(&data)?; 321 + let data = serde_json::from_str::<CurrentlyPlaying>(&data)?; 244 322 245 - cache.setex(user_id, &serde_json::to_string(&data)?, match previous.is_none() { 246 - true => 30, 247 - false => 15, 248 - })?; 249 - cache.del(&format!("{}:current", user_id))?; 323 + cache.setex( 324 + user_id, 325 + &serde_json::to_string(&data)?, 326 + match previous.is_none() { 327 + true => 30, 328 + false => 15, 329 + }, 330 + )?; 331 + cache.del(&format!("{}:current", user_id))?; 250 332 251 - // detect if the song has changed 252 - let previous = cache.get(&format!("{}:previous", user_id))?; 253 - let changed = match previous { 254 - Some(previous) => { 255 - let previous: CurrentlyPlaying = serde_json::from_str(&previous)?; 256 - if previous.item.is_none() || data.item.is_none() { 257 - return Ok(Some((data, false))); 258 - } 333 + // detect if the song has changed 334 + let previous = cache.get(&format!("{}:previous", user_id))?; 335 + let changed = match previous { 336 + Some(previous) => { 337 + let previous: CurrentlyPlaying = serde_json::from_str(&previous)?; 338 + if previous.item.is_none() || data.item.is_none() { 339 + return Ok(Some((data, false))); 340 + } 259 341 260 - let previous_item = previous.item.unwrap(); 261 - let data_item = data.clone().item.unwrap(); 342 + let previous_item = previous.item.unwrap(); 343 + let data_item = data.clone().item.unwrap(); 262 344 263 - previous_item.id != data_item.id && previous.progress_ms.unwrap_or(0) != data.progress_ms.unwrap_or(0) 264 - }, 265 - _ => false 266 - }; 345 + previous_item.id != data_item.id 346 + && previous.progress_ms.unwrap_or(0) != data.progress_ms.unwrap_or(0) 347 + } 348 + _ => false, 349 + }; 267 350 268 - // save as previous song 269 - cache.setex(&format!("{}:previous", user_id), &serde_json::to_string(&data)?, 600)?; 351 + // save as previous song 352 + cache.setex( 353 + &format!("{}:previous", user_id), 354 + &serde_json::to_string(&data)?, 355 + 600, 356 + )?; 270 357 271 - Ok(Some((data, changed))) 358 + Ok(Some((data, changed))) 272 359 } 273 360 274 - pub async fn get_artist(cache: Cache, artist_id: &str, token: &str) -> Result<Option<Artist>, Error> { 275 - if let Ok(Some(data)) = cache.get(artist_id) { 276 - return Ok(Some(serde_json::from_str(&data)?)); 277 - } 361 + pub async fn get_artist( 362 + cache: Cache, 363 + artist_id: &str, 364 + token: &str, 365 + ) -> Result<Option<Artist>, Error> { 366 + if let Ok(Some(data)) = cache.get(artist_id) { 367 + return Ok(Some(serde_json::from_str(&data)?)); 368 + } 278 369 279 - let token = refresh_token(token).await?; 280 - let client = Client::new(); 281 - let response = client.get(&format!("{}/artists/{}", BASE_URL, artist_id)) 282 - .bearer_auth(token.access_token) 283 - .send() 284 - .await?; 370 + let token = refresh_token(token).await?; 371 + let client = Client::new(); 372 + let response = client 373 + .get(&format!("{}/artists/{}", BASE_URL, artist_id)) 374 + .bearer_auth(token.access_token) 375 + .send() 376 + .await?; 285 377 378 + let headers = response.headers().clone(); 379 + let data = response.text().await?; 286 380 287 - let headers = response.headers().clone(); 288 - let data = response.text().await?; 381 + if data == "Too many requests" { 382 + println!( 383 + "> retry-after {}", 384 + headers.get("retry-after").unwrap().to_str().unwrap() 385 + ); 386 + println!("> {} [get_artist]", data); 387 + return Ok(None); 388 + } 289 389 290 - if data == "Too many requests" { 291 - println!("> retry-after {}", headers.get("retry-after").unwrap().to_str().unwrap()); 292 - println!("> {} [get_artist]", data); 293 - return Ok(None); 294 - } 390 + cache.setex(artist_id, &data, 20)?; 295 391 296 - cache.setex(artist_id, &data, 20)?; 297 - 298 - Ok(Some(serde_json::from_str(&data)?)) 392 + Ok(Some(serde_json::from_str(&data)?)) 299 393 } 300 394 301 395 pub async fn get_album(cache: Cache, album_id: &str, token: &str) -> Result<Option<Album>, Error> { 302 - if let Ok(Some(data)) = cache.get(album_id) { 303 - return Ok(Some(serde_json::from_str(&data)?)); 304 - } 396 + if let Ok(Some(data)) = cache.get(album_id) { 397 + return Ok(Some(serde_json::from_str(&data)?)); 398 + } 305 399 306 - let token = refresh_token(token).await?; 307 - let client = Client::new(); 308 - let response = client.get(&format!("{}/albums/{}", BASE_URL, album_id)) 309 - .bearer_auth(token.access_token) 310 - .send() 311 - .await?; 400 + let token = refresh_token(token).await?; 401 + let client = Client::new(); 402 + let response = client 403 + .get(&format!("{}/albums/{}", BASE_URL, album_id)) 404 + .bearer_auth(token.access_token) 405 + .send() 406 + .await?; 312 407 313 408 let headers = response.headers().clone(); 314 409 let data = response.text().await?; 315 410 316 411 if data == "Too many requests" { 317 - println!("> retry-after {}", headers.get("retry-after").unwrap().to_str().unwrap()); 318 - println!("> {} [get_album]", data); 319 - return Ok(None); 412 + println!( 413 + "> retry-after {}", 414 + headers.get("retry-after").unwrap().to_str().unwrap() 415 + ); 416 + println!("> {} [get_album]", data); 417 + return Ok(None); 320 418 } 321 419 322 - cache.setex(album_id, &data, 20)?; 420 + cache.setex(album_id, &data, 20)?; 323 421 324 - Ok(Some(serde_json::from_str(&data)?)) 422 + Ok(Some(serde_json::from_str(&data)?)) 325 423 } 326 424 327 - pub async fn get_album_tracks(cache: Cache, album_id: &str, token: &str) -> Result<AlbumTracks, Error> { 328 - if let Ok(Some(data)) = cache.get(&format!("{}:tracks", album_id)) { 329 - return Ok(serde_json::from_str(&data)?); 330 - } 425 + pub async fn get_album_tracks( 426 + cache: Cache, 427 + album_id: &str, 428 + token: &str, 429 + ) -> Result<AlbumTracks, Error> { 430 + if let Ok(Some(data)) = cache.get(&format!("{}:tracks", album_id)) { 431 + return Ok(serde_json::from_str(&data)?); 432 + } 331 433 332 - let token = refresh_token(token).await?; 333 - let client = Client::new(); 334 - let mut all_tracks = Vec::new(); 335 - let mut offset = 0; 336 - let limit = 50; 434 + let token = refresh_token(token).await?; 435 + let client = Client::new(); 436 + let mut all_tracks = Vec::new(); 437 + let mut offset = 0; 438 + let limit = 50; 337 439 440 + loop { 441 + let response = client 442 + .get(&format!("{}/albums/{}/tracks", BASE_URL, album_id)) 443 + .bearer_auth(&token.access_token) 444 + .query(&[ 445 + ("limit", &limit.to_string()), 446 + ("offset", &offset.to_string()), 447 + ]) 448 + .send() 449 + .await?; 338 450 339 - loop { 340 - let response = client.get(&format!("{}/albums/{}/tracks", BASE_URL, album_id)) 341 - .bearer_auth(&token.access_token) 342 - .query(&[("limit", &limit.to_string()), ("offset", &offset.to_string())]) 343 - .send() 344 - .await?; 451 + let headers = response.headers().clone(); 452 + let data = response.text().await?; 453 + if data == "Too many requests" { 454 + println!( 455 + "> retry-after {}", 456 + headers.get("retry-after").unwrap().to_str().unwrap() 457 + ); 458 + println!("> {} [get_album_tracks]", data); 459 + continue; 460 + } 345 461 346 - let headers = response.headers().clone(); 347 - let data = response.text().await?; 348 - if data == "Too many requests" { 349 - println!("> retry-after {}", headers.get("retry-after").unwrap().to_str().unwrap()); 350 - println!("> {} [get_album_tracks]", data); 351 - continue; 352 - } 462 + let album_tracks: AlbumTracks = serde_json::from_str(&data)?; 353 463 354 - let album_tracks: AlbumTracks = serde_json::from_str(&data)?; 464 + if album_tracks.items.is_empty() { 465 + break; 466 + } 355 467 356 - if album_tracks.items.is_empty() { 357 - break; 358 - } 359 - 360 - all_tracks.extend(album_tracks.items); 361 - offset += limit; 362 - } 468 + all_tracks.extend(album_tracks.items); 469 + offset += limit; 470 + } 363 471 364 - let all_tracks_json = serde_json::to_string(&all_tracks)?; 365 - cache.setex(&format!("{}:tracks", album_id), &all_tracks_json, 20)?; 472 + let all_tracks_json = serde_json::to_string(&all_tracks)?; 473 + cache.setex(&format!("{}:tracks", album_id), &all_tracks_json, 20)?; 366 474 367 - Ok(AlbumTracks { 368 - items: all_tracks, 369 - ..Default::default() 370 - }) 475 + Ok(AlbumTracks { 476 + items: all_tracks, 477 + ..Default::default() 478 + }) 371 479 } 372 480 373 481 pub async fn find_spotify_users( 374 - pool: &Pool<Postgres>, 375 - offset: usize, 376 - limit: usize 482 + pool: &Pool<Postgres>, 483 + offset: usize, 484 + limit: usize, 377 485 ) -> Result<Vec<(String, String, String)>, Error> { 378 - let results: Vec<SpotifyTokenWithEmail> = sqlx::query_as(r#" 486 + let results: Vec<SpotifyTokenWithEmail> = sqlx::query_as( 487 + r#" 379 488 SELECT * FROM spotify_tokens 380 489 LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id 381 490 LEFT JOIN users ON spotify_accounts.user_id = users.xata_id 382 491 LIMIT $1 OFFSET $2 383 - "#) 492 + "#, 493 + ) 384 494 .bind(limit as i64) 385 495 .bind(offset as i64) 386 496 .fetch_all(pool) 387 497 .await?; 388 498 389 - let mut user_tokens = vec![]; 499 + let mut user_tokens = vec![]; 390 500 391 - for result in &results { 392 - let token = decrypt_aes_256_ctr( 393 - &result.refresh_token, 394 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 395 - )?; 396 - user_tokens.push((result.email.clone(), token, result.did.clone())); 397 - } 501 + for result in &results { 502 + let token = decrypt_aes_256_ctr( 503 + &result.refresh_token, 504 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 505 + )?; 506 + user_tokens.push((result.email.clone(), token, result.did.clone())); 507 + } 398 508 399 - Ok(user_tokens) 509 + Ok(user_tokens) 400 510 } 401 511 402 512 pub async fn find_spotify_user( 403 - pool: &Pool<Postgres>, 404 - email: &str 513 + pool: &Pool<Postgres>, 514 + email: &str, 405 515 ) -> Result<Option<(String, String, String)>, Error> { 406 - let result: Vec<SpotifyTokenWithEmail> = sqlx::query_as(r#" 516 + let result: Vec<SpotifyTokenWithEmail> = sqlx::query_as( 517 + r#" 407 518 SELECT * FROM spotify_tokens 408 519 LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id 409 520 LEFT JOIN users ON spotify_accounts.user_id = users.xata_id 410 521 WHERE spotify_accounts.email = $1 411 - "#) 522 + "#, 523 + ) 412 524 .bind(email) 413 525 .fetch_all(pool) 414 526 .await?; 415 527 416 - match result.first() { 417 - Some(result) => { 418 - let token = decrypt_aes_256_ctr( 419 - &result.refresh_token, 420 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 421 - )?; 422 - Ok(Some((result.email.clone(), token, result.did.clone()))) 423 - }, 424 - None => Ok(None) 425 - } 528 + match result.first() { 529 + Some(result) => { 530 + let token = decrypt_aes_256_ctr( 531 + &result.refresh_token, 532 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 533 + )?; 534 + Ok(Some((result.email.clone(), token, result.did.clone()))) 535 + } 536 + None => Ok(None), 537 + } 426 538 } 427 539 428 - pub async fn watch_currently_playing(spotify_email: String, token: String, did: String, stop_flag: Arc<AtomicBool>, cache: Cache) -> Result<(), Error> { 429 - println!("{} {}", format!("[{}]", spotify_email).bright_green(), "Checking currently playing".cyan()); 540 + pub async fn watch_currently_playing( 541 + spotify_email: String, 542 + token: String, 543 + did: String, 544 + stop_flag: Arc<AtomicBool>, 545 + cache: Cache, 546 + ) -> Result<(), Error> { 547 + println!( 548 + "{} {}", 549 + format!("[{}]", spotify_email).bright_green(), 550 + "Checking currently playing".cyan() 551 + ); 430 552 431 - let stop_flag_clone = stop_flag.clone(); 432 - let spotify_email_clone = spotify_email.clone(); 433 - let cache_clone = cache.clone(); 434 - thread::spawn(move || { 435 - loop { 436 - if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) { 437 - println!("{} Stopping Thread", format!("[{}]", spotify_email_clone).bright_green()); 438 - break; 439 - } 440 - if let Some(cached) = cache_clone.get(&format!("{}:current", spotify_email_clone))? { 441 - if serde_json::from_str::<CurrentlyPlaying>(&cached).is_err() { 442 - thread::sleep(std::time::Duration::from_millis(800)); 443 - continue; 444 - } 445 - let mut current_song = serde_json::from_str::<CurrentlyPlaying>(&cached)?; 553 + let stop_flag_clone = stop_flag.clone(); 554 + let spotify_email_clone = spotify_email.clone(); 555 + let cache_clone = cache.clone(); 556 + thread::spawn(move || { 557 + loop { 558 + if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) { 559 + println!( 560 + "{} Stopping Thread", 561 + format!("[{}]", spotify_email_clone).bright_green() 562 + ); 563 + break; 564 + } 565 + if let Some(cached) = cache_clone.get(&format!("{}:current", spotify_email_clone))? { 566 + if serde_json::from_str::<CurrentlyPlaying>(&cached).is_err() { 567 + thread::sleep(std::time::Duration::from_millis(800)); 568 + continue; 569 + } 570 + let mut current_song = serde_json::from_str::<CurrentlyPlaying>(&cached)?; 446 571 447 - if let Some(item) = current_song.item.clone() { 448 - if current_song.is_playing && current_song.progress_ms.unwrap_or(0) < item.duration_ms.into() { 449 - current_song.progress_ms = Some(current_song.progress_ms.unwrap_or(0) + 800); 450 - match cache_clone.setex(&format!("{}:current", spotify_email_clone), &serde_json::to_string(&current_song)?, 16) { 451 - Ok(_) => {}, 452 - Err(e) => { 453 - println!("{} redis error: {}", format!("[{}]", spotify_email_clone).bright_green(), e.to_string().bright_red()); 454 - } 572 + if let Some(item) = current_song.item.clone() { 573 + if current_song.is_playing 574 + && current_song.progress_ms.unwrap_or(0) < item.duration_ms.into() 575 + { 576 + current_song.progress_ms = 577 + Some(current_song.progress_ms.unwrap_or(0) + 800); 578 + match cache_clone.setex( 579 + &format!("{}:current", spotify_email_clone), 580 + &serde_json::to_string(&current_song)?, 581 + 16, 582 + ) { 583 + Ok(_) => {} 584 + Err(e) => { 585 + println!( 586 + "{} redis error: {}", 587 + format!("[{}]", spotify_email_clone).bright_green(), 588 + e.to_string().bright_red() 589 + ); 590 + } 591 + } 592 + thread::sleep(std::time::Duration::from_millis(800)); 593 + continue; 594 + } 595 + } 596 + continue; 597 + } 598 + 599 + if let Ok(Some(cached)) = cache_clone.get(&spotify_email_clone) { 600 + if cached == "No content" { 601 + thread::sleep(std::time::Duration::from_millis(800)); 602 + continue; 603 + } 604 + match cache_clone.setex(&format!("{}:current", spotify_email_clone), &cached, 16) { 605 + Ok(_) => {} 606 + Err(e) => { 607 + println!( 608 + "{} redis error: {}", 609 + format!("[{}]", spotify_email_clone).bright_green(), 610 + e.to_string().bright_red() 611 + ); 612 + } 613 + } 455 614 } 615 + 456 616 thread::sleep(std::time::Duration::from_millis(800)); 457 - continue; 458 - } 459 617 } 460 - continue; 461 - } 618 + Ok::<(), Error>(()) 619 + }); 462 620 463 - if let Ok(Some(cached)) = cache_clone.get(&spotify_email_clone) { 464 - if cached == "No content" { 465 - thread::sleep(std::time::Duration::from_millis(800)); 466 - continue; 621 + loop { 622 + if stop_flag.load(std::sync::atomic::Ordering::Relaxed) { 623 + println!( 624 + "{} Stopping Thread", 625 + format!("[{}]", spotify_email).bright_green() 626 + ); 627 + break; 467 628 } 468 - match cache_clone.setex(&format!("{}:current", spotify_email_clone), &cached, 16) { 469 - Ok(_) => {}, 470 - Err(e) => { 471 - println!("{} redis error: {}", format!("[{}]", spotify_email_clone).bright_green(), e.to_string().bright_red()); 472 - } 473 - } 474 - } 629 + let spotify_email = spotify_email.clone(); 630 + let token = token.clone(); 631 + let did = did.clone(); 632 + let cache = cache.clone(); 475 633 634 + let currently_playing = get_currently_playing(cache.clone(), &spotify_email, &token).await; 635 + let currently_playing = match currently_playing { 636 + Ok(currently_playing) => currently_playing, 637 + Err(e) => { 638 + println!( 639 + "{} {}", 640 + format!("[{}]", spotify_email).bright_green(), 641 + e.to_string().bright_red() 642 + ); 643 + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 644 + continue; 645 + } 646 + }; 476 647 477 - thread::sleep(std::time::Duration::from_millis(800)); 478 - } 479 - Ok::<(), Error>(()) 480 - }); 648 + if let Some((data, changed)) = currently_playing { 649 + if data.item.is_none() { 650 + println!( 651 + "{} {}", 652 + format!("[{}]", spotify_email).bright_green(), 653 + "No song playing".yellow() 654 + ); 655 + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 656 + continue; 657 + } 658 + let data_item = data.item.unwrap(); 659 + println!( 660 + "{} {} is_playing: {} changed: {}", 661 + format!("[{}]", spotify_email).bright_green(), 662 + format!("{} - {}", data_item.name, data_item.artists[0].name).yellow(), 663 + data.is_playing, 664 + changed 665 + ); 481 666 482 - loop { 483 - if stop_flag.load(std::sync::atomic::Ordering::Relaxed) { 484 - println!("{} Stopping Thread", format!("[{}]", spotify_email).bright_green()); 485 - break; 486 - } 487 - let spotify_email = spotify_email.clone(); 488 - let token = token.clone(); 489 - let did = did.clone(); 490 - let cache = cache.clone(); 667 + if changed { 668 + scrobble(cache.clone(), &spotify_email, &did, &token).await?; 491 669 492 - let currently_playing = get_currently_playing( 493 - cache.clone(), 494 - &spotify_email, 495 - &token 496 - ).await; 497 - let currently_playing = match currently_playing { 498 - Ok(currently_playing) => currently_playing, 499 - Err(e) => { 500 - println!("{} {}", format!("[{}]", spotify_email).bright_green(), e.to_string().bright_red()); 501 - tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 502 - continue; 503 - } 504 - }; 670 + thread::spawn(move || { 671 + let rt = tokio::runtime::Runtime::new().unwrap(); 672 + match rt.block_on(async { 673 + get_album_tracks(cache.clone(), &data_item.album.id, &token).await?; 674 + get_album(cache.clone(), &data_item.album.id, &token).await?; 675 + update_library(cache.clone(), &spotify_email, &did, &token).await?; 676 + Ok::<(), Error>(()) 677 + }) { 678 + Ok(_) => {} 679 + Err(e) => { 680 + println!( 681 + "{} {}", 682 + format!("[{}]", spotify_email).bright_green(), 683 + e.to_string().bright_red() 684 + ); 685 + } 686 + } 687 + }); 688 + } 689 + } 505 690 506 - if let Some((data, changed)) = currently_playing { 507 - if data.item.is_none() { 508 - println!("{} {}", format!("[{}]", spotify_email).bright_green(), "No song playing".yellow()); 509 691 tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 510 - continue; 511 - } 512 - let data_item = data.item.unwrap(); 513 - println!("{} {} is_playing: {} changed: {}", format!("[{}]", spotify_email).bright_green(), format!("{} - {}", data_item.name, data_item.artists[0].name).yellow(), data.is_playing, changed); 514 - 515 - if changed { 516 - scrobble( 517 - cache.clone(), 518 - &spotify_email, 519 - &did, 520 - &token 521 - ).await?; 522 - 523 - thread::spawn(move || { 524 - let rt = tokio::runtime::Runtime::new().unwrap(); 525 - match rt.block_on(async { 526 - get_album_tracks(cache.clone(), &data_item.album.id, &token).await?; 527 - get_album(cache.clone(), &data_item.album.id, &token).await?; 528 - update_library(cache.clone(), &spotify_email, &did, &token).await?; 529 - Ok::<(), Error>(()) 530 - }) { 531 - Ok(_) => {}, 532 - Err(e) => { 533 - println!("{} {}", format!("[{}]", spotify_email).bright_green(), e.to_string().bright_red()); 534 - } 535 - } 536 - }); 537 - } 538 692 } 539 693 540 - tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 541 - } 542 - 543 - Ok(()) 694 + Ok(()) 544 695 }
+81 -57
crates/spotify/src/rocksky.rs
··· 1 1 use anyhow::Error; 2 2 use reqwest::Client; 3 3 4 - use crate::{cache::Cache, get_artist, get_currently_playing, token::generate_token, types::{album_tracks::Track, currently_playing::{Album, CurrentlyPlaying}}}; 4 + use crate::{ 5 + cache::Cache, 6 + get_artist, get_currently_playing, 7 + token::generate_token, 8 + types::{ 9 + album_tracks::Track, 10 + currently_playing::{Album, CurrentlyPlaying}, 11 + }, 12 + }; 5 13 6 14 const ROCKSKY_API: &str = "https://api.rocksky.app"; 7 15 8 - pub async fn scrobble(cache: Cache, spotify_email: &str, did: &str, refresh_token: &str) -> Result<(), Error> { 9 - let cached = cache.get(spotify_email)?; 10 - if cached.is_none() { 11 - println!("No currently playing song is cached for {}, skipping", spotify_email); 12 - return Ok(()); 13 - } 16 + pub async fn scrobble( 17 + cache: Cache, 18 + spotify_email: &str, 19 + did: &str, 20 + refresh_token: &str, 21 + ) -> Result<(), Error> { 22 + let cached = cache.get(spotify_email)?; 23 + if cached.is_none() { 24 + println!( 25 + "No currently playing song is cached for {}, skipping", 26 + spotify_email 27 + ); 28 + return Ok(()); 29 + } 14 30 15 - let track = serde_json::from_str::<CurrentlyPlaying>(&cached.unwrap())?; 16 - if track.item.is_none() { 17 - println!("No currently playing song found, skipping"); 18 - return Ok(()); 19 - } 31 + let track = serde_json::from_str::<CurrentlyPlaying>(&cached.unwrap())?; 32 + if track.item.is_none() { 33 + println!("No currently playing song found, skipping"); 34 + return Ok(()); 35 + } 20 36 21 - let track_item = track.item.unwrap(); 37 + let track_item = track.item.unwrap(); 22 38 23 - let artist = get_artist(cache.clone(), &track_item.artists.first().unwrap().id, &refresh_token).await?; 39 + let artist = get_artist( 40 + cache.clone(), 41 + &track_item.artists.first().unwrap().id, 42 + &refresh_token, 43 + ) 44 + .await?; 24 45 25 - let token = generate_token(did)?; 26 - let client = Client::new(); 27 - let response = client 46 + let token = generate_token(did)?; 47 + let client = Client::new(); 48 + let response = client 28 49 .post(&format!("{}/now-playing", ROCKSKY_API)) 29 50 .bearer_auth(token) 30 51 .json(&serde_json::json!({ ··· 58 79 .send() 59 80 .await?; 60 81 61 - 62 - if !response.status().is_success() { 63 - println!("Failed to scrobble: {}", response.text().await?); 64 - } 82 + if !response.status().is_success() { 83 + println!("Failed to scrobble: {}", response.text().await?); 84 + } 65 85 66 - Ok(()) 86 + Ok(()) 67 87 } 68 88 69 - pub async fn update_library(cache: Cache, spotify_email: &str, did: &str, refresh_token: &str) -> Result<(), Error> { 70 - let cached = cache.get(spotify_email)?; 71 - if cached.is_none() { 72 - println!("No currently playing song is cached for {}, refreshing", spotify_email); 73 - get_currently_playing( 74 - cache.clone(), 75 - &spotify_email, 76 - &refresh_token, 77 - ).await?; 78 - } 89 + pub async fn update_library( 90 + cache: Cache, 91 + spotify_email: &str, 92 + did: &str, 93 + refresh_token: &str, 94 + ) -> Result<(), Error> { 95 + let cached = cache.get(spotify_email)?; 96 + if cached.is_none() { 97 + println!( 98 + "No currently playing song is cached for {}, refreshing", 99 + spotify_email 100 + ); 101 + get_currently_playing(cache.clone(), &spotify_email, &refresh_token).await?; 102 + } 79 103 80 - let cached = cache.get(spotify_email)?; 81 - let track = serde_json::from_str::<CurrentlyPlaying>(&cached.unwrap())?; 82 - if track.item.is_none() { 83 - println!("No currently playing song found, skipping"); 84 - return Ok(()); 85 - } 86 - let track_item = track.item.unwrap(); 87 - let cached = cache.get(&format!("{}:tracks", track_item.album.id))?; 88 - if cached.is_none() { 89 - println!("Album not cached {}, skipping", track_item.album.id); 90 - return Ok(()); 91 - } 104 + let cached = cache.get(spotify_email)?; 105 + let track = serde_json::from_str::<CurrentlyPlaying>(&cached.unwrap())?; 106 + if track.item.is_none() { 107 + println!("No currently playing song found, skipping"); 108 + return Ok(()); 109 + } 110 + let track_item = track.item.unwrap(); 111 + let cached = cache.get(&format!("{}:tracks", track_item.album.id))?; 112 + if cached.is_none() { 113 + println!("Album not cached {}, skipping", track_item.album.id); 114 + return Ok(()); 115 + } 92 116 93 - let tracks = serde_json::from_str::<Vec<Track>>(&cached.unwrap())?; 117 + let tracks = serde_json::from_str::<Vec<Track>>(&cached.unwrap())?; 94 118 95 - let cached = cache.get(&track_item.album.id)?; 96 - let album = serde_json::from_str::<Album>(&cached.unwrap())?; 119 + let cached = cache.get(&track_item.album.id)?; 120 + let album = serde_json::from_str::<Album>(&cached.unwrap())?; 97 121 98 - let token = generate_token(did)?; 122 + let token = generate_token(did)?; 99 123 100 - for track in tracks { 101 - let client = Client::new(); 102 - let response = client 124 + for track in tracks { 125 + let client = Client::new(); 126 + let response = client 103 127 .post(&format!("{}/tracks", ROCKSKY_API)) 104 128 .bearer_auth(&token) 105 129 .json(&serde_json::json!({ ··· 130 154 .send() 131 155 .await?; 132 156 133 - // wait 50 seconds to avoid rate limiting 134 - tokio::time::sleep(tokio::time::Duration::from_secs(50)).await; 157 + // wait 50 seconds to avoid rate limiting 158 + tokio::time::sleep(tokio::time::Duration::from_secs(50)).await; 135 159 136 - if !response.status().is_success() { 137 - println!("Failed to save track: {}", response.text().await?); 160 + if !response.status().is_success() { 161 + println!("Failed to save track: {}", response.text().await?); 162 + } 138 163 } 139 - } 140 164 141 - Ok(()) 165 + Ok(()) 142 166 }
+7 -3
crates/storage/src/handlers/files.rs
··· 2 2 use anyhow::Error; 3 3 use s3::Bucket; 4 4 5 - pub async fn presign_get(_payload: &mut web::Payload, _req: &HttpRequest, _bucket: Box<Bucket>) -> Result<HttpResponse, Error> { 6 - todo!() 7 - } 5 + pub async fn presign_get( 6 + _payload: &mut web::Payload, 7 + _req: &HttpRequest, 8 + _bucket: Box<Bucket>, 9 + ) -> Result<HttpResponse, Error> { 10 + todo!() 11 + }
+33 -21
crates/storage/src/handlers/folders.rs
··· 6 6 7 7 use crate::{read_payload, types::params::ListParams}; 8 8 9 - pub async fn list(payload: &mut web::Payload, _req: &HttpRequest, bucket: Box<Bucket>) -> Result<HttpResponse, Error> { 10 - let body = read_payload!(payload); 11 - let params = serde_json::from_slice::<ListParams>(&body)?; 12 - let (res, _) = bucket.list_page(params.prefix, params.delimiter, params.continuation_token, params.start_after, params.max_keys).await?; 9 + pub async fn list( 10 + payload: &mut web::Payload, 11 + _req: &HttpRequest, 12 + bucket: Box<Bucket>, 13 + ) -> Result<HttpResponse, Error> { 14 + let body = read_payload!(payload); 15 + let params = serde_json::from_slice::<ListParams>(&body)?; 16 + let (res, _) = bucket 17 + .list_page( 18 + params.prefix, 19 + params.delimiter, 20 + params.continuation_token, 21 + params.start_after, 22 + params.max_keys, 23 + ) 24 + .await?; 13 25 14 - Ok(HttpResponse::Ok().json(json!({ 15 - "max_keys": res.max_keys, 16 - "prefix": res.prefix, 17 - "continuation_token": res.continuation_token, 18 - "encoding_type": res.encoding_type, 19 - "is_truncated": res.is_truncated, 20 - "next_continuation_token": res.next_continuation_token, 21 - "contents": res.contents.iter().map(|content| { 22 - json!({ 23 - "key": content.key, 24 - "last_modified": content.last_modified, 25 - "etag": content.e_tag, 26 - "size": content.size, 27 - "storage_class": content.storage_class, 28 - }) 29 - }).collect::<Vec<_>>() 30 - }))) 26 + Ok(HttpResponse::Ok().json(json!({ 27 + "max_keys": res.max_keys, 28 + "prefix": res.prefix, 29 + "continuation_token": res.continuation_token, 30 + "encoding_type": res.encoding_type, 31 + "is_truncated": res.is_truncated, 32 + "next_continuation_token": res.next_continuation_token, 33 + "contents": res.contents.iter().map(|content| { 34 + json!({ 35 + "key": content.key, 36 + "last_modified": content.last_modified, 37 + "etag": content.e_tag, 38 + "size": content.size, 39 + "storage_class": content.storage_class, 40 + }) 41 + }).collect::<Vec<_>>() 42 + }))) 31 43 }
+21 -16
crates/storage/src/handlers/mod.rs
··· 9 9 10 10 #[macro_export] 11 11 macro_rules! read_payload { 12 - ($payload:expr) => {{ 13 - let mut body = Vec::new(); 14 - while let Some(chunk) = $payload.next().await { 15 - match chunk { 16 - Ok(bytes) => body.extend_from_slice(&bytes), 17 - Err(err) => return Err(err.into()), 18 - } 19 - } 20 - body 21 - }}; 12 + ($payload:expr) => {{ 13 + let mut body = Vec::new(); 14 + while let Some(chunk) = $payload.next().await { 15 + match chunk { 16 + Ok(bytes) => body.extend_from_slice(&bytes), 17 + Err(err) => return Err(err.into()), 18 + } 19 + } 20 + body 21 + }}; 22 22 } 23 23 24 - pub async fn handle(method: &str, payload: &mut web::Payload, req: &HttpRequest, bucket: Box<Bucket>) -> Result<HttpResponse, Error> { 25 - match method { 26 - "storage.list" => list(payload, req, bucket.clone()).await, 27 - "storage.presignGet" => presign_get(payload, req, bucket.clone()).await, 28 - _ => return Err(anyhow::anyhow!("Method not found")), 29 - } 24 + pub async fn handle( 25 + method: &str, 26 + payload: &mut web::Payload, 27 + req: &HttpRequest, 28 + bucket: Box<Bucket>, 29 + ) -> Result<HttpResponse, Error> { 30 + match method { 31 + "storage.list" => list(payload, req, bucket.clone()).await, 32 + "storage.presignGet" => presign_get(payload, req, bucket.clone()).await, 33 + _ => return Err(anyhow::anyhow!("Method not found")), 34 + } 30 35 }
+53 -50
crates/storage/src/server.rs
··· 1 1 use std::env; 2 2 3 - use actix_web::{get, post, web::{self, Data}, App, HttpRequest, HttpResponse, HttpServer, Responder}; 3 + use actix_web::{ 4 + get, post, 5 + web::{self, Data}, 6 + App, HttpRequest, HttpResponse, HttpServer, Responder, 7 + }; 8 + use anyhow::Error; 4 9 use owo_colors::OwoColorize; 5 10 use s3::{creds::Credentials, Bucket, Region}; 6 11 use serde_json::json; 7 - use anyhow::Error; 8 12 9 13 use crate::handlers::handle; 10 - 11 14 12 15 #[get("/")] 13 16 async fn index(_req: HttpRequest) -> HttpResponse { 14 - HttpResponse::Ok().json(json!({ 15 - "server": "Rocksky Storage Server", 16 - "version": "0.1.0", 17 - })) 17 + HttpResponse::Ok().json(json!({ 18 + "server": "Rocksky Storage Server", 19 + "version": "0.1.0", 20 + })) 18 21 } 19 22 20 23 #[post("/{method}")] 21 24 async fn call_method( 22 - data: web::Data<Box<Bucket>>, 23 - mut payload: web::Payload, 24 - req: HttpRequest) -> Result<impl Responder, actix_web::Error> { 25 - let method = req.match_info().get("method").unwrap_or("unknown"); 26 - println!("Method: {}", method.bright_green()); 25 + data: web::Data<Box<Bucket>>, 26 + mut payload: web::Payload, 27 + req: HttpRequest, 28 + ) -> Result<impl Responder, actix_web::Error> { 29 + let method = req.match_info().get("method").unwrap_or("unknown"); 30 + println!("Method: {}", method.bright_green()); 27 31 28 - let bucket = data.get_ref().clone(); 29 - handle(method, &mut payload, &req, bucket).await 30 - .map_err(actix_web::error::ErrorInternalServerError) 32 + let bucket = data.get_ref().clone(); 33 + handle(method, &mut payload, &req, bucket) 34 + .await 35 + .map_err(actix_web::error::ErrorInternalServerError) 31 36 } 32 - 33 37 34 38 pub async fn serve() -> Result<(), Error> { 39 + let host = env::var("STORAGE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 40 + let port = env::var("STORAGE_PORT").unwrap_or_else(|_| "7883".to_string()); 41 + let addr = format!("{}:{}", host, port); 35 42 36 - let host = env::var("STORAGE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 37 - let port = env::var("STORAGE_PORT").unwrap_or_else(|_| "7883".to_string()); 38 - let addr = format!("{}:{}", host, port); 43 + let url = format!("http://{}", addr); 44 + println!("Listening on {}", url.bright_green()); 39 45 40 - let url = format!("http://{}", addr); 41 - println!("Listening on {}", url.bright_green()); 46 + let access_key = std::env::var("ACCESS_KEY").expect("ACCESS_KEY not set"); 47 + let secret_key = std::env::var("SECRET_KEY").expect("SECRET_KEY not set"); 48 + let bucket_name = std::env::var("BUCKET_NAME").expect("BUCKET_NAME not set"); 49 + let account_id = std::env::var("ACCOUNT_ID").expect("ACCOUNT_ID not set"); 42 50 43 - let access_key = std::env::var("ACCESS_KEY").expect("ACCESS_KEY not set"); 44 - let secret_key = std::env::var("SECRET_KEY").expect("SECRET_KEY not set"); 45 - let bucket_name = std::env::var("BUCKET_NAME").expect("BUCKET_NAME not set"); 46 - let account_id = std::env::var("ACCOUNT_ID").expect("ACCOUNT_ID not set"); 51 + let bucket = Bucket::new( 52 + &bucket_name, 53 + Region::R2 { account_id }, 54 + Credentials::new( 55 + Some(access_key.as_str()), 56 + Some(secret_key.as_str()), 57 + None, 58 + None, 59 + None, 60 + )?, 61 + )? 62 + .with_path_style(); 47 63 48 - let bucket = Bucket::new( 49 - &bucket_name, 50 - Region::R2 { account_id }, 51 - Credentials::new( 52 - Some(access_key.as_str()), 53 - Some(secret_key.as_str()), 54 - None, 55 - None, 56 - None 57 - )?, 58 - )? 59 - .with_path_style(); 64 + HttpServer::new(move || { 65 + App::new() 66 + .app_data(Data::new(bucket.clone())) 67 + .service(index) 68 + .service(call_method) 69 + }) 70 + .bind(&addr)? 71 + .run() 72 + .await 73 + .map_err(Error::new)?; 60 74 61 - HttpServer::new(move || { 62 - App::new() 63 - .app_data(Data::new(bucket.clone())) 64 - .service(index) 65 - .service(call_method) 66 - }) 67 - .bind(&addr)? 68 - .run() 69 - .await 70 - .map_err(Error::new)?; 71 - 72 - Ok(()) 73 - } 75 + Ok(()) 76 + }
+84 -66
crates/webscrobbler/src/handlers.rs
··· 1 - use std::sync::Arc; 1 + use crate::{cache::Cache, repo, scrobbler::scrobble, types::ScrobbleRequest, BANNER}; 2 2 use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder}; 3 3 use owo_colors::OwoColorize; 4 4 use sqlx::{Pool, Postgres}; 5 - use crate::{cache::Cache, repo, scrobbler::scrobble, types::ScrobbleRequest, BANNER}; 5 + use std::sync::Arc; 6 6 use tokio_stream::StreamExt; 7 7 8 8 #[macro_export] 9 9 macro_rules! read_payload { 10 - ($payload:expr) => {{ 11 - let mut body = Vec::new(); 12 - while let Some(chunk) = $payload.next().await { 13 - match chunk { 14 - Ok(bytes) => body.extend_from_slice(&bytes), 15 - Err(err) => return Err(err.into()), 16 - } 17 - } 18 - body 19 - }}; 10 + ($payload:expr) => {{ 11 + let mut body = Vec::new(); 12 + while let Some(chunk) = $payload.next().await { 13 + match chunk { 14 + Ok(bytes) => body.extend_from_slice(&bytes), 15 + Err(err) => return Err(err.into()), 16 + } 17 + } 18 + body 19 + }}; 20 20 } 21 21 22 22 #[get("/")] 23 23 pub async fn index() -> impl Responder { 24 - HttpResponse::Ok().body(BANNER) 24 + HttpResponse::Ok().body(BANNER) 25 25 } 26 26 27 27 #[post("/{id}")] 28 28 async fn handle_scrobble( 29 - data: web::Data<Arc<Pool<Postgres>>>, 30 - cache: web::Data<Cache>, 31 - mut payload: web::Payload, 32 - req: HttpRequest, 29 + data: web::Data<Arc<Pool<Postgres>>>, 30 + cache: web::Data<Cache>, 31 + mut payload: web::Payload, 32 + req: HttpRequest, 33 33 ) -> Result<impl Responder, actix_web::Error> { 34 - let id = req.match_info().get("id").unwrap(); 35 - println!("Received scrobble for ID: {}", id.cyan()); 34 + let id = req.match_info().get("id").unwrap(); 35 + println!("Received scrobble for ID: {}", id.cyan()); 36 36 37 - let pool = data.get_ref().clone(); 37 + let pool = data.get_ref().clone(); 38 38 39 - let user = repo::user::get_user_by_webscrobbler(&pool, id).await 40 - .map_err(|err| actix_web::error::ErrorInternalServerError(format!("Database error: {}", err)))?; 39 + let user = repo::user::get_user_by_webscrobbler(&pool, id) 40 + .await 41 + .map_err(|err| { 42 + actix_web::error::ErrorInternalServerError(format!("Database error: {}", err)) 43 + })?; 41 44 42 - if user.is_none() { 43 - return Ok(HttpResponse::NotFound().body("There is no user with this webscrobbler ID")); 44 - } 45 - let user = user.unwrap(); 45 + if user.is_none() { 46 + return Ok(HttpResponse::NotFound().body("There is no user with this webscrobbler ID")); 47 + } 48 + let user = user.unwrap(); 46 49 47 - let body = read_payload!(payload); 48 - let params = serde_json::from_slice::<ScrobbleRequest>(&body) 49 - .map_err(|err| { 50 - let body = String::from_utf8_lossy(&body); 51 - println!("Failed to parse JSON: {}", body); 52 - println!("Failed to parse JSON: {}", err); 53 - actix_web::error::ErrorBadRequest(format!("Failed to parse JSON: {}", err))})?; 50 + let body = read_payload!(payload); 51 + let params = serde_json::from_slice::<ScrobbleRequest>(&body).map_err(|err| { 52 + let body = String::from_utf8_lossy(&body); 53 + println!("Failed to parse JSON: {}", body); 54 + println!("Failed to parse JSON: {}", err); 55 + actix_web::error::ErrorBadRequest(format!("Failed to parse JSON: {}", err)) 56 + })?; 54 57 55 - println!("Parsed scrobble request: {:#?}", params); 58 + println!("Parsed scrobble request: {:#?}", params); 56 59 57 - if params.event_name != "scrobble" { 58 - println!("Skipping non-scrobble event: {}", params.event_name.green()); 59 - return Ok(HttpResponse::Ok().body("Skipping non-scrobble event")); 60 - } 60 + if params.event_name != "scrobble" { 61 + println!("Skipping non-scrobble event: {}", params.event_name.green()); 62 + return Ok(HttpResponse::Ok().body("Skipping non-scrobble event")); 63 + } 61 64 62 - // Check if connector is Spotify 63 - if params.data.song.connector.id == "spotify" { 64 - // Skip if the user has a Spotify token 65 - let spotify_token = repo::spotify_token::get_spotify_token(&pool, &user.did).await 66 - .map_err(|err| actix_web::error::ErrorInternalServerError(format!("Failed to get Spotify tokens: {}", err)))?; 65 + // Check if connector is Spotify 66 + if params.data.song.connector.id == "spotify" { 67 + // Skip if the user has a Spotify token 68 + let spotify_token = repo::spotify_token::get_spotify_token(&pool, &user.did) 69 + .await 70 + .map_err(|err| { 71 + actix_web::error::ErrorInternalServerError(format!( 72 + "Failed to get Spotify tokens: {}", 73 + err 74 + )) 75 + })?; 67 76 68 - if spotify_token.is_some() { 69 - println!("User has a Spotify token, skipping scrobble"); 70 - return Ok(HttpResponse::Ok().body("User has a Spotify token, skipping scrobble")); 77 + if spotify_token.is_some() { 78 + println!("User has a Spotify token, skipping scrobble"); 79 + return Ok(HttpResponse::Ok().body("User has a Spotify token, skipping scrobble")); 80 + } 71 81 } 72 - } 73 82 83 + let cache = cache.get_ref().clone(); 74 84 75 - let cache = cache.get_ref().clone(); 85 + if params.data.song.connector.id == "emby" { 86 + let artist = params.data.song.parsed.artist.clone(); 87 + let track = params.data.song.parsed.track.clone(); 88 + let cached = cache.get(&format!( 89 + "listenbrainz:emby:{}:{}:{}", 90 + artist, track, user.did 91 + )); 76 92 77 - if params.data.song.connector.id == "emby" { 78 - let artist = params.data.song.parsed.artist.clone(); 79 - let track = params.data.song.parsed.track.clone(); 80 - let cached = cache.get(&format!("listenbrainz:emby:{}:{}:{}", artist, track, user.did)); 93 + if cached.is_err() { 94 + println!( 95 + "Failed to check cache for Emby scrobble: {}", 96 + cached.unwrap_err() 97 + ); 98 + return Ok(HttpResponse::Ok().body("Failed to check cache for Emby scrobble")); 99 + } 81 100 82 - if cached.is_err() { 83 - println!("Failed to check cache for Emby scrobble: {}", cached.unwrap_err()); 84 - return Ok(HttpResponse::Ok().body("Failed to check cache for Emby scrobble")); 101 + if cached.unwrap().is_some() { 102 + println!( 103 + "Skipping duplicate scrobble for Emby: {} - {}", 104 + artist, track 105 + ); 106 + return Ok(HttpResponse::Ok().body("Skipping duplicate scrobble for Emby")); 107 + } 85 108 } 86 109 87 - if cached.unwrap().is_some() { 88 - println!("Skipping duplicate scrobble for Emby: {} - {}", artist, track); 89 - return Ok(HttpResponse::Ok().body("Skipping duplicate scrobble for Emby")); 90 - } 91 - } 92 - 93 - 94 - scrobble(&pool, &cache, params, &user.did).await 95 - .map_err(|err| actix_web::error::ErrorInternalServerError(format!("Failed to scrobble: {}", err)))?; 96 - 110 + scrobble(&pool, &cache, params, &user.did) 111 + .await 112 + .map_err(|err| { 113 + actix_web::error::ErrorInternalServerError(format!("Failed to scrobble: {}", err)) 114 + })?; 97 115 98 - Ok(HttpResponse::Ok().body("Scrobble received")) 116 + Ok(HttpResponse::Ok().body("Scrobble received")) 99 117 }
+14 -10
crates/webscrobbler/src/main.rs
··· 1 1 use std::{env, sync::Arc, time::Duration}; 2 2 3 + use actix_limitation::{Limiter, RateLimiter}; 3 4 use actix_session::SessionExt as _; 4 - use actix_limitation::{Limiter, RateLimiter}; 5 - use actix_web::{dev::ServiceRequest, web::{self, Data}, App, HttpServer}; 5 + use actix_web::{ 6 + dev::ServiceRequest, 7 + web::{self, Data}, 8 + App, HttpServer, 9 + }; 6 10 use anyhow::Error; 7 11 use cache::Cache; 8 12 use dotenv::dotenv; 9 13 use owo_colors::OwoColorize; 10 14 use sqlx::postgres::PgPoolOptions; 11 15 12 - pub mod rocksky; 16 + pub mod auth; 13 17 pub mod cache; 18 + pub mod crypto; 14 19 pub mod handlers; 15 - pub mod xata; 16 - pub mod types; 20 + pub mod musicbrainz; 17 21 pub mod repo; 18 - pub mod auth; 22 + pub mod rocksky; 23 + pub mod scrobbler; 19 24 pub mod spotify; 20 - pub mod musicbrainz; 21 - pub mod scrobbler; 22 - pub mod crypto; 25 + pub mod types; 26 + pub mod xata; 23 27 24 28 pub const BANNER: &str = r#" 25 29 _ __ __ _____ __ __ __ ··· 58 62 format!("{}:{}", host, port).green() 59 63 ); 60 64 61 - let limiter = web::Data::new( 65 + let limiter = web::Data::new( 62 66 Limiter::builder("redis://127.0.0.1") 63 67 .key_by(|req: &ServiceRequest| { 64 68 req.get_session()
+4 -19
crates/webscrobbler/src/musicbrainz/client.rs
··· 11 11 MusicbrainzClient {} 12 12 } 13 13 14 - pub async fn search( 15 - &self, 16 - query: &str, 17 - ) -> Result<Recordings, Error> { 14 + pub async fn search(&self, query: &str) -> Result<Recordings, Error> { 18 15 let url = format!("{}/recording", BASE_URL); 19 16 let client = reqwest::Client::new(); 20 17 let response = client 21 18 .get(&url) 22 19 .header("Accept", "application/json") 23 20 .header("User-Agent", USER_AGENT) 24 - .query( 25 - &[ 26 - ("query", query), 27 - ("inc", "artist-credits+releases"), 28 - ], 29 - ) 21 + .query(&[("query", query), ("inc", "artist-credits+releases")]) 30 22 .send() 31 23 .await?; 32 24 33 25 Ok(response.json().await?) 34 26 } 35 27 36 - pub async fn get_recording( 37 - &self, 38 - mbid: &str, 39 - ) -> Result<Recording, Error> { 28 + pub async fn get_recording(&self, mbid: &str) -> Result<Recording, Error> { 40 29 let url = format!("{}/recording/{}", BASE_URL, mbid); 41 30 let client = reqwest::Client::new(); 42 31 let response = client 43 32 .get(&url) 44 33 .header("Accept", "application/json") 45 34 .header("User-Agent", USER_AGENT) 46 - .query( 47 - &[ 48 - ("inc", "artist-credits+releases"), 49 - ], 50 - ) 35 + .query(&[("inc", "artist-credits+releases")]) 51 36 .send() 52 37 .await?; 53 38
+6 -4
crates/webscrobbler/src/repo/album.rs
··· 4 4 use crate::xata::album::Album; 5 5 6 6 pub async fn get_album_by_track_id(pool: &Pool<Postgres>, track_id: &str) -> Result<Album, Error> { 7 - let results: Vec<Album> = sqlx::query_as(r#" 7 + let results: Vec<Album> = sqlx::query_as( 8 + r#" 8 9 SELECT * FROM albums 9 10 LEFT JOIN album_tracks ON albums.xata_id = album_tracks.album_id 10 11 WHERE album_tracks.track_id = $1 11 - "#) 12 + "#, 13 + ) 12 14 .bind(track_id) 13 15 .fetch_all(pool) 14 16 .await?; 15 17 16 - Ok(results[0].clone()) 17 - } 18 + Ok(results[0].clone()) 19 + }
+10 -5
crates/webscrobbler/src/repo/artist.rs
··· 3 3 4 4 use crate::xata::artist::Artist; 5 5 6 - pub async fn get_artist_by_track_id(pool: &Pool<Postgres>, track_id: &str) -> Result<Artist, Error> { 7 - let results: Vec<Artist> = sqlx::query_as(r#" 6 + pub async fn get_artist_by_track_id( 7 + pool: &Pool<Postgres>, 8 + track_id: &str, 9 + ) -> Result<Artist, Error> { 10 + let results: Vec<Artist> = sqlx::query_as( 11 + r#" 8 12 SELECT * FROM artists 9 13 LEFT JOIN artist_tracks ON artists.xata_id = artist_tracks.artist_id 10 14 WHERE artist_tracks.track_id = $1 11 - "#) 15 + "#, 16 + ) 12 17 .bind(track_id) 13 18 .fetch_all(pool) 14 19 .await?; 15 20 16 - Ok(results[0].clone()) 17 - } 21 + Ok(results[0].clone()) 22 + }
+10 -5
crates/webscrobbler/src/repo/spotify_account.rs
··· 1 - use sqlx::{Pool, Postgres}; 2 - use anyhow::Error; 3 1 use crate::xata::spotify_account::SpotifyAccount; 2 + use anyhow::Error; 3 + use sqlx::{Pool, Postgres}; 4 4 5 - pub async fn get_spotify_account(pool: &Pool<Postgres>, user_id: &str) -> Result<Option<SpotifyAccount>, Error> { 6 - let results: Vec<SpotifyAccount> = sqlx::query_as(r#" 5 + pub async fn get_spotify_account( 6 + pool: &Pool<Postgres>, 7 + user_id: &str, 8 + ) -> Result<Option<SpotifyAccount>, Error> { 9 + let results: Vec<SpotifyAccount> = sqlx::query_as( 10 + r#" 7 11 SELECT * FROM spotify_accounts 8 12 WHERE user_id = $1 9 - "#) 13 + "#, 14 + ) 10 15 .bind(user_id) 11 16 .fetch_all(pool) 12 17 .await?;
+28 -19
crates/webscrobbler/src/repo/spotify_token.rs
··· 3 3 4 4 use crate::xata::spotify_token::SpotifyToken; 5 5 6 - 7 - pub async fn get_spotify_token(pool: &Pool<Postgres>, did: &str) -> Result<Option<SpotifyToken>, Error> { 8 - let results: Vec<SpotifyToken> = sqlx::query_as(r#" 6 + pub async fn get_spotify_token( 7 + pool: &Pool<Postgres>, 8 + did: &str, 9 + ) -> Result<Option<SpotifyToken>, Error> { 10 + let results: Vec<SpotifyToken> = sqlx::query_as( 11 + r#" 9 12 SELECT * FROM spotify_tokens 10 13 LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id 11 14 LEFT JOIN users ON spotify_accounts.user_id = users.xata_id 12 15 WHERE users.did = $1 13 - "#) 14 - .bind(did) 15 - .fetch_all(pool) 16 - .await?; 16 + "#, 17 + ) 18 + .bind(did) 19 + .fetch_all(pool) 20 + .await?; 17 21 18 - if results.len() == 0 { 19 - return Ok(None); 20 - } 22 + if results.len() == 0 { 23 + return Ok(None); 24 + } 21 25 22 - Ok(Some(results[0].clone())) 26 + Ok(Some(results[0].clone())) 23 27 } 24 28 25 - pub async fn get_spotify_tokens(pool: &Pool<Postgres>, limit: u32) -> Result<Vec<SpotifyToken>, Error> { 26 - let results: Vec<SpotifyToken> = sqlx::query_as(r#" 29 + pub async fn get_spotify_tokens( 30 + pool: &Pool<Postgres>, 31 + limit: u32, 32 + ) -> Result<Vec<SpotifyToken>, Error> { 33 + let results: Vec<SpotifyToken> = sqlx::query_as( 34 + r#" 27 35 SELECT * FROM spotify_tokens 28 36 LEFT JOIN spotify_accounts ON spotify_tokens.user_id = spotify_accounts.user_id 29 37 LEFT JOIN users ON spotify_accounts.user_id = users.xata_id 30 38 LIMIT $1 31 - "#) 32 - .bind(limit as i32) 33 - .fetch_all(pool) 34 - .await?; 39 + "#, 40 + ) 41 + .bind(limit as i32) 42 + .fetch_all(pool) 43 + .await?; 35 44 36 - Ok(results) 37 - } 45 + Ok(results) 46 + }
+22 -14
crates/webscrobbler/src/repo/track.rs
··· 3 3 4 4 use crate::xata::track::Track; 5 5 6 - pub async fn get_track(pool: &Pool<Postgres>, title: &str, artist: &str) -> Result<Option<Track>, Error> { 7 - let results: Vec<Track> = sqlx::query_as(r#" 6 + pub async fn get_track( 7 + pool: &Pool<Postgres>, 8 + title: &str, 9 + artist: &str, 10 + ) -> Result<Option<Track>, Error> { 11 + let results: Vec<Track> = sqlx::query_as( 12 + r#" 8 13 SELECT * FROM tracks 9 14 WHERE LOWER(title) = LOWER($1) 10 15 AND (LOWER(artist) = LOWER($2) OR LOWER(album_artist) = LOWER($2)) 11 - "#) 16 + "#, 17 + ) 12 18 .bind(title) 13 19 .bind(artist) 14 20 .fetch_all(pool) 15 21 .await?; 16 22 17 - if results.len() == 0 { 18 - return Ok(None); 19 - } 23 + if results.len() == 0 { 24 + return Ok(None); 25 + } 20 26 21 - Ok(Some(results[0].clone())) 27 + Ok(Some(results[0].clone())) 22 28 } 23 29 24 30 pub async fn get_track_by_mbid(pool: &Pool<Postgres>, mbid: &str) -> Result<Option<Track>, Error> { 25 - let results: Vec<Track> = sqlx::query_as(r#" 31 + let results: Vec<Track> = sqlx::query_as( 32 + r#" 26 33 SELECT * FROM tracks WHERE mb_id = $1 27 - "#) 34 + "#, 35 + ) 28 36 .bind(mbid) 29 37 .fetch_all(pool) 30 38 .await?; 31 39 32 - if results.len() == 0 { 33 - return Ok(None); 34 - } 40 + if results.len() == 0 { 41 + return Ok(None); 42 + } 35 43 36 - Ok(Some(results[0].clone())) 37 - } 44 + Ok(Some(results[0].clone())) 45 + }
+15 -11
crates/webscrobbler/src/repo/user.rs
··· 3 3 4 4 use crate::xata::user::User; 5 5 6 - 7 - pub async fn get_user_by_webscrobbler(pool: &Pool<Postgres>, uuid: &str) -> Result<Option<User>, Error> { 8 - let results: Vec<User> = sqlx::query_as(r#" 6 + pub async fn get_user_by_webscrobbler( 7 + pool: &Pool<Postgres>, 8 + uuid: &str, 9 + ) -> Result<Option<User>, Error> { 10 + let results: Vec<User> = sqlx::query_as( 11 + r#" 9 12 SELECT * FROM users 10 13 LEFT JOIN webscrobblers ON users.xata_id = webscrobblers.user_id 11 14 WHERE webscrobblers.uuid = $1 12 - "#) 13 - .bind(uuid) 14 - .fetch_all(pool) 15 - .await?; 15 + "#, 16 + ) 17 + .bind(uuid) 18 + .fetch_all(pool) 19 + .await?; 16 20 17 - if results.len() == 0 { 18 - return Ok(None); 19 - } 21 + if results.len() == 0 { 22 + return Ok(None); 23 + } 20 24 21 - Ok(Some(results[0].clone())) 25 + Ok(Some(results[0].clone())) 22 26 }
+16 -11
crates/webscrobbler/src/repo/webscrobbler.rs
··· 1 + use crate::xata::webscrobbler::Webscrobbler; 1 2 use anyhow::Error; 2 3 use sqlx::{Pool, Postgres}; 3 - use crate::xata::webscrobbler::Webscrobbler; 4 4 5 - pub async fn get_webscrobbler(pool: &Pool<Postgres>, uuid: &str) -> Result<Option<Webscrobbler>, Error> { 6 - let results: Vec<Webscrobbler> = sqlx::query_as(r#" 5 + pub async fn get_webscrobbler( 6 + pool: &Pool<Postgres>, 7 + uuid: &str, 8 + ) -> Result<Option<Webscrobbler>, Error> { 9 + let results: Vec<Webscrobbler> = sqlx::query_as( 10 + r#" 7 11 SELECT * FROM webscrobblers 8 12 WHERE uuid = $1 9 - "#) 10 - .bind(uuid) 11 - .fetch_all(pool) 12 - .await?; 13 + "#, 14 + ) 15 + .bind(uuid) 16 + .fetch_all(pool) 17 + .await?; 13 18 14 - if results.len() == 0 { 15 - return Ok(None); 16 - } 19 + if results.len() == 0 { 20 + return Ok(None); 21 + } 17 22 18 - Ok(Some(results[0].clone())) 23 + Ok(Some(results[0].clone())) 19 24 }
+33 -26
crates/webscrobbler/src/rocksky.rs
··· 7 7 const ROCKSKY_API: &str = "https://api.rocksky.app"; 8 8 9 9 pub async fn scrobble(cache: &Cache, did: &str, track: Track, timestamp: u64) -> Result<(), Error> { 10 - let key = format!("{} - {}", track.artist.to_lowercase(), track.title.to_lowercase()); 10 + let key = format!( 11 + "{} - {}", 12 + track.artist.to_lowercase(), 13 + track.title.to_lowercase() 14 + ); 11 15 12 - // Check if the track is already in the cache, if not add it 13 - if !cache.exists(&key)? { 14 - let value = serde_json::to_string(&track)?; 15 - let ttl = 15 * 60; // 15 minutes 16 - cache.setex(&key, &value, ttl)?; 17 - } 16 + // Check if the track is already in the cache, if not add it 17 + if !cache.exists(&key)? { 18 + let value = serde_json::to_string(&track)?; 19 + let ttl = 15 * 60; // 15 minutes 20 + cache.setex(&key, &value, ttl)?; 21 + } 18 22 19 - let mut track = track; 20 - track.timestamp = Some(timestamp / 1000 as u64); 23 + let mut track = track; 24 + track.timestamp = Some(timestamp / 1000 as u64); 21 25 22 - let token = generate_token(did)?; 23 - let client = Client::new(); 26 + let token = generate_token(did)?; 27 + let client = Client::new(); 24 28 25 - println!("Scrobbling track: \n {:#?}", track); 29 + println!("Scrobbling track: \n {:#?}", track); 26 30 27 - let response= client 28 - .post(&format!("{}/now-playing", ROCKSKY_API)) 29 - .bearer_auth(token) 30 - .json(&track) 31 - .send() 32 - .await?; 31 + let response = client 32 + .post(&format!("{}/now-playing", ROCKSKY_API)) 33 + .bearer_auth(token) 34 + .json(&track) 35 + .send() 36 + .await?; 33 37 34 - if !response.status().is_success() { 35 - println!("Failed to scrobble track: {}", response.status().to_string()); 36 - let text = response.text().await?; 37 - println!("Response: {}", text); 38 - return Err(Error::msg(format!("Failed to scrobble track: {}", text))); 39 - } 38 + if !response.status().is_success() { 39 + println!( 40 + "Failed to scrobble track: {}", 41 + response.status().to_string() 42 + ); 43 + let text = response.text().await?; 44 + println!("Response: {}", text); 45 + return Err(Error::msg(format!("Failed to scrobble track: {}", text))); 46 + } 40 47 41 - println!("Scrobbled track: {}", track.title.green()); 48 + println!("Scrobbled track: {}", track.title.green()); 42 49 43 - Ok(()) 50 + Ok(()) 44 51 }
+136 -102
crates/webscrobbler/src/scrobbler.rs
··· 1 1 use std::env; 2 2 3 - use owo_colors::OwoColorize; 4 - use rand::Rng; 5 - use sqlx::{Pool, Postgres}; 6 - use anyhow::Error; 7 3 use crate::cache::Cache; 8 4 use crate::crypto::decrypt_aes_256_ctr; 9 5 use crate::musicbrainz::client::MusicbrainzClient; 10 6 use crate::spotify::client::SpotifyClient; 11 7 use crate::spotify::refresh_token; 8 + use crate::types::{ScrobbleRequest, Track}; 12 9 use crate::{repo, rocksky}; 13 - use crate::types::{ScrobbleRequest, Track}; 10 + use anyhow::Error; 11 + use owo_colors::OwoColorize; 12 + use rand::Rng; 13 + use sqlx::{Pool, Postgres}; 14 14 15 - pub async fn scrobble(pool: &Pool<Postgres>, cache: &Cache, scrobble: ScrobbleRequest, did: &str) -> Result<(), Error> { 16 - let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?; 15 + pub async fn scrobble( 16 + pool: &Pool<Postgres>, 17 + cache: &Cache, 18 + scrobble: ScrobbleRequest, 19 + did: &str, 20 + ) -> Result<(), Error> { 21 + let spofity_tokens = repo::spotify_token::get_spotify_tokens(pool, 100).await?; 17 22 18 - if spofity_tokens.is_empty() { 19 - return Err(Error::msg("No Spotify tokens found")); 20 - } 23 + if spofity_tokens.is_empty() { 24 + return Err(Error::msg("No Spotify tokens found")); 25 + } 21 26 22 - let mb_client = MusicbrainzClient::new(); 27 + let mb_client = MusicbrainzClient::new(); 23 28 24 - let key = format!("{} - {}", scrobble.data.song.parsed.artist.to_lowercase(), scrobble.data.song.parsed.track.to_lowercase()); 29 + let key = format!( 30 + "{} - {}", 31 + scrobble.data.song.parsed.artist.to_lowercase(), 32 + scrobble.data.song.parsed.track.to_lowercase() 33 + ); 25 34 26 - let cached = cache.get(&key)?; 27 - if cached.is_some() { 28 - println!("{}", format!("Cached: {}", key).yellow()); 29 - let track = serde_json::from_str::<Track>(&cached.unwrap())?; 30 - rocksky::scrobble(cache, &did, track, scrobble.time).await?; 31 - tokio::time::sleep(std::time::Duration::from_secs(1)).await; 32 - return Ok(()); 33 - } 35 + let cached = cache.get(&key)?; 36 + if cached.is_some() { 37 + println!("{}", format!("Cached: {}", key).yellow()); 38 + let track = serde_json::from_str::<Track>(&cached.unwrap())?; 39 + rocksky::scrobble(cache, &did, track, scrobble.time).await?; 40 + tokio::time::sleep(std::time::Duration::from_secs(1)).await; 41 + return Ok(()); 42 + } 34 43 35 - let result = repo::track::get_track(pool, &scrobble.data.song.parsed.track, &scrobble.data.song.parsed.artist).await?; 44 + let result = repo::track::get_track( 45 + pool, 46 + &scrobble.data.song.parsed.track, 47 + &scrobble.data.song.parsed.artist, 48 + ) 49 + .await?; 36 50 37 - if let Some(track) = result { 38 - println!("{}", "Xata (track)".yellow()); 39 - let album = repo::album::get_album_by_track_id(pool, &track.xata_id).await?; 40 - let artist = repo::artist::get_artist_by_track_id(pool, &track.xata_id).await?; 41 - let mut track: Track = track.into(); 42 - track.year = match album.year { 43 - Some(year) => Some(year as u32), 44 - None => match album.release_date.clone() { 45 - Some(release_date) => { 46 - let year = release_date.split("-").next(); 47 - year.and_then(|x| x.parse::<u32>().ok()) 48 - } 49 - None => None, 50 - }, 51 - }; 52 - track.release_date = album.release_date.map(|x| x.split("T").next().unwrap().to_string()); 53 - track.artist_picture = artist.picture.clone(); 51 + if let Some(track) = result { 52 + println!("{}", "Xata (track)".yellow()); 53 + let album = repo::album::get_album_by_track_id(pool, &track.xata_id).await?; 54 + let artist = repo::artist::get_artist_by_track_id(pool, &track.xata_id).await?; 55 + let mut track: Track = track.into(); 56 + track.year = match album.year { 57 + Some(year) => Some(year as u32), 58 + None => match album.release_date.clone() { 59 + Some(release_date) => { 60 + let year = release_date.split("-").next(); 61 + year.and_then(|x| x.parse::<u32>().ok()) 62 + } 63 + None => None, 64 + }, 65 + }; 66 + track.release_date = album 67 + .release_date 68 + .map(|x| x.split("T").next().unwrap().to_string()); 69 + track.artist_picture = artist.picture.clone(); 54 70 55 - rocksky::scrobble(cache, &did, track, scrobble.time).await?; 56 - tokio::time::sleep(std::time::Duration::from_secs(1)).await; 57 - return Ok(()); 58 - } 71 + rocksky::scrobble(cache, &did, track, scrobble.time).await?; 72 + tokio::time::sleep(std::time::Duration::from_secs(1)).await; 73 + return Ok(()); 74 + } 59 75 60 - // we need to pick a random token to avoid Spotify rate limiting 61 - // and to avoid using the same token for all scrobbles 62 - // this is a simple way to do it, but we can improve it later 63 - // by using a more sophisticated algorithm 64 - // or by using a token pool 65 - let mut rng = rand::rng(); 66 - let random_index = rng.random_range(0..spofity_tokens.len()); 67 - let spotify_token = &spofity_tokens[random_index]; 76 + // we need to pick a random token to avoid Spotify rate limiting 77 + // and to avoid using the same token for all scrobbles 78 + // this is a simple way to do it, but we can improve it later 79 + // by using a more sophisticated algorithm 80 + // or by using a token pool 81 + let mut rng = rand::rng(); 82 + let random_index = rng.random_range(0..spofity_tokens.len()); 83 + let spotify_token = &spofity_tokens[random_index]; 68 84 69 - let spotify_token = decrypt_aes_256_ctr( 70 - &spotify_token.refresh_token, 71 - &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)? 72 - )?; 85 + let spotify_token = decrypt_aes_256_ctr( 86 + &spotify_token.refresh_token, 87 + &hex::decode(env::var("SPOTIFY_ENCRYPTION_KEY")?)?, 88 + )?; 73 89 74 - let spotify_token = refresh_token(&spotify_token).await?; 75 - let spotify_client = SpotifyClient::new(&spotify_token.access_token); 90 + let spotify_token = refresh_token(&spotify_token).await?; 91 + let spotify_client = SpotifyClient::new(&spotify_token.access_token); 76 92 77 - let query = match scrobble.data.song.parsed.artist.contains(" x ") { 93 + let query = match scrobble.data.song.parsed.artist.contains(" x ") { 94 + true => { 95 + let artists = scrobble 96 + .data 97 + .song 98 + .parsed 99 + .artist 100 + .split(" x ") 101 + .map(|a| format!(r#"artist:"{}""#, a.trim())) 102 + .collect::<Vec<_>>() 103 + .join(" "); 104 + format!(r#"track:"{}" {}"#, scrobble.data.song.parsed.track, artists) 105 + } 106 + false => match scrobble.data.song.parsed.artist.contains(", ") { 78 107 true => { 79 - let artists = scrobble.data.song.parsed.artist 80 - .split(" x ") 108 + let artists = scrobble 109 + .data 110 + .song 111 + .parsed 112 + .artist 113 + .split(", ") 81 114 .map(|a| format!(r#"artist:"{}""#, a.trim())) 82 115 .collect::<Vec<_>>() 83 116 .join(" "); 84 117 format!(r#"track:"{}" {}"#, scrobble.data.song.parsed.track, artists) 85 118 } 86 - false => { 87 - match scrobble.data.song.parsed.artist.contains(", ") { 88 - true => { 89 - let artists = scrobble.data.song.parsed.artist 90 - .split(", ") 91 - .map(|a| format!(r#"artist:"{}""#, a.trim())) 92 - .collect::<Vec<_>>() 93 - .join(" "); 94 - format!(r#"track:"{}" {}"#, scrobble.data.song.parsed.track, artists) 95 - } 96 - false => format!(r#"track:"{}" artist:"{}""#, scrobble.data.song.parsed.track, scrobble.data.song.parsed.artist.trim()), 97 - } 98 - }, 99 - }; 119 + false => format!( 120 + r#"track:"{}" artist:"{}""#, 121 + scrobble.data.song.parsed.track, 122 + scrobble.data.song.parsed.artist.trim() 123 + ), 124 + }, 125 + }; 100 126 101 - let result = spotify_client.search(&query).await?; 127 + let result = spotify_client.search(&query).await?; 102 128 103 - if let Some(track) = result.tracks.items.first() { 104 - println!("{}", "Spotify (track)".yellow()); 105 - let mut track = track.clone(); 129 + if let Some(track) = result.tracks.items.first() { 130 + println!("{}", "Spotify (track)".yellow()); 131 + let mut track = track.clone(); 106 132 107 - if let Some(album) = spotify_client.get_album(&track.album.id).await? { 108 - track.album = album; 109 - } 133 + if let Some(album) = spotify_client.get_album(&track.album.id).await? { 134 + track.album = album; 135 + } 110 136 111 - if let Some(artist) = spotify_client.get_artist(&track.album.artists[0].id).await? { 112 - track.album.artists[0] = artist; 113 - } 114 - 115 - rocksky::scrobble(cache, &did, track.into(), scrobble.time).await?; 116 - tokio::time::sleep(std::time::Duration::from_secs(1)).await; 117 - return Ok(()); 137 + if let Some(artist) = spotify_client 138 + .get_artist(&track.album.artists[0].id) 139 + .await? 140 + { 141 + track.album.artists[0] = artist; 118 142 } 119 143 120 - let query = format!( 121 - r#"recording:"{}" AND artist:"{}""#, 122 - scrobble.data.song.parsed.track, scrobble.data.song.parsed.artist 123 - ); 124 - let result = mb_client.search(&query).await?; 144 + rocksky::scrobble(cache, &did, track.into(), scrobble.time).await?; 145 + tokio::time::sleep(std::time::Duration::from_secs(1)).await; 146 + return Ok(()); 147 + } 148 + 149 + let query = format!( 150 + r#"recording:"{}" AND artist:"{}""#, 151 + scrobble.data.song.parsed.track, scrobble.data.song.parsed.artist 152 + ); 153 + let result = mb_client.search(&query).await?; 125 154 126 - if let Some(recording) = result.recordings.first() { 127 - let result = mb_client.get_recording(&recording.id).await?; 128 - println!("{}", "Musicbrainz (recording)".yellow()); 129 - rocksky::scrobble(cache, &did, result.into(), scrobble.time).await?; 130 - tokio::time::sleep(std::time::Duration::from_secs(1)).await; 131 - return Ok(()); 132 - } 155 + if let Some(recording) = result.recordings.first() { 156 + let result = mb_client.get_recording(&recording.id).await?; 157 + println!("{}", "Musicbrainz (recording)".yellow()); 158 + rocksky::scrobble(cache, &did, result.into(), scrobble.time).await?; 159 + tokio::time::sleep(std::time::Duration::from_secs(1)).await; 160 + return Ok(()); 161 + } 133 162 134 - println!("{} {} - {}, skipping", "Track not found: ".yellow(), scrobble.data.song.parsed.artist, scrobble.data.song.parsed.track); 163 + println!( 164 + "{} {} - {}, skipping", 165 + "Track not found: ".yellow(), 166 + scrobble.data.song.parsed.artist, 167 + scrobble.data.song.parsed.track 168 + ); 135 169 136 - Ok(()) 137 - } 170 + Ok(()) 171 + }
+38 -37
crates/webscrobbler/src/spotify/client.rs
··· 4 4 pub const BASE_URL: &str = "https://api.spotify.com/v1"; 5 5 6 6 pub struct SpotifyClient { 7 - token: String, 7 + token: String, 8 8 } 9 9 10 10 impl SpotifyClient { ··· 17 17 pub async fn search(&self, query: &str) -> Result<SearchResponse, Error> { 18 18 let url = format!("{}/search", BASE_URL); 19 19 let client = reqwest::Client::new(); 20 - let response = client.get(&url) 21 - .bearer_auth(&self.token) 22 - .query(&[ 23 - ("type", "track"), 24 - ("q", query), 25 - ]) 26 - .send().await?; 20 + let response = client 21 + .get(&url) 22 + .bearer_auth(&self.token) 23 + .query(&[("type", "track"), ("q", query)]) 24 + .send() 25 + .await?; 27 26 let result = response.json().await?; 28 27 Ok(result) 29 28 } 30 29 31 30 pub async fn get_album(&self, id: &str) -> Result<Option<Album>, Error> { 32 - let url = format!("{}/albums/{}", BASE_URL, id); 33 - let client = reqwest::Client::new(); 34 - let response = client.get(&url) 35 - .bearer_auth(&self.token) 36 - .send().await?; 31 + let url = format!("{}/albums/{}", BASE_URL, id); 32 + let client = reqwest::Client::new(); 33 + let response = client.get(&url).bearer_auth(&self.token).send().await?; 37 34 38 - let headers = response.headers().clone(); 39 - let data = response.text().await?; 35 + let headers = response.headers().clone(); 36 + let data = response.text().await?; 40 37 41 - if data == "Too many requests" { 42 - println!("> retry-after {}", headers.get("retry-after").unwrap().to_str().unwrap()); 43 - println!("> {} [get_album]", data); 44 - return Ok(None); 45 - } 38 + if data == "Too many requests" { 39 + println!( 40 + "> retry-after {}", 41 + headers.get("retry-after").unwrap().to_str().unwrap() 42 + ); 43 + println!("> {} [get_album]", data); 44 + return Ok(None); 45 + } 46 46 47 - Ok(Some(serde_json::from_str(&data)?)) 48 - } 47 + Ok(Some(serde_json::from_str(&data)?)) 48 + } 49 49 50 - pub async fn get_artist(&self, id: &str) -> Result<Option<Artist>, Error> { 51 - let url = format!("{}/artists/{}", BASE_URL, id); 52 - let client = reqwest::Client::new(); 53 - let response = client.get(&url) 54 - .bearer_auth(&self.token) 55 - .send().await?; 50 + pub async fn get_artist(&self, id: &str) -> Result<Option<Artist>, Error> { 51 + let url = format!("{}/artists/{}", BASE_URL, id); 52 + let client = reqwest::Client::new(); 53 + let response = client.get(&url).bearer_auth(&self.token).send().await?; 56 54 57 - let headers = response.headers().clone(); 58 - let data = response.text().await?; 55 + let headers = response.headers().clone(); 56 + let data = response.text().await?; 59 57 60 - if data == "Too many requests" { 61 - println!("> retry-after {}", headers.get("retry-after").unwrap().to_str().unwrap()); 62 - println!("> {} [get_artist]", data); 63 - return Ok(None); 64 - } 58 + if data == "Too many requests" { 59 + println!( 60 + "> retry-after {}", 61 + headers.get("retry-after").unwrap().to_str().unwrap() 62 + ); 63 + println!("> {} [get_artist]", data); 64 + return Ok(None); 65 + } 65 66 66 - Ok(Some(serde_json::from_str(&data)?)) 67 - } 67 + Ok(Some(serde_json::from_str(&data)?)) 68 + } 68 69 }
+20 -20
crates/webscrobbler/src/spotify/mod.rs
··· 1 1 use std::env; 2 2 3 + use anyhow::Error; 3 4 use reqwest::Client; 4 5 use types::AccessToken; 5 - use anyhow::Error; 6 6 7 7 pub mod client; 8 8 pub mod types; 9 - 10 9 11 10 pub async fn refresh_token(token: &str) -> Result<AccessToken, Error> { 12 - if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { 13 - panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); 14 - } 11 + if env::var("SPOTIFY_CLIENT_ID").is_err() || env::var("SPOTIFY_CLIENT_SECRET").is_err() { 12 + panic!("Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables"); 13 + } 15 14 16 - let client_id = env::var("SPOTIFY_CLIENT_ID")?; 17 - let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; 15 + let client_id = env::var("SPOTIFY_CLIENT_ID")?; 16 + let client_secret = env::var("SPOTIFY_CLIENT_SECRET")?; 18 17 19 - let client = Client::new(); 18 + let client = Client::new(); 20 19 21 - let response = client.post("https://accounts.spotify.com/api/token") 22 - .basic_auth(&client_id, Some(client_secret)) 23 - .form(&[ 24 - ("grant_type", "refresh_token"), 25 - ("refresh_token", token), 26 - ("client_id", &client_id) 27 - ]) 28 - .send() 29 - .await?; 30 - let token = response.json::<AccessToken>().await?; 31 - Ok(token) 32 - } 20 + let response = client 21 + .post("https://accounts.spotify.com/api/token") 22 + .basic_auth(&client_id, Some(client_secret)) 23 + .form(&[ 24 + ("grant_type", "refresh_token"), 25 + ("refresh_token", token), 26 + ("client_id", &client_id), 27 + ]) 28 + .send() 29 + .await?; 30 + let token = response.json::<AccessToken>().await?; 31 + Ok(token) 32 + }