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

scan google drive and dropbox Music folder

+1277 -29
+316 -25
Cargo.lock
··· 8 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 9 checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" 10 10 dependencies = [ 11 - "bitflags", 11 + "bitflags 2.8.0", 12 12 "bytes", 13 13 "futures-core", 14 14 "futures-sink", ··· 31 31 "actix-utils", 32 32 "ahash 0.8.11", 33 33 "base64", 34 - "bitflags", 34 + "bitflags 2.8.0", 35 35 "brotli", 36 36 "bytes", 37 37 "bytestring", ··· 508 508 source = "registry+https://github.com/rust-lang/crates.io-index" 509 509 checksum = "0f40f6be8f78af1ab610db7d9b236e21d587b7168e368a36275d2e5670096735" 510 510 dependencies = [ 511 - "bitflags", 511 + "bitflags 2.8.0", 512 512 ] 513 513 514 514 [[package]] ··· 667 667 668 668 [[package]] 669 669 name = "bitflags" 670 + version = "1.3.2" 671 + source = "registry+https://github.com/rust-lang/crates.io-index" 672 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 673 + 674 + [[package]] 675 + name = "bitflags" 670 676 version = "2.8.0" 671 677 source = "registry+https://github.com/rust-lang/crates.io-index" 672 678 checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" ··· 1114 1120 source = "registry+https://github.com/rust-lang/crates.io-index" 1115 1121 checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 1116 1122 dependencies = [ 1117 - "bitflags", 1123 + "bitflags 2.8.0", 1118 1124 "crossterm_winapi", 1119 1125 "parking_lot", 1120 - "rustix", 1126 + "rustix 0.38.44", 1121 1127 "winapi", 1122 1128 ] 1123 1129 ··· 1267 1273 "chrono", 1268 1274 "ctr", 1269 1275 "dotenv", 1276 + "futures", 1270 1277 "hex", 1271 1278 "jsonwebtoken", 1279 + "lofty", 1280 + "md5", 1272 1281 "owo-colors", 1273 1282 "redis", 1274 1283 "reqwest", 1275 1284 "serde", 1276 1285 "serde_json", 1286 + "sha256", 1277 1287 "sqlx", 1288 + "symphonia", 1289 + "tempfile", 1278 1290 "tokio", 1279 1291 "tokio-stream", 1280 1292 ] ··· 1402 1414 ] 1403 1415 1404 1416 [[package]] 1417 + name = "extended" 1418 + version = "0.1.0" 1419 + source = "registry+https://github.com/rust-lang/crates.io-index" 1420 + checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" 1421 + 1422 + [[package]] 1405 1423 name = "fallible-iterator" 1406 1424 version = "0.3.0" 1407 1425 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1649 1667 "chrono", 1650 1668 "ctr", 1651 1669 "dotenv", 1670 + "futures", 1652 1671 "hex", 1653 1672 "jsonwebtoken", 1673 + "lofty", 1674 + "md5", 1654 1675 "owo-colors", 1655 1676 "redis", 1656 1677 "reqwest", 1657 1678 "serde", 1658 1679 "serde_json", 1659 1680 "serde_urlencoded", 1681 + "sha256", 1660 1682 "sqlx", 1683 + "symphonia", 1684 + "tempfile", 1661 1685 "tokio", 1662 1686 "tokio-stream", 1663 1687 ] ··· 2254 2278 source = "registry+https://github.com/rust-lang/crates.io-index" 2255 2279 checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 2256 2280 dependencies = [ 2257 - "bitflags", 2281 + "bitflags 2.8.0", 2258 2282 "libc", 2259 2283 "redox_syscall", 2260 2284 ] ··· 2274 2298 version = "0.4.15" 2275 2299 source = "registry+https://github.com/rust-lang/crates.io-index" 2276 2300 checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 2301 + 2302 + [[package]] 2303 + name = "linux-raw-sys" 2304 + version = "0.9.3" 2305 + source = "registry+https://github.com/rust-lang/crates.io-index" 2306 + checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 2277 2307 2278 2308 [[package]] 2279 2309 name = "litemap" ··· 2309 2339 ] 2310 2340 2311 2341 [[package]] 2342 + name = "lofty" 2343 + version = "0.22.2" 2344 + source = "registry+https://github.com/rust-lang/crates.io-index" 2345 + checksum = "781de624f162b1a8cbfbd577103ee9b8e5f62854b053ff48f4e31e68a0a7df6f" 2346 + dependencies = [ 2347 + "byteorder", 2348 + "data-encoding", 2349 + "flate2", 2350 + "lofty_attr", 2351 + "log", 2352 + "ogg_pager", 2353 + "paste", 2354 + ] 2355 + 2356 + [[package]] 2357 + name = "lofty_attr" 2358 + version = "0.11.1" 2359 + source = "registry+https://github.com/rust-lang/crates.io-index" 2360 + checksum = "ed9983e64b2358522f745c1251924e3ab7252d55637e80f6a0a3de642d6a9efc" 2361 + dependencies = [ 2362 + "proc-macro2", 2363 + "quote", 2364 + "syn 2.0.98", 2365 + ] 2366 + 2367 + [[package]] 2312 2368 name = "log" 2313 2369 version = "0.4.25" 2314 2370 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2342 2398 "cfg-if", 2343 2399 "digest", 2344 2400 ] 2401 + 2402 + [[package]] 2403 + name = "md5" 2404 + version = "0.7.0" 2405 + source = "registry+https://github.com/rust-lang/crates.io-index" 2406 + checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" 2345 2407 2346 2408 [[package]] 2347 2409 name = "memchr" ··· 2541 2603 checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 2542 2604 dependencies = [ 2543 2605 "memchr", 2606 + ] 2607 + 2608 + [[package]] 2609 + name = "ogg_pager" 2610 + version = "0.7.0" 2611 + source = "registry+https://github.com/rust-lang/crates.io-index" 2612 + checksum = "e034c10fb5c1c012c1b327b85df89fb0ef98ae66ec28af30f0d1eed804a40c19" 2613 + dependencies = [ 2614 + "byteorder", 2544 2615 ] 2545 2616 2546 2617 [[package]] ··· 2851 2922 checksum = "796d06eae7e6e74ed28ea54a8fccc584ebac84e6cf0e1e9ba41ffc807b169a01" 2852 2923 dependencies = [ 2853 2924 "ahash 0.8.11", 2854 - "bitflags", 2925 + "bitflags 2.8.0", 2855 2926 "bytemuck", 2856 2927 "chrono", 2857 2928 "chrono-tz", ··· 2898 2969 checksum = "c8e639991a8ad4fb12880ab44bcc3cf44a5703df003142334d9caf86d77d77e7" 2899 2970 dependencies = [ 2900 2971 "ahash 0.8.11", 2901 - "bitflags", 2972 + "bitflags 2.8.0", 2902 2973 "hashbrown 0.15.2", 2903 2974 "num-traits", 2904 2975 "once_cell", ··· 2959 3030 checksum = "a0a731a672dfc8ac38c1f73c9a4b2ae38d2fc8ac363bfb64c5f3a3e072ffc5ad" 2960 3031 dependencies = [ 2961 3032 "ahash 0.8.11", 2962 - "bitflags", 3033 + "bitflags 2.8.0", 2963 3034 "chrono", 2964 3035 "memchr", 2965 3036 "once_cell", ··· 3097 3168 checksum = "4f03533a93aa66127fcb909a87153a3c7cfee6f0ae59f497e73d7736208da54c" 3098 3169 dependencies = [ 3099 3170 "ahash 0.8.11", 3100 - "bitflags", 3171 + "bitflags 2.8.0", 3101 3172 "bytemuck", 3102 3173 "bytes", 3103 3174 "chrono", ··· 3128 3199 source = "registry+https://github.com/rust-lang/crates.io-index" 3129 3200 checksum = "6bf47f7409f8e75328d7d034be390842924eb276716d0458607be0bddb8cc839" 3130 3201 dependencies = [ 3131 - "bitflags", 3202 + "bitflags 2.8.0", 3132 3203 "bytemuck", 3133 3204 "polars-arrow", 3134 3205 "polars-compute", ··· 3428 3499 source = "registry+https://github.com/rust-lang/crates.io-index" 3429 3500 checksum = "529468c1335c1c03919960dfefdb1b3648858c20d7ec2d0663e728e4a717efbc" 3430 3501 dependencies = [ 3431 - "bitflags", 3502 + "bitflags 2.8.0", 3432 3503 ] 3433 3504 3434 3505 [[package]] ··· 3494 3565 source = "registry+https://github.com/rust-lang/crates.io-index" 3495 3566 checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 3496 3567 dependencies = [ 3497 - "bitflags", 3568 + "bitflags 2.8.0", 3498 3569 ] 3499 3570 3500 3571 [[package]] ··· 3693 3764 source = "registry+https://github.com/rust-lang/crates.io-index" 3694 3765 checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 3695 3766 dependencies = [ 3696 - "bitflags", 3767 + "bitflags 2.8.0", 3768 + "errno", 3769 + "libc", 3770 + "linux-raw-sys 0.4.15", 3771 + "windows-sys 0.59.0", 3772 + ] 3773 + 3774 + [[package]] 3775 + name = "rustix" 3776 + version = "1.0.3" 3777 + source = "registry+https://github.com/rust-lang/crates.io-index" 3778 + checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" 3779 + dependencies = [ 3780 + "bitflags 2.8.0", 3697 3781 "errno", 3698 3782 "libc", 3699 - "linux-raw-sys", 3783 + "linux-raw-sys 0.9.3", 3700 3784 "windows-sys 0.59.0", 3701 3785 ] 3702 3786 ··· 3795 3879 source = "registry+https://github.com/rust-lang/crates.io-index" 3796 3880 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 3797 3881 dependencies = [ 3798 - "bitflags", 3882 + "bitflags 2.8.0", 3799 3883 "core-foundation", 3800 3884 "core-foundation-sys", 3801 3885 "libc", ··· 3908 3992 "cfg-if", 3909 3993 "cpufeatures", 3910 3994 "digest", 3995 + ] 3996 + 3997 + [[package]] 3998 + name = "sha256" 3999 + version = "1.6.0" 4000 + source = "registry+https://github.com/rust-lang/crates.io-index" 4001 + checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" 4002 + dependencies = [ 4003 + "async-trait", 4004 + "bytes", 4005 + "hex", 4006 + "sha2", 4007 + "tokio", 3911 4008 ] 3912 4009 3913 4010 [[package]] ··· 4155 4252 dependencies = [ 4156 4253 "atoi", 4157 4254 "base64", 4158 - "bitflags", 4255 + "bitflags 2.8.0", 4159 4256 "byteorder", 4160 4257 "bytes", 4161 4258 "chrono", ··· 4198 4295 dependencies = [ 4199 4296 "atoi", 4200 4297 "base64", 4201 - "bitflags", 4298 + "bitflags 2.8.0", 4202 4299 "byteorder", 4203 4300 "chrono", 4204 4301 "crc", ··· 4357 4454 checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 4358 4455 4359 4456 [[package]] 4457 + name = "symphonia" 4458 + version = "0.5.4" 4459 + source = "registry+https://github.com/rust-lang/crates.io-index" 4460 + checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" 4461 + dependencies = [ 4462 + "lazy_static", 4463 + "symphonia-bundle-flac", 4464 + "symphonia-bundle-mp3", 4465 + "symphonia-codec-aac", 4466 + "symphonia-codec-adpcm", 4467 + "symphonia-codec-alac", 4468 + "symphonia-codec-pcm", 4469 + "symphonia-codec-vorbis", 4470 + "symphonia-core", 4471 + "symphonia-format-caf", 4472 + "symphonia-format-isomp4", 4473 + "symphonia-format-mkv", 4474 + "symphonia-format-ogg", 4475 + "symphonia-format-riff", 4476 + "symphonia-metadata", 4477 + ] 4478 + 4479 + [[package]] 4480 + name = "symphonia-bundle-flac" 4481 + version = "0.5.4" 4482 + source = "registry+https://github.com/rust-lang/crates.io-index" 4483 + checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" 4484 + dependencies = [ 4485 + "log", 4486 + "symphonia-core", 4487 + "symphonia-metadata", 4488 + "symphonia-utils-xiph", 4489 + ] 4490 + 4491 + [[package]] 4492 + name = "symphonia-bundle-mp3" 4493 + version = "0.5.4" 4494 + source = "registry+https://github.com/rust-lang/crates.io-index" 4495 + checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" 4496 + dependencies = [ 4497 + "lazy_static", 4498 + "log", 4499 + "symphonia-core", 4500 + "symphonia-metadata", 4501 + ] 4502 + 4503 + [[package]] 4504 + name = "symphonia-codec-aac" 4505 + version = "0.5.4" 4506 + source = "registry+https://github.com/rust-lang/crates.io-index" 4507 + checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" 4508 + dependencies = [ 4509 + "lazy_static", 4510 + "log", 4511 + "symphonia-core", 4512 + ] 4513 + 4514 + [[package]] 4515 + name = "symphonia-codec-adpcm" 4516 + version = "0.5.4" 4517 + source = "registry+https://github.com/rust-lang/crates.io-index" 4518 + checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" 4519 + dependencies = [ 4520 + "log", 4521 + "symphonia-core", 4522 + ] 4523 + 4524 + [[package]] 4525 + name = "symphonia-codec-alac" 4526 + version = "0.5.4" 4527 + source = "registry+https://github.com/rust-lang/crates.io-index" 4528 + checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" 4529 + dependencies = [ 4530 + "log", 4531 + "symphonia-core", 4532 + ] 4533 + 4534 + [[package]] 4535 + name = "symphonia-codec-pcm" 4536 + version = "0.5.4" 4537 + source = "registry+https://github.com/rust-lang/crates.io-index" 4538 + checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" 4539 + dependencies = [ 4540 + "log", 4541 + "symphonia-core", 4542 + ] 4543 + 4544 + [[package]] 4545 + name = "symphonia-codec-vorbis" 4546 + version = "0.5.4" 4547 + source = "registry+https://github.com/rust-lang/crates.io-index" 4548 + checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" 4549 + dependencies = [ 4550 + "log", 4551 + "symphonia-core", 4552 + "symphonia-utils-xiph", 4553 + ] 4554 + 4555 + [[package]] 4556 + name = "symphonia-core" 4557 + version = "0.5.4" 4558 + source = "registry+https://github.com/rust-lang/crates.io-index" 4559 + checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" 4560 + dependencies = [ 4561 + "arrayvec", 4562 + "bitflags 1.3.2", 4563 + "bytemuck", 4564 + "lazy_static", 4565 + "log", 4566 + ] 4567 + 4568 + [[package]] 4569 + name = "symphonia-format-caf" 4570 + version = "0.5.4" 4571 + source = "registry+https://github.com/rust-lang/crates.io-index" 4572 + checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" 4573 + dependencies = [ 4574 + "log", 4575 + "symphonia-core", 4576 + "symphonia-metadata", 4577 + ] 4578 + 4579 + [[package]] 4580 + name = "symphonia-format-isomp4" 4581 + version = "0.5.4" 4582 + source = "registry+https://github.com/rust-lang/crates.io-index" 4583 + checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" 4584 + dependencies = [ 4585 + "encoding_rs", 4586 + "log", 4587 + "symphonia-core", 4588 + "symphonia-metadata", 4589 + "symphonia-utils-xiph", 4590 + ] 4591 + 4592 + [[package]] 4593 + name = "symphonia-format-mkv" 4594 + version = "0.5.4" 4595 + source = "registry+https://github.com/rust-lang/crates.io-index" 4596 + checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" 4597 + dependencies = [ 4598 + "lazy_static", 4599 + "log", 4600 + "symphonia-core", 4601 + "symphonia-metadata", 4602 + "symphonia-utils-xiph", 4603 + ] 4604 + 4605 + [[package]] 4606 + name = "symphonia-format-ogg" 4607 + version = "0.5.4" 4608 + source = "registry+https://github.com/rust-lang/crates.io-index" 4609 + checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" 4610 + dependencies = [ 4611 + "log", 4612 + "symphonia-core", 4613 + "symphonia-metadata", 4614 + "symphonia-utils-xiph", 4615 + ] 4616 + 4617 + [[package]] 4618 + name = "symphonia-format-riff" 4619 + version = "0.5.4" 4620 + source = "registry+https://github.com/rust-lang/crates.io-index" 4621 + checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" 4622 + dependencies = [ 4623 + "extended", 4624 + "log", 4625 + "symphonia-core", 4626 + "symphonia-metadata", 4627 + ] 4628 + 4629 + [[package]] 4630 + name = "symphonia-metadata" 4631 + version = "0.5.4" 4632 + source = "registry+https://github.com/rust-lang/crates.io-index" 4633 + checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" 4634 + dependencies = [ 4635 + "encoding_rs", 4636 + "lazy_static", 4637 + "log", 4638 + "symphonia-core", 4639 + ] 4640 + 4641 + [[package]] 4642 + name = "symphonia-utils-xiph" 4643 + version = "0.5.4" 4644 + source = "registry+https://github.com/rust-lang/crates.io-index" 4645 + checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" 4646 + dependencies = [ 4647 + "symphonia-core", 4648 + "symphonia-metadata", 4649 + ] 4650 + 4651 + [[package]] 4360 4652 name = "syn" 4361 4653 version = "1.0.109" 4362 4654 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4430 4722 4431 4723 [[package]] 4432 4724 name = "tempfile" 4433 - version = "3.17.1" 4725 + version = "3.19.1" 4434 4726 source = "registry+https://github.com/rust-lang/crates.io-index" 4435 - checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" 4727 + checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 4436 4728 dependencies = [ 4437 - "cfg-if", 4438 4729 "fastrand", 4439 4730 "getrandom 0.3.1", 4440 4731 "once_cell", 4441 - "rustix", 4732 + "rustix 1.0.3", 4442 4733 "windows-sys 0.59.0", 4443 4734 ] 4444 4735 ··· 5271 5562 source = "registry+https://github.com/rust-lang/crates.io-index" 5272 5563 checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 5273 5564 dependencies = [ 5274 - "bitflags", 5565 + "bitflags 2.8.0", 5275 5566 ] 5276 5567 5277 5568 [[package]] ··· 5302 5593 checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" 5303 5594 dependencies = [ 5304 5595 "libc", 5305 - "linux-raw-sys", 5306 - "rustix", 5596 + "linux-raw-sys 0.4.15", 5597 + "rustix 0.38.44", 5307 5598 ] 5308 5599 5309 5600 [[package]]
+6
crates/dropbox/Cargo.toml
··· 14 14 chrono = { version = "0.4.39", features = ["serde"] } 15 15 ctr = "0.9.2" 16 16 dotenv = "0.15.0" 17 + futures = "0.3.31" 17 18 hex = "0.4.3" 18 19 jsonwebtoken = "9.3.1" 20 + lofty = "0.22.2" 21 + md5 = "0.7.0" 19 22 owo-colors = "4.1.0" 20 23 redis = "0.29.0" 21 24 reqwest = { version = "0.12.12", features = [ ··· 26 29 ], default-features = false } 27 30 serde = { version = "1.0.217", features = ["derive"] } 28 31 serde_json = "1.0.139" 32 + sha256 = "1.6.0" 29 33 sqlx = { version = "0.8.3", features = [ 30 34 "runtime-tokio", 31 35 "tls-rustls", ··· 34 38 "derive", 35 39 "macros", 36 40 ] } 41 + symphonia = { version = "0.5.4", features = ["all"] } 42 + tempfile = "3.19.1" 37 43 tokio = { version = "1.43.0", features = ["full"] } 38 44 tokio-stream = { version = "0.1.17", features = ["full"] }
+4
crates/dropbox/src/consts.rs
··· 1 + pub const AUDIO_EXTENSIONS: [&str; 18] = [ 2 + "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "aif", "ac3", 3 + "opus", "spx", "sid", "ape", "wma", 4 + ];
+15 -1
crates/dropbox/src/main.rs
··· 1 - use std::{env, sync::Arc}; 1 + use std::{env, sync::Arc, thread}; 2 2 use actix_web::{get, post, web::{self, Data}, App, HttpRequest, HttpResponse, HttpServer, Responder}; 3 3 use anyhow::Error; 4 4 use dotenv::dotenv; 5 5 use handlers::handle; 6 6 use owo_colors::OwoColorize; 7 + use scan::scan_dropbox; 7 8 use serde_json::json; 8 9 use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; 9 10 ··· 13 14 pub mod repo; 14 15 pub mod types; 15 16 pub mod client; 17 + pub mod consts; 18 + pub mod scan; 19 + pub mod token; 16 20 17 21 #[get("/")] 18 22 async fn index(_req: HttpRequest) -> HttpResponse { ··· 48 52 49 53 let pool = PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; 50 54 let conn = Arc::new(pool); 55 + 56 + let cloned_conn = conn.clone(); 57 + 58 + thread::spawn(move || { 59 + let rt = tokio::runtime::Runtime::new().unwrap(); 60 + rt.block_on( 61 + scan_dropbox(cloned_conn) 62 + )?; 63 + Ok::<(), Error>(()) 64 + }); 51 65 52 66 let conn = conn.clone(); 53 67 HttpServer::new(move || {
+25
crates/dropbox/src/repo/dropbox_path.rs
··· 1 + use sqlx::{Pool, Postgres}; 2 + 3 + use crate::xata::track::Track; 4 + use crate::types::file::Entry; 5 + 6 + pub async fn create_dropbox_path( 7 + pool: &Pool<Postgres>, 8 + file: &Entry, 9 + track: &Track, 10 + dropbox_id: &str 11 + ) -> Result<(), sqlx::Error> { 12 + sqlx::query(r#" 13 + INSERT INTO dropbox_paths (dropbox_id, path, track_id, name) 14 + VALUES ($1, $2, $3, $4) 15 + ON CONFLICT (dropbox_id, track_id) DO NOTHING 16 + "#) 17 + .bind(dropbox_id) 18 + .bind(&file.path_display) 19 + .bind(&track.xata_id) 20 + .bind(&file.name) 21 + .execute(pool) 22 + .await?; 23 + 24 + Ok(()) 25 + }
+27 -1
crates/dropbox/src/repo/dropbox_token.rs
··· 5 5 6 6 pub async fn find_dropbox_refresh_token(pool: &Pool<Postgres>, did: &str) -> Result<Option<String>, Error> { 7 7 let results: Vec<DropboxTokenWithDid> = sqlx::query_as(r#" 8 - SELECT * FROM dropbox d 8 + SELECT 9 + d.xata_id, 10 + d.xata_version, 11 + d.xata_createdat, 12 + d.xata_updatedat, 13 + u.did as did, 14 + dt.refresh_token as refresh_token 15 + FROM dropbox d 9 16 LEFT JOIN users u ON d.user_id = u.xata_id 10 17 LEFT JOIN dropbox_tokens dt ON d.dropbox_token_id = dt.xata_id 11 18 WHERE u.did = $1 ··· 20 27 21 28 Ok(Some(results[0].refresh_token.clone())) 22 29 } 30 + 31 + pub async fn find_dropbox_refresh_tokens(pool: &Pool<Postgres>) -> Result<Vec<DropboxTokenWithDid>, Error> { 32 + let results: Vec<DropboxTokenWithDid> = sqlx::query_as(r#" 33 + SELECT 34 + d.xata_id, 35 + d.xata_version, 36 + d.xata_createdat, 37 + d.xata_updatedat, 38 + u.did, 39 + dt.refresh_token as refresh_token 40 + FROM dropbox d 41 + LEFT JOIN users u ON d.user_id = u.xata_id 42 + LEFT JOIN dropbox_tokens dt ON d.dropbox_token_id = dt.xata_id 43 + "#) 44 + .fetch_all(pool) 45 + .await?; 46 + 47 + Ok(results) 48 + }
+2
crates/dropbox/src/repo/mod.rs
··· 1 + pub mod dropbox_path; 1 2 pub mod dropbox_token; 3 + pub mod track;
+19
crates/dropbox/src/repo/track.rs
··· 1 + use anyhow::Error; 2 + use sqlx::{Pool, Postgres}; 3 + 4 + use crate::xata::track::Track; 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#" 8 + SELECT * FROM tracks WHERE sha256 = $1 9 + "#) 10 + .bind(sha256) 11 + .fetch_all(pool) 12 + .await?; 13 + 14 + if results.len() == 0 { 15 + return Ok(None); 16 + } 17 + 18 + Ok(Some(results[0].clone())) 19 + }
+316
crates/dropbox/src/scan.rs
··· 1 + use std::{env, fs::File, io::Write, path::Path, sync::Arc}; 2 + 3 + use anyhow::Error; 4 + use futures::future::BoxFuture; 5 + use lofty::{file::TaggedFileExt, picture::{MimeType, Picture}, probe::Probe, tag::Accessor}; 6 + use owo_colors::OwoColorize; 7 + use reqwest::{multipart, Client}; 8 + use serde_json::json; 9 + use sqlx::{Pool, Postgres}; 10 + use symphonia::core::{formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint}; 11 + use tempfile::TempDir; 12 + 13 + 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_tokens, track::get_track_by_hash}, 17 + token::generate_token, 18 + types::file::{Entry, EntryList} 19 + }; 20 + 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 + 29 + let res = get_access_token(&refresh_token).await?; 30 + scan_audio_files( 31 + pool.clone(), 32 + "/Music".to_string(), 33 + res.access_token, 34 + token.did, 35 + token.xata_id 36 + ).await?; 37 + } 38 + Ok(()) 39 + } 40 + 41 + pub fn scan_audio_files( 42 + pool: Arc<Pool<Postgres>>, 43 + path: String, 44 + access_token: String, 45 + did: String, 46 + dropbox_id: String, 47 + ) -> BoxFuture<'static, Result<(), Error>> { 48 + Box::pin(async move { 49 + let client = Client::new(); 50 + 51 + let res = client.post(&format!("{}/files/get_metadata", BASE_URL)) 52 + .bearer_auth(&access_token) 53 + .json(&json!({ "path": path })) 54 + .send() 55 + .await?; 56 + 57 + if res.status().as_u16() == 400 || res.status().as_u16() == 409 { 58 + println!("Path not found: {}", path.bright_red()); 59 + return Ok(()); 60 + } 61 + 62 + let entry = res.json::<Entry>().await?; 63 + 64 + if entry.tag.clone().unwrap().as_str() == "folder" { 65 + println!("Scanning folder: {}", path.bright_green()); 66 + let res = client.post(&format!("{}/files/list_folder", BASE_URL)) 67 + .bearer_auth(&access_token) 68 + .json(&json!({ "path": path })) 69 + .send() 70 + .await?; 71 + 72 + let entries = res.json::<EntryList>().await?; 73 + 74 + for entry in entries.entries { 75 + scan_audio_files(pool.clone(), entry.path_display, access_token.clone(), did.clone(), dropbox_id.clone()).await?; 76 + tokio::time::sleep(std::time::Duration::from_secs(3)).await; 77 + } 78 + 79 + return Ok(()); 80 + } 81 + 82 + if !AUDIO_EXTENSIONS 83 + .into_iter() 84 + .any(|ext| path.ends_with(&format!(".{}", ext))) 85 + { 86 + return Ok(()); 87 + } 88 + 89 + let client = Client::new(); 90 + 91 + println!("Downloading file: {}", path.bright_green()); 92 + 93 + let res = client.post(&format!("{}/files/download", CONTENT_URL)) 94 + .bearer_auth(&access_token) 95 + .header("Dropbox-API-Arg", &json!({ "path": path }).to_string()) 96 + .send() 97 + .await?; 98 + 99 + let bytes = res.bytes().await?; 100 + 101 + let temp_dir = TempDir::new()?; 102 + let tmppath = temp_dir.path().join(&format!("{}", entry.name)); 103 + let mut tmpfile = File::create(&tmppath)?; 104 + tmpfile.write_all(&bytes)?; 105 + 106 + println!("Reading file: {}", &tmppath.clone().display().to_string().bright_green()); 107 + 108 + let tagged_file = match Probe::open(&tmppath)?.read() 109 + { 110 + Ok(tagged_file) => tagged_file, 111 + Err(e) => { 112 + println!("Error opening file: {}", e); 113 + return Ok(()); 114 + } 115 + }; 116 + 117 + let primary_tag = tagged_file.primary_tag(); 118 + let tag = match primary_tag { 119 + Some(tag) => tag, 120 + None => { 121 + println!("No tag found in file"); 122 + return Ok(()); 123 + } 124 + }; 125 + 126 + let pictures = tag.pictures(); 127 + 128 + println!("Title: {}", tag.get_string(&lofty::tag::ItemKey::TrackTitle).unwrap_or_default().bright_green()); 129 + println!("Artist: {}", tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default().bright_green()); 130 + println!("Album Artist: {}", tag.get_string(&lofty::tag::ItemKey::AlbumArtist).unwrap_or_default().bright_green()); 131 + println!("Album: {}", tag.get_string(&lofty::tag::ItemKey::AlbumTitle).unwrap_or_default().bright_green()); 132 + println!("Lyrics: {}", tag.get_string(&lofty::tag::ItemKey::Lyrics).unwrap_or_default().bright_green()); 133 + println!("Year: {}", tag.year().unwrap_or_default().bright_green()); 134 + println!("Track Number: {}", tag.track().unwrap_or_default().bright_green()); 135 + println!("Track Total: {}", tag.track_total().unwrap_or_default().bright_green()); 136 + println!("Release Date: {:?}", tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).unwrap_or_default().bright_green()); 137 + println!("Recording Date: {:?}", tag.get_string(&lofty::tag::ItemKey::RecordingDate).unwrap_or_default().bright_green()); 138 + println!("Copyright Message: {}", tag.get_string(&lofty::tag::ItemKey::CopyrightMessage).unwrap_or_default().bright_green()); 139 + println!("Pictures: {:?}", pictures); 140 + 141 + let title = tag.get_string(&lofty::tag::ItemKey::TrackTitle).unwrap_or_default(); 142 + let artist = tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default(); 143 + let album = tag.get_string(&lofty::tag::ItemKey::AlbumTitle).unwrap_or_default(); 144 + let album_artist = tag.get_string(&lofty::tag::ItemKey::AlbumArtist).unwrap_or_default(); 145 + 146 + let access_token = generate_token(&did)?; 147 + 148 + // check if track exists 149 + // 150 + // if not, create track 151 + // upload album art 152 + // 153 + // link path to track 154 + 155 + let hash = sha256::digest( 156 + format!("{} - {} - {}", title, artist, album).to_lowercase(), 157 + ); 158 + 159 + let track = get_track_by_hash(&pool, &hash).await?; 160 + let duration = get_track_duration(&tmppath).await?; 161 + let albumart_id = md5::compute(&format!("{} - {}", album_artist, album).to_lowercase()); 162 + let albumart_id = format!("{:x}", albumart_id); 163 + 164 + match track { 165 + Some(track) => { 166 + println!("Track exists: {}", title.bright_green()); 167 + let status = create_dropbox_path( 168 + &pool, 169 + &entry, 170 + &track, 171 + &dropbox_id, 172 + ) 173 + .await; 174 + println!("status {:?}", status); 175 + }, 176 + None => { 177 + println!("Creating track: {}", title.bright_green()); 178 + let album_art = upload_album_cover(albumart_id.into(), pictures, &access_token).await?; 179 + let client = Client::new(); 180 + const URL: &str = "https://api.rocksky.app/tracks"; 181 + let response = client 182 + .post(URL) 183 + .header("Authorization", format!("Bearer {}", access_token)) 184 + .json(&serde_json::json!({ 185 + "title": tag.get_string(&lofty::tag::ItemKey::TrackTitle), 186 + "album": tag.get_string(&lofty::tag::ItemKey::AlbumTitle), 187 + "artist": tag.get_string(&lofty::tag::ItemKey::TrackArtist), 188 + "albumArtist": tag.get_string(&lofty::tag::ItemKey::AlbumArtist), 189 + "duration": duration, 190 + "trackNumber": tag.track(), 191 + "releaseDate": tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).map(|date| match date.contains("-") { 192 + true => Some(date), 193 + false => None, 194 + }), 195 + "year": tag.year(), 196 + "discNumber": tag.disk(), 197 + "composer": tag.get_string(&lofty::tag::ItemKey::Composer), 198 + "albumArt": match album_art{ 199 + Some(album_art) => Some(format!("https://cdn.rocksky.app/covers/{}", album_art)), 200 + None => None 201 + }, 202 + "lyrics": tag.get_string(&lofty::tag::ItemKey::Lyrics), 203 + "copyrightMessage": tag.get_string(&lofty::tag::ItemKey::CopyrightMessage), 204 + })) 205 + .send() 206 + .await?; 207 + println!("Track Saved: {} {}", title, response.status()); 208 + 209 + 210 + let track = get_track_by_hash(&pool, &hash).await?; 211 + if let Some(track) = track { 212 + create_dropbox_path( 213 + &pool, 214 + &entry, 215 + &track, 216 + &dropbox_id, 217 + ) 218 + .await?; 219 + return Ok(()); 220 + } 221 + 222 + println!("Failed to create track: {}", title.bright_green()); 223 + } 224 + } 225 + 226 + Ok(()) 227 + }) 228 + } 229 + 230 + pub async fn upload_album_cover(name: String, pictures: &[Picture], token: &str) -> Result<Option<String>, Error> { 231 + if pictures.is_empty() { 232 + return Ok(None); 233 + } 234 + 235 + let picture = &pictures[0]; 236 + 237 + let buffer = match picture.mime_type() { 238 + Some(MimeType::Jpeg) => Some(picture.data().to_vec()), 239 + Some(MimeType::Png) => Some(picture.data().to_vec()), 240 + Some(MimeType::Gif) => Some(picture.data().to_vec()), 241 + Some(MimeType::Bmp) => Some(picture.data().to_vec()), 242 + Some(MimeType::Tiff) => Some(picture.data().to_vec()), 243 + _ => None 244 + }; 245 + 246 + if buffer.is_none() { 247 + return Ok(None); 248 + } 249 + 250 + let buffer = buffer.unwrap(); 251 + 252 + let ext = match picture.mime_type() { 253 + Some(MimeType::Jpeg) => "jpg", 254 + Some(MimeType::Png) => "png", 255 + Some(MimeType::Gif) => "gif", 256 + Some(MimeType::Bmp) => "bmp", 257 + Some(MimeType::Tiff) => "tiff", 258 + _ => { 259 + return Ok(None); 260 + } 261 + }; 262 + 263 + let name = format!("{}.{}", name, ext); 264 + 265 + let part = multipart::Part::bytes(buffer).file_name(name.clone()); 266 + let form = multipart::Form::new().part("file", part); 267 + let client = Client::new(); 268 + 269 + const URL: &str = "https://uploads.rocksky.app"; 270 + 271 + let response = client 272 + .post(URL) 273 + .header("Authorization", format!("Bearer {}", token)) 274 + .multipart(form) 275 + .send() 276 + .await?; 277 + 278 + println!("Cover uploaded: {}", response.status()); 279 + 280 + Ok(Some(name)) 281 + } 282 + 283 + 284 + 285 + pub async fn get_track_duration(path: &Path) -> Result<u64, Error> { 286 + let duration = 0; 287 + let media_source = MediaSourceStream::new(Box::new(std::fs::File::open(path)?), Default::default()); 288 + let mut hint = Hint::new(); 289 + 290 + if let Some(extension) = path.extension() { 291 + if let Some(extension) = extension.to_str() { 292 + hint.with_extension(extension); 293 + } 294 + } 295 + 296 + 297 + let meta_opts = MetadataOptions::default(); 298 + let format_opts = FormatOptions::default(); 299 + 300 + let probed = match symphonia::default::get_probe().format(&hint, media_source, &format_opts, &meta_opts) { 301 + Ok(probed) => probed, 302 + Err(_) => { 303 + println!("Error probing file"); 304 + return Ok(duration); 305 + }, 306 + }; 307 + 308 + if let Some(track) = probed.format.tracks().first() { 309 + if let Some(duration) = track.codec_params.n_frames { 310 + if let Some(sample_rate) = track.codec_params.sample_rate { 311 + return Ok((duration as f64 / sample_rate as f64) as u64 * 1000); 312 + } 313 + } 314 + } 315 + Ok(duration) 316 + }
+64
crates/dropbox/src/token.rs
··· 1 + use std::env; 2 + 3 + use anyhow::Error; 4 + use jsonwebtoken::DecodingKey; 5 + use jsonwebtoken::EncodingKey; 6 + use jsonwebtoken::Header; 7 + use jsonwebtoken::Validation; 8 + use serde::{Deserialize, Serialize}; 9 + 10 + #[derive(Debug, Serialize, Deserialize)] 11 + pub struct Claims { 12 + exp: usize, 13 + iat: usize, 14 + did: String, 15 + } 16 + 17 + pub fn generate_token(did: &str) -> Result<String, Error> { 18 + if env::var("JWT_SECRET").is_err() { 19 + return Err(Error::msg("JWT_SECRET is not set")); 20 + } 21 + 22 + let claims = Claims { 23 + exp: chrono::Utc::now().timestamp() as usize + 3600, 24 + iat: chrono::Utc::now().timestamp() as usize, 25 + did: did.to_string(), 26 + }; 27 + 28 + jsonwebtoken::encode( 29 + &Header::default(), 30 + &claims, 31 + &EncodingKey::from_secret(env::var("JWT_SECRET")?.as_ref()), 32 + ) 33 + .map_err(Into::into) 34 + } 35 + 36 + pub fn decode_token(token: &str) -> Result<Claims, Error> { 37 + if env::var("JWT_SECRET").is_err() { 38 + return Err(Error::msg("JWT_SECRET is not set")); 39 + } 40 + 41 + jsonwebtoken::decode::<Claims>( 42 + token, 43 + &DecodingKey::from_secret(env::var("JWT_SECRET")?.as_ref()), 44 + &Validation::default(), 45 + ) 46 + .map(|data| data.claims) 47 + .map_err(Into::into) 48 + } 49 + 50 + #[cfg(test)] 51 + mod tests { 52 + use dotenv::dotenv; 53 + 54 + use super::*; 55 + 56 + #[test] 57 + fn test_generate_token() { 58 + dotenv().ok(); 59 + let token = generate_token("did:plc:7vdlgi2bflelz7mmuxoqjfcr").unwrap(); 60 + let claims = decode_token(&token).unwrap(); 61 + 62 + assert_eq!(claims.did, "did:plc:7vdlgi2bflelz7mmuxoqjfcr"); 63 + } 64 + }
+6
crates/googledrive/Cargo.toml
··· 14 14 chrono = { version = "0.4.39", features = ["serde"] } 15 15 ctr = "0.9.2" 16 16 dotenv = "0.15.0" 17 + futures = "0.3.31" 17 18 hex = "0.4.3" 18 19 jsonwebtoken = "9.3.1" 20 + lofty = "0.22.2" 21 + md5 = "0.7.0" 19 22 owo-colors = "4.1.0" 20 23 redis = "0.29.0" 21 24 reqwest = { version = "0.12.12", features = [ ··· 27 30 serde = { version = "1.0.217", features = ["derive"] } 28 31 serde_json = "1.0.139" 29 32 serde_urlencoded = "0.7.1" 33 + sha256 = "1.6.0" 30 34 sqlx = { version = "0.8.3", features = [ 31 35 "runtime-tokio", 32 36 "tls-rustls", ··· 35 39 "derive", 36 40 "macros", 37 41 ] } 42 + symphonia = { version = "0.5.4", features = ["all"] } 43 + tempfile = "3.19.1" 38 44 tokio = { version = "1.43.0", features = ["full"] } 39 45 tokio-stream = { version = "0.1.17", features = ["full"] }
+4
crates/googledrive/src/consts.rs
··· 1 + pub const AUDIO_EXTENSIONS: [&str; 18] = [ 2 + "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "aif", "ac3", 3 + "opus", "spx", "sid", "ape", "wma", 4 + ];
+15 -1
crates/googledrive/src/main.rs
··· 1 - use std::{env, sync::Arc}; 1 + use std::{env, sync::Arc, thread}; 2 2 use actix_web::{get, post, web::{self, Data}, App, HttpRequest, HttpResponse, HttpServer, Responder}; 3 3 use anyhow::Error; 4 4 use dotenv::dotenv; 5 5 use handlers::handle; 6 6 use owo_colors::OwoColorize; 7 + use scan::scan_googledrive; 7 8 use serde_json::json; 8 9 use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; 9 10 11 + pub mod token; 10 12 pub mod xata; 11 13 pub mod crypto; 12 14 pub mod handlers; 13 15 pub mod repo; 14 16 pub mod types; 15 17 pub mod client; 18 + pub mod consts; 19 + pub mod scan; 16 20 17 21 #[get("/")] 18 22 async fn index(_req: HttpRequest) -> HttpResponse { ··· 49 53 50 54 let pool = PgPoolOptions::new().max_connections(5).connect(&env::var("XATA_POSTGRES_URL")?).await?; 51 55 let conn = Arc::new(pool); 56 + 57 + let cloned_conn = conn.clone(); 58 + 59 + thread::spawn(move || { 60 + let rt = tokio::runtime::Runtime::new().unwrap(); 61 + rt.block_on( 62 + scan_googledrive(cloned_conn) 63 + )?; 64 + Ok::<(), Error>(()) 65 + }); 52 66 53 67 let conn = conn.clone(); 54 68 HttpServer::new(move || {
+24
crates/googledrive/src/repo/google_drive_path.rs
··· 1 + use sqlx::{Pool, Postgres}; 2 + 3 + use crate::{types::file::File, xata::track::Track}; 4 + 5 + pub async fn create_google_drive_path( 6 + pool: &Pool<Postgres>, 7 + file: &File, 8 + track: &Track, 9 + google_drive_id: &str 10 + ) -> Result<(), sqlx::Error> { 11 + sqlx::query(r#" 12 + INSERT INTO google_drive_paths (google_drive_id, file_id, track_id, name) 13 + VALUES ($1, $2, $3, $4) 14 + ON CONFLICT (google_drive_id, file_id, track_id) DO NOTHING 15 + "#) 16 + .bind(google_drive_id) 17 + .bind(&file.id) 18 + .bind(&track.xata_id) 19 + .bind(&file.name) 20 + .execute(pool) 21 + .await?; 22 + 23 + Ok(()) 24 + }
+27 -1
crates/googledrive/src/repo/google_drive_token.rs
··· 5 5 6 6 pub async fn find_google_drive_refresh_token(pool: &Pool<Postgres>, did: &str) -> Result<Option<String>, Error> { 7 7 let results: Vec<GoogleDriveTokenWithDid> = sqlx::query_as(r#" 8 - SELECT * FROM google_drive gd 8 + SELECT 9 + gd.xata_id, 10 + gd.xata_version, 11 + gd.xata_createdat, 12 + gd.xata_updatedat, 13 + u.did, 14 + gt.refresh_token 15 + FROM google_drive gd 9 16 LEFT JOIN users u ON gd.user_id = u.xata_id 10 17 LEFT JOIN google_drive_tokens gt ON gd.google_drive_token_id = gt.xata_id 11 18 WHERE u.did = $1 ··· 20 27 21 28 Ok(Some(results[0].refresh_token.clone())) 22 29 } 30 + 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#" 33 + SELECT 34 + gd.xata_id, 35 + gd.xata_version, 36 + gd.xata_createdat, 37 + gd.xata_updatedat, 38 + u.did, 39 + gt.refresh_token 40 + FROM google_drive gd 41 + LEFT JOIN users u ON gd.user_id = u.xata_id 42 + LEFT JOIN google_drive_tokens gt ON gd.google_drive_token_id = gt.xata_id 43 + "#) 44 + .fetch_all(pool) 45 + .await?; 46 + 47 + Ok(results) 48 + }
+2
crates/googledrive/src/repo/mod.rs
··· 1 + pub mod google_drive_path; 1 2 pub mod google_drive_token; 3 + pub mod track;
+19
crates/googledrive/src/repo/track.rs
··· 1 + use anyhow::Error; 2 + use sqlx::{Pool, Postgres}; 3 + 4 + use crate::xata::track::Track; 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#" 8 + SELECT * FROM tracks WHERE sha256 = $1 9 + "#) 10 + .bind(sha256) 11 + .fetch_all(pool) 12 + .await?; 13 + 14 + if results.len() == 0 { 15 + return Ok(None); 16 + } 17 + 18 + Ok(Some(results[0].clone())) 19 + }
+322
crates/googledrive/src/scan.rs
··· 1 + use std::{env, io::Write, path::Path, sync::Arc}; 2 + 3 + use anyhow::Error; 4 + use futures::future::BoxFuture; 5 + use lofty::{file::TaggedFileExt, picture::{MimeType, Picture}, probe::Probe, tag::Accessor}; 6 + use owo_colors::OwoColorize; 7 + use reqwest::{multipart, Client}; 8 + use sqlx::{Pool, Postgres}; 9 + use symphonia::core::{formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint}; 10 + use tempfile::TempDir; 11 + 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_tokens, track::get_track_by_hash}, token::generate_token, types::file::{File, FileList}}; 13 + 14 + 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 + )?; 21 + 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 + let access_token = client.access_token.clone(); 26 + scan_audio_files( 27 + pool.clone(), 28 + music_dir.id.clone(), 29 + access_token, 30 + token.did.clone(), 31 + token.xata_id.clone() 32 + ).await?; 33 + } 34 + Ok(()) 35 + } 36 + 37 + pub fn scan_audio_files( 38 + pool: Arc<Pool<Postgres>>, 39 + file_id: String, 40 + access_token: String, 41 + did: String, 42 + google_drive_id: String, 43 + ) -> BoxFuture<'static, Result<(), Error>> { 44 + Box::pin(async move { 45 + let client = Client::new(); 46 + let url = format!("{}/files/{}", BASE_URL, file_id); 47 + let res = client.get(&url) 48 + .bearer_auth(&access_token) 49 + .query(&[ 50 + ("fields", "id, name, mimeType, parents"), 51 + ]) 52 + .send() 53 + .await?; 54 + 55 + let file = res.json::<File>().await?; 56 + 57 + if file.mime_type == "application/vnd.google-apps.folder" { 58 + println!("Scanning folder: {}", file.name.bright_green()); 59 + 60 + let url = format!("{}/files", BASE_URL); 61 + let res = client.get(&url) 62 + .bearer_auth(&access_token) 63 + .query(&[ 64 + ("q", format!("'{}' in parents", file.id).as_str()), 65 + ("fields", "files(id, name, mimeType, parents)"), 66 + ("orderBy", "name"), 67 + ]) 68 + .send() 69 + .await?; 70 + let filelist = res.json::<FileList>().await?; 71 + 72 + for file in filelist.files { 73 + scan_audio_files( 74 + pool.clone(), 75 + file.id, 76 + access_token.clone(), 77 + did.clone(), 78 + google_drive_id.clone() 79 + ).await?; 80 + tokio::time::sleep(std::time::Duration::from_secs(3)).await; 81 + } 82 + 83 + return Ok(()); 84 + } 85 + 86 + if !AUDIO_EXTENSIONS 87 + .into_iter() 88 + .any(|ext| file.name.ends_with(&format!(".{}", ext))) 89 + { 90 + return Ok(()); 91 + } 92 + 93 + println!("Downloading file: {}", file.name.bright_green()); 94 + 95 + let client = Client::new(); 96 + 97 + let url = format!("{}/files/{}", BASE_URL, file_id); 98 + let res = client.get(&url) 99 + .bearer_auth(&access_token) 100 + .query(&[ 101 + ("alt", "media"), 102 + ]) 103 + .send() 104 + .await?; 105 + 106 + let bytes = res.bytes().await?; 107 + 108 + let temp_dir = TempDir::new()?; 109 + let tmppath = temp_dir.path().join(&format!("{}", file.name)); 110 + let mut tmpfile = std::fs::File::create(&tmppath)?; 111 + tmpfile.write_all(&bytes)?; 112 + 113 + println!("Reading file: {}", &tmppath.clone().display().to_string().bright_green()); 114 + 115 + let tagged_file = match Probe::open(&tmppath)?.read() 116 + { 117 + Ok(tagged_file) => tagged_file, 118 + Err(e) => { 119 + println!("Error opening file: {}", e); 120 + return Ok(()); 121 + } 122 + }; 123 + 124 + let primary_tag = tagged_file.primary_tag(); 125 + let tag = match primary_tag { 126 + Some(tag) => tag, 127 + None => { 128 + println!("No tag found in file"); 129 + return Ok(()); 130 + } 131 + }; 132 + 133 + let pictures = tag.pictures(); 134 + 135 + println!("Title: {}", tag.get_string(&lofty::tag::ItemKey::TrackTitle).unwrap_or_default().bright_green()); 136 + println!("Artist: {}", tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default().bright_green()); 137 + println!("Album Artist: {}", tag.get_string(&lofty::tag::ItemKey::AlbumArtist).unwrap_or_default().bright_green()); 138 + println!("Album: {}", tag.get_string(&lofty::tag::ItemKey::AlbumTitle).unwrap_or_default().bright_green()); 139 + println!("Lyrics: {}", tag.get_string(&lofty::tag::ItemKey::Lyrics).unwrap_or_default().bright_green()); 140 + println!("Year: {}", tag.year().unwrap_or_default().bright_green()); 141 + println!("Track Number: {}", tag.track().unwrap_or_default().bright_green()); 142 + println!("Track Total: {}", tag.track_total().unwrap_or_default().bright_green()); 143 + println!("Release Date: {:?}", tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).unwrap_or_default().bright_green()); 144 + println!("Recording Date: {:?}", tag.get_string(&lofty::tag::ItemKey::RecordingDate).unwrap_or_default().bright_green()); 145 + println!("Copyright Message: {}", tag.get_string(&lofty::tag::ItemKey::CopyrightMessage).unwrap_or_default().bright_green()); 146 + println!("Pictures: {:?}", pictures); 147 + 148 + let title = tag.get_string(&lofty::tag::ItemKey::TrackTitle).unwrap_or_default(); 149 + let artist = tag.get_string(&lofty::tag::ItemKey::TrackArtist).unwrap_or_default(); 150 + let album_artist = tag.get_string(&lofty::tag::ItemKey::AlbumArtist).unwrap_or_default(); 151 + let album = tag.get_string(&lofty::tag::ItemKey::AlbumTitle).unwrap_or_default(); 152 + let access_token = generate_token(&did)?; 153 + 154 + // check if track exists 155 + // 156 + // if not, create track 157 + // upload album artist 158 + // 159 + // link path to track 160 + 161 + let hash = sha256::digest( 162 + format!("{} - {} - {}", title, artist, album).to_lowercase(), 163 + ); 164 + 165 + let track = get_track_by_hash(&pool, &hash).await?; 166 + let duration = get_track_duration(&tmppath).await?; 167 + let albumart_id = md5::compute(&format!("{} - {}", album_artist, album).to_lowercase()); 168 + let albumart_id = format!("{:x}", albumart_id); 169 + 170 + match track { 171 + Some(track) => { 172 + println!("Track exists: {}", title.bright_green()); 173 + create_google_drive_path( 174 + &pool, 175 + &file, 176 + &track, 177 + &google_drive_id, 178 + ) 179 + .await?; 180 + }, 181 + None => { 182 + println!("Creating track: {}", title.bright_green()); 183 + 184 + let albumart = upload_album_cover(albumart_id.into(), pictures, &access_token).await?; 185 + 186 + let client = Client::new(); 187 + const URL: &str = "https://api.rocksky.app/tracks"; 188 + let response = client 189 + .post(URL) 190 + .header("Authorization", format!("Bearer {}", access_token)) 191 + .json(&serde_json::json!({ 192 + "title": tag.get_string(&lofty::tag::ItemKey::TrackTitle), 193 + "album": tag.get_string(&lofty::tag::ItemKey::AlbumTitle), 194 + "artist": tag.get_string(&lofty::tag::ItemKey::TrackArtist), 195 + "albumArtist": tag.get_string(&lofty::tag::ItemKey::AlbumArtist), 196 + "duration": duration, 197 + "trackNumber": tag.track(), 198 + "releaseDate": tag.get_string(&lofty::tag::ItemKey::OriginalReleaseDate).map(|date| match date.contains("-") { 199 + true => Some(date), 200 + false => None, 201 + }), 202 + "year": tag.year(), 203 + "discNumber": tag.disk(), 204 + "composer": tag.get_string(&lofty::tag::ItemKey::Composer), 205 + "albumArt": match albumart { 206 + Some(albumart) => Some(format!("https://cdn.rocksky.app/covers/{}", albumart)), 207 + None => None 208 + }, 209 + "lyrics": tag.get_string(&lofty::tag::ItemKey::Lyrics), 210 + "copyrightMessage": tag.get_string(&lofty::tag::ItemKey::CopyrightMessage), 211 + })) 212 + .send() 213 + .await?; 214 + println!("Track Saved: {} {}", title, response.status()); 215 + 216 + 217 + let track = get_track_by_hash(&pool, &hash).await?; 218 + if let Some(track) = track { 219 + create_google_drive_path( 220 + &pool, 221 + &file, 222 + &track, 223 + &google_drive_id, 224 + ) 225 + .await?; 226 + return Ok(()); 227 + } 228 + 229 + println!("Failed to create track: {}", title.bright_green()); 230 + } 231 + } 232 + 233 + Ok(()) 234 + }) 235 + } 236 + 237 + pub async fn upload_album_cover(name: String, pictures: &[Picture], token: &str) -> Result<Option<String>, Error> { 238 + if pictures.is_empty() { 239 + return Ok(None); 240 + } 241 + 242 + let picture = &pictures[0]; 243 + 244 + let buffer = match picture.mime_type() { 245 + Some(MimeType::Jpeg) => Some(picture.data().to_vec()), 246 + Some(MimeType::Png) => Some(picture.data().to_vec()), 247 + Some(MimeType::Gif) => Some(picture.data().to_vec()), 248 + Some(MimeType::Bmp) => Some(picture.data().to_vec()), 249 + Some(MimeType::Tiff) => Some(picture.data().to_vec()), 250 + _ => None 251 + }; 252 + 253 + if buffer.is_none() { 254 + return Ok(None); 255 + } 256 + 257 + let buffer = buffer.unwrap(); 258 + 259 + 260 + let ext = match picture.mime_type() { 261 + Some(MimeType::Jpeg) => "jpg", 262 + Some(MimeType::Png) => "png", 263 + Some(MimeType::Gif) => "gif", 264 + Some(MimeType::Bmp) => "bmp", 265 + Some(MimeType::Tiff) => "tiff", 266 + _ => { 267 + return Ok(None); 268 + } 269 + }; 270 + 271 + let name = format!("{}.{}", name, ext); 272 + 273 + let part = multipart::Part::bytes(buffer).file_name(name.clone()); 274 + let form = multipart::Form::new().part("file", part); 275 + let client = Client::new(); 276 + 277 + const URL: &str = "https://uploads.rocksky.app"; 278 + 279 + let response = client 280 + .post(URL) 281 + .header("Authorization", format!("Bearer {}", token)) 282 + .multipart(form) 283 + .send() 284 + .await?; 285 + 286 + println!("Cover uploaded: {}", response.status()); 287 + 288 + Ok(Some(name)) 289 + } 290 + 291 + pub async fn get_track_duration(path: &Path) -> Result<u64, Error> { 292 + let duration = 0; 293 + let media_source = MediaSourceStream::new(Box::new(std::fs::File::open(path)?), Default::default()); 294 + let mut hint = Hint::new(); 295 + 296 + if let Some(extension) = path.extension() { 297 + if let Some(extension) = extension.to_str() { 298 + hint.with_extension(extension); 299 + } 300 + } 301 + 302 + 303 + let meta_opts = MetadataOptions::default(); 304 + let format_opts = FormatOptions::default(); 305 + 306 + let probed = match symphonia::default::get_probe().format(&hint, media_source, &format_opts, &meta_opts) { 307 + Ok(probed) => probed, 308 + Err(_) => { 309 + println!("Error probing file"); 310 + return Ok(duration); 311 + }, 312 + }; 313 + 314 + if let Some(track) = probed.format.tracks().first() { 315 + if let Some(duration) = track.codec_params.n_frames { 316 + if let Some(sample_rate) = track.codec_params.sample_rate { 317 + return Ok((duration as f64 / sample_rate as f64) as u64 * 1000); 318 + } 319 + } 320 + } 321 + Ok(duration) 322 + }
+64
crates/googledrive/src/token.rs
··· 1 + use std::env; 2 + 3 + use anyhow::Error; 4 + use jsonwebtoken::DecodingKey; 5 + use jsonwebtoken::EncodingKey; 6 + use jsonwebtoken::Header; 7 + use jsonwebtoken::Validation; 8 + use serde::{Deserialize, Serialize}; 9 + 10 + #[derive(Debug, Serialize, Deserialize)] 11 + pub struct Claims { 12 + exp: usize, 13 + iat: usize, 14 + did: String, 15 + } 16 + 17 + pub fn generate_token(did: &str) -> Result<String, Error> { 18 + if env::var("JWT_SECRET").is_err() { 19 + return Err(Error::msg("JWT_SECRET is not set")); 20 + } 21 + 22 + let claims = Claims { 23 + exp: chrono::Utc::now().timestamp() as usize + 3600, 24 + iat: chrono::Utc::now().timestamp() as usize, 25 + did: did.to_string(), 26 + }; 27 + 28 + jsonwebtoken::encode( 29 + &Header::default(), 30 + &claims, 31 + &EncodingKey::from_secret(env::var("JWT_SECRET")?.as_ref()), 32 + ) 33 + .map_err(Into::into) 34 + } 35 + 36 + pub fn decode_token(token: &str) -> Result<Claims, Error> { 37 + if env::var("JWT_SECRET").is_err() { 38 + return Err(Error::msg("JWT_SECRET is not set")); 39 + } 40 + 41 + jsonwebtoken::decode::<Claims>( 42 + token, 43 + &DecodingKey::from_secret(env::var("JWT_SECRET")?.as_ref()), 44 + &Validation::default(), 45 + ) 46 + .map(|data| data.claims) 47 + .map_err(Into::into) 48 + } 49 + 50 + #[cfg(test)] 51 + mod tests { 52 + use dotenv::dotenv; 53 + 54 + use super::*; 55 + 56 + #[test] 57 + fn test_generate_token() { 58 + dotenv().ok(); 59 + let token = generate_token("did:plc:7vdlgi2bflelz7mmuxoqjfcr").unwrap(); 60 + let claims = decode_token(&token).unwrap(); 61 + 62 + assert_eq!(claims.did, "did:plc:7vdlgi2bflelz7mmuxoqjfcr"); 63 + } 64 + }