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